@lensmcp/cluster 1.19.0 → 1.20.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.
- package/executors/gateway/gateway-errors.d.ts +2 -2
- package/executors/gateway/gateway-errors.js +1 -1
- package/executors/gateway/gateway.impl.js +1 -1
- package/executors/gateway/gateway.lib.d.ts +1 -1
- package/executors/gateway/gateway.lib.js +1 -1
- package/executors/gateway/runtime/control.js +1 -1
- package/executors/gateway/runtime/discovery.d.ts +5 -1
- package/executors/gateway/runtime/discovery.js +1 -1
- package/executors/gateway/runtime/edge.d.ts +1 -1
- package/executors/gateway/runtime/edge.js +1 -1
- package/executors/gateway/runtime/hmr-reload.d.ts +2 -2
- package/executors/gateway/runtime/hmr-reload.js +1 -1
- package/executors/gateway/runtime/lens-children.d.ts +3 -7
- package/executors/gateway/runtime/lens-children.js +1 -1
- package/executors/gateway/runtime/lifecycle.d.ts +11 -3
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/types.d.ts +57 -1
- package/executors/gateway/runtime/types.js +1 -1
- package/executors/gateway/runtime/upgrade.js +1 -1
- package/package.json +4 -4
|
@@ -18,7 +18,7 @@ import type * as http from 'node:http';
|
|
|
18
18
|
* This keeps the gateway generic (no consumer-specific keys baked in beyond the neutral `gateway.*`
|
|
19
19
|
* namespace) while integrating cleanly with a localized-error system.
|
|
20
20
|
*/
|
|
21
|
-
export type GatewayErrorReason = 'bad_request' | 'unauthorized' | 'forbidden' | 'not_found' | 'rate_limited' | 'unavailable' | 'internal';
|
|
21
|
+
export type GatewayErrorReason = 'bad_request' | 'unauthorized' | 'forbidden' | 'not_found' | 'rate_limited' | 'starting' | 'unavailable' | 'internal';
|
|
22
22
|
/** Reason → stable, neutral localized key (the consumer translates the `gateway.*` namespace). */
|
|
23
23
|
export declare const GATEWAY_ERROR_KEY: Record<GatewayErrorReason, string>;
|
|
24
24
|
/**
|
|
@@ -31,5 +31,5 @@ export declare function ensureRequestId(req: http.IncomingMessage): string;
|
|
|
31
31
|
* Build the JSON edge-error body `{ key, status, traceId, message? }`. `detail` becomes the dev-facing
|
|
32
32
|
* `message` (e.g. the old plain-text reason), never surfaced to end users.
|
|
33
33
|
*/
|
|
34
|
-
export declare function gatewayErrorBody(status: number, reason: GatewayErrorReason, traceId: string, detail?: string): string;
|
|
34
|
+
export declare function gatewayErrorBody(status: number, reason: GatewayErrorReason, traceId: string, detail?: string, metadata?: Readonly<Record<string, unknown>>): string;
|
|
35
35
|
export declare const GATEWAY_ERROR_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var d=Object.defineProperty;var n=(e,t)=>d(e,"name",{value:t,configurable:!0});var s=Object.defineProperty,r=n((e,t)=>s(e,"name",{value:t,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_ERROR_CONTENT_TYPE=exports.GATEWAY_ERROR_KEY=void 0,exports.ensureRequestId=ensureRequestId,exports.gatewayErrorBody=gatewayErrorBody,exports.GATEWAY_ERROR_KEY={bad_request:"gateway.badRequest",unauthorized:"gateway.unauthorized",forbidden:"gateway.forbidden",not_found:"gateway.notFound",rate_limited:"gateway.rateLimited",starting:"gateway.starting",unavailable:"gateway.unavailable",internal:"gateway.internal"};const headerValue=r(e=>typeof e=="string"&&e!==""?e:Array.isArray(e)?e[0]:void 0,"headerValue"),mintId=r(()=>Date.now().toString(36)+Math.random().toString(36).slice(2,12),"mintId");function ensureRequestId(e){const t=headerValue(e.headers["x-request-id"]);if(t)return t;const a=mintId();return e.headers["x-request-id"]=a,a}n(ensureRequestId,"ensureRequestId"),r(ensureRequestId,"ensureRequestId");function gatewayErrorBody(e,t,a,o,i){return JSON.stringify({...i??{},key:exports.GATEWAY_ERROR_KEY[t],status:e,traceId:a,...o?{message:o}:{}})}n(gatewayErrorBody,"gatewayErrorBody"),r(gatewayErrorBody,"gatewayErrorBody"),exports.GATEWAY_ERROR_CONTENT_TYPE="application/json; charset=utf-8";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var p=Object.defineProperty;var a=(t,r)=>p(t,"name",{value:r,configurable:!0});var u=Object.defineProperty,o=a((t,r)=>u(t,"name",{value:r,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=gatewayExecutor;const devkit_1=require("@nx/devkit"),gateway_lib_1=require("./gateway.lib"),server_1=require("./runtime/server");async function gatewayExecutor(t,r){process.on("uncaughtException",e=>{if((e.syscall==="read"||e.syscall==="write")&&server_1.BENIGN_SOCKET_ERRNOS.has(e.code??"")){console.warn(`[gateway] survived a stray socket ${e.syscall} error (${e.code}) \u2014 one connection dropped, the daemon lives`);return}console.error(e),process.exit(1)});let c;try{let e;try{const s=await(0,devkit_1.createProjectGraphAsync)();e=Object.fromEntries(Object.entries(s.dependencies).map(([n,i])=>[n,i.map(l=>l.target)]))}catch{}c=await(0,gateway_lib_1.startGateway)(t,{...r,...e?{projectDependencies:e}:{}})}catch(e){return console.error(e.message),{success:!1}}return new Promise(e=>{const s=o(()=>{c.stop().then(()=>e({success:!0}))},"stop");process.on("SIGINT",s),process.on("SIGTERM",s)})}a(gatewayExecutor,"gatewayExecutor"),o(gatewayExecutor,"gatewayExecutor"),module.exports=gatewayExecutor,module.exports.default=gatewayExecutor,module.exports.startGateway=gateway_lib_1.startGateway;
|
|
@@ -14,5 +14,5 @@ export { hostMatches } from './manifest';
|
|
|
14
14
|
export { startGateway } from './runtime/server';
|
|
15
15
|
export { verifyDevJwt, stampIdentity } from './runtime/dev-auth';
|
|
16
16
|
export { baseDomainOf, deriveMcpHttpPort, readLensScope, type LensScope } from './runtime/scope';
|
|
17
|
-
export { pickSock, discoverRoutes } from './runtime/discovery';
|
|
17
|
+
export { pickSock, discoverRoutes, projectSourceRoots } from './runtime/discovery';
|
|
18
18
|
export type { GatewayContext, ClusterProjectDecl, SockPool, Route, LensFrontend, ServiceCtl, GatewayRuntimeOptions, GatewayHandle, GatewayHooks, GatewayPhase, HookHandler, HookContext, } from './runtime/types';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var s=Object.defineProperty;var n=(t,r)=>s(t,"name",{value:r,configurable:!0});var o=Object.defineProperty,e=n((t,r)=>o(t,"name",{value:r,configurable:!0}),"e");Object.defineProperty(exports,"__esModule",{value:!0}),exports.projectSourceRoots=exports.discoverRoutes=exports.pickSock=exports.readLensScope=exports.deriveMcpHttpPort=exports.baseDomainOf=exports.stampIdentity=exports.verifyDevJwt=exports.startGateway=exports.hostMatches=void 0;var manifest_1=require("./manifest");Object.defineProperty(exports,"hostMatches",{enumerable:!0,get:e(function(){return manifest_1.hostMatches},"get")});var server_1=require("./runtime/server");Object.defineProperty(exports,"startGateway",{enumerable:!0,get:e(function(){return server_1.startGateway},"get")});var dev_auth_1=require("./runtime/dev-auth");Object.defineProperty(exports,"verifyDevJwt",{enumerable:!0,get:e(function(){return dev_auth_1.verifyDevJwt},"get")}),Object.defineProperty(exports,"stampIdentity",{enumerable:!0,get:e(function(){return dev_auth_1.stampIdentity},"get")});var scope_1=require("./runtime/scope");Object.defineProperty(exports,"baseDomainOf",{enumerable:!0,get:e(function(){return scope_1.baseDomainOf},"get")}),Object.defineProperty(exports,"deriveMcpHttpPort",{enumerable:!0,get:e(function(){return scope_1.deriveMcpHttpPort},"get")}),Object.defineProperty(exports,"readLensScope",{enumerable:!0,get:e(function(){return scope_1.readLensScope},"get")});var discovery_1=require("./runtime/discovery");Object.defineProperty(exports,"pickSock",{enumerable:!0,get:e(function(){return discovery_1.pickSock},"get")}),Object.defineProperty(exports,"discoverRoutes",{enumerable:!0,get:e(function(){return discovery_1.discoverRoutes},"get")}),Object.defineProperty(exports,"projectSourceRoots",{enumerable:!0,get:e(function(){return discovery_1.projectSourceRoots},"get")});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var v=Object.defineProperty;var d=(o,r)=>v(o,"name",{value:r,configurable:!0});var w=Object.defineProperty,y=d((o,r)=>w(o,"name",{value:r,configurable:!0}),"y");Object.defineProperty(exports,"__esModule",{value:!0}),exports.controlSocketPath=controlSocketPath,exports.startControlServer=startControlServer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery");function controlSocketPath(){const o=process.env.LENSMCP_HOME||os.homedir();return path.join(o,".lensmcp","control.sock")}d(controlSocketPath,"controlSocketPath"),y(controlSocketPath,"controlSocketPath");function readJsonBody(o){return new Promise((r,c)=>{let s="";o.on("data",u=>{s+=u,s.length>1e6&&c(new Error("body too large"))}),o.on("end",()=>{try{r(s?JSON.parse(s):{})}catch(u){c(u)}}),o.on("error",c)})}d(readJsonBody,"readJsonBody"),y(readJsonBody,"readJsonBody");function startControlServer(o){const{rt:r}=o,c=controlSocketPath();fs.mkdirSync(path.dirname(c),{recursive:!0});try{fs.unlinkSync(c)}catch{}const s=y((n,i,a)=>{n.writeHead(i,{"content-type":"application/json"}),n.end(JSON.stringify(a))},"send"),u=http.createServer((n,i)=>{(async()=>{try{const a=(n.url??"").split("?")[0];if(n.method==="GET"&&a==="/list")return s(i,200,{workspaces:r.registry.keys().map(e=>{const t=r.registry.get(e);return{key:e,root:t?.root,routes:t?.routes.length??0,services:t?.services.length??0}})});if(n.method==="GET"&&a==="/status")return s(i,200,{daemon:{pid:process.pid,wsKey:r.wsKey},workspaces:r.registry.keys().map(e=>{const t=r.registry.get(e);return{key:e,root:t?.root,routes:t?.routes.length??0,services:t?.services.length??0,isDaemon:e===r.wsKey}}),services:r.registry.allServices().map(e=>({project:e.project,wsKey:e.wsKey,host:e.decl.host,state:e.state}))});if(n.method==="POST"&&a==="/register"){const e=await readJsonBody(n);if(!e.wsKey||!e.root)return s(i,400,{error:"wsKey and root are required"});const t=e.projects??{},l=(0,discovery_1.projectSourceRoots)(e.root,t,e.projectDependencies),{routes:p,services:g}=(0,discovery_1.discoverRoutes)(e.root,t,void 0,e.wsKey,l),h={wsKey:e.wsKey,root:e.root,routes:p,services:g};return r.registry.register(h),await o.onRegister?.(h),r.rebuildRoutes(),s(i,200,{ok:!0,wsKey:e.wsKey,routes:p.length,services:g.length,workspaces:r.registry.keys()})}if(n.method==="POST"&&a==="/unregister"){const e=await readJsonBody(n);if(!e.wsKey)return s(i,400,{error:"wsKey is required"});const t=r.registry.unregister(e.wsKey);if(t){for(const l of t.services)o.killService(l,"workspace unregistered");o.onUnregister?.(t)}return r.rebuildRoutes(),s(i,200,{ok:!0,removed:!!t,workspaces:r.registry.keys()})}s(i,404,{error:"not found"})}catch(a){s(i,500,{error:a.message})}})()});return u.listen(c),{socketPath:c,close:y(()=>{u.close();try{fs.unlinkSync(c)}catch{}},"close")}}d(startControlServer,"startControlServer"),y(startControlServer,"startControlServer");
|
|
@@ -10,7 +10,11 @@ export declare function lensPortFromUpstream(upstream?: string): number | undefi
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function discoverRoutes(rootDir: string, projects: Record<string, {
|
|
12
12
|
root: string;
|
|
13
|
-
}>, sockBaseDir?: string, wsKey?: string): {
|
|
13
|
+
}>, sockBaseDir?: string, wsKey?: string, sourceRootsByProject?: Readonly<Record<string, string[]>>): {
|
|
14
14
|
routes: Route[];
|
|
15
15
|
services: ServiceCtl[];
|
|
16
16
|
};
|
|
17
|
+
/** Resolve each project's own root plus every transitive Nx dependency root. */
|
|
18
|
+
export declare function projectSourceRoots(workspaceRoot: string, projects: Record<string, {
|
|
19
|
+
root: string;
|
|
20
|
+
}>, dependencies?: Readonly<Record<string, readonly string[]>>): Record<string, string[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var b=Object.defineProperty;var a=(e,r)=>b(e,"name",{value:r,configurable:!0});var S=Object.defineProperty,u=a((e,r)=>S(e,"name",{value:r,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.pickSock=pickSock,exports.lensPortFromUpstream=lensPortFromUpstream,exports.discoverRoutes=discoverRoutes,exports.projectSourceRoots=projectSourceRoots;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),manifest_1=require("../manifest"),scope_1=require("./scope");function pickSock(e,r=1500){const p=Date.now();if(p-e.scannedAt>r){try{e.socks=fs.readdirSync(e.dir).filter(s=>/^child-\d+\.sock$/.test(s)).map(s=>path.join(e.dir,s))}catch{e.socks=[]}e.scannedAt=p}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}a(pickSock,"pickSock"),u(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const r=/:(\d+)/.exec(e);return r?Number(r[1]):void 0}a(lensPortFromUpstream,"lensPortFromUpstream"),u(lensPortFromUpstream,"lensPortFromUpstream");function discoverRoutes(e,r,p=os.tmpdir(),s="",l={}){const d=u(i=>s?path.join(p,s,`${i}-devserver`):path.join(p,`${i}-devserver`),"sockDirFor"),c=[],h=[],o=[];let n;for(const[i,x]of Object.entries(r)){let t;try{const f=JSON.parse(fs.readFileSync(path.join(e,x.root,"project.json"),"utf8"));t=f.cluster??f.davnx}catch{continue}if(!t)continue;let v,j,m,y;if(t.service)t.lens&&console.warn(`[gateway] ${i}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),v={dir:d(t.service),socks:[],idx:-1,scannedAt:0},j={project:i,decl:t,root:e,wsKey:s,pool:v,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},h.push(j);else if(t.lens){const f=t.port??lensPortFromUpstream(t.upstream)??5173;m=`http://localhost:${f}`,y={project:i,...s?{workspaceKey:s}:{},projectRoot:path.join(e,x.root),port:f,...l[i]?.length?{sourceRoots:l[i]}:{}}}else if(m=t.upstream??(t.port?`http://localhost:${t.port}`:void 0),!m){console.warn(`[gateway] ${i}: no service/upstream/port \u2014 skipped.`);continue}const k={project:i,prependPrefix:t.prependPrefix,target:m,pool:v,svc:j,prefix:t.path,auth:t.auth,lens:y};t.host&&(c.push({...k,host:t.host}),o.push(t.host),v&&t.internalHost!==!1&&c.push({...k,host:typeof t.internalHost=="string"?t.internalHost:`internal.${t.host}`,internal:!0})),t.default&&(n={...k,prefix:void 0})}return n&&(n.apex=(0,scope_1.baseDomainOf)(o),c.push(n)),(0,manifest_1.sortBySpecificity)(c),{routes:c,services:h}}a(discoverRoutes,"discoverRoutes"),u(discoverRoutes,"discoverRoutes");function projectSourceRoots(e,r,p={}){const s={};for(const l of Object.keys(r)){const d=new Set,c=u(o=>{if(!d.has(o)){d.add(o);for(const n of p[o]??[])c(n)}},"visit");c(l);const h=[...d].map(o=>r[o]?.root).filter(o=>typeof o=="string").map(o=>path.resolve(e,o)).filter(o=>{const n=path.relative(e,o);return n===""||n!==".."&&!n.startsWith(`..${path.sep}`)&&!path.isAbsolute(n)});s[l]=[...new Set(h)]}return s}a(projectSourceRoots,"projectSourceRoots"),u(projectSourceRoots,"projectSourceRoots");
|
|
@@ -10,7 +10,7 @@ import { type GatewayErrorReason } from '../gateway-errors';
|
|
|
10
10
|
* `text/plain`) + an `x-request-id` header — the SAME localized contract a service returns, so a browser
|
|
11
11
|
* app renders `t(key)` and surfaces the trace id. Mirrors the prod gateway's `sendError`.
|
|
12
12
|
*/
|
|
13
|
-
export declare const sendError: (res: http.ServerResponse, req: http.IncomingMessage, status: number, reason: GatewayErrorReason, detail?: string) =>
|
|
13
|
+
export declare const sendError: (res: http.ServerResponse, req: http.IncomingMessage, status: number, reason: GatewayErrorReason, detail?: string, metadata?: Readonly<Record<string, unknown>>, headers?: http.OutgoingHttpHeaders) => string;
|
|
14
14
|
/**
|
|
15
15
|
* The gateway OWNS these headers — strip any client-supplied copy at ingress so identity + trust can
|
|
16
16
|
* only originate from the gateway (mirrors the prod gateway's stripTrust). Applied at the very top of
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var T=Object.defineProperty;var n=(r,e)=>T(r,"name",{value:e,configurable:!0});var i=Object.defineProperty,s=n((r,e)=>i(r,"name",{value:e,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.PREFLIGHT_METHODS_ROUTED=exports.PREFLIGHT_METHODS_NOROUTE=exports.corsPreflight=exports.cors=exports.GATEWAY_OWNED_HEADERS=exports.sendError=void 0;const gateway_errors_1=require("../gateway-errors"),sendError=s((r,e,o,a,E,c,d)=>{const t=(0,gateway_errors_1.ensureRequestId)(e);return r.headersSent||r.writeHead(o,{"content-type":gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE,"x-request-id":t,...d??{}}),r.end((0,gateway_errors_1.gatewayErrorBody)(o,a,t,E,c)),t},"sendError");exports.sendError=sendError,exports.GATEWAY_OWNED_HEADERS=["x-internal-token","x-api-key-id","x-user-id","x-tenant-id","x-user-roles","x-user-permissions"];const cors=s((r,e)=>{const o=String(r.headers.origin??"*");e.setHeader("access-control-allow-origin",o),e.setHeader("access-control-allow-credentials","true"),e.setHeader("vary","Origin")},"cors");exports.cors=cors;const corsPreflight=s((r,e,o)=>{(0,exports.cors)(r,e),e.writeHead(204,{"access-control-allow-methods":o,"access-control-allow-headers":String(r.headers["access-control-request-headers"]??"*")}),e.end()},"corsPreflight");exports.corsPreflight=corsPreflight,exports.PREFLIGHT_METHODS_NOROUTE="GET,HEAD,OPTIONS",exports.PREFLIGHT_METHODS_ROUTED="GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS";
|
|
@@ -35,7 +35,7 @@ export declare function encodeWsTextFrame(payload: string): Buffer;
|
|
|
35
35
|
* `close`/`error`, so a long-lived gateway does not accumulate dead handles (a tab is opened and closed far
|
|
36
36
|
* more often than a pod is recycled). No-op for a project with no lens frontend.
|
|
37
37
|
*/
|
|
38
|
-
export declare function registerHmrClient(rt: GatewayRuntime,
|
|
38
|
+
export declare function registerHmrClient(rt: GatewayRuntime, frontendKey: string, socket: HmrClientSocket): void;
|
|
39
39
|
/**
|
|
40
40
|
* Push `full-reload` to every live tab of `project`. Returns how many sockets were written, so the caller can
|
|
41
41
|
* log it (and a test can assert it without a browser).
|
|
@@ -45,4 +45,4 @@ export declare function registerHmrClient(rt: GatewayRuntime, project: string, s
|
|
|
45
45
|
* proxied one. A write failure is swallowed per-socket: a tab that vanished mid-broadcast must never take
|
|
46
46
|
* down the recycle (or the daemon).
|
|
47
47
|
*/
|
|
48
|
-
export declare function broadcastFullReload(rt: GatewayRuntime,
|
|
48
|
+
export declare function broadcastFullReload(rt: GatewayRuntime, frontendKey: string): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var a=Object.defineProperty;var l=(n,e)=>a(n,"name",{value:e,configurable:!0});var c=Object.defineProperty,
|
|
1
|
+
"use strict";var a=Object.defineProperty;var l=(n,e)=>a(n,"name",{value:e,configurable:!0});var c=Object.defineProperty,f=l((n,e)=>c(n,"name",{value:e,configurable:!0}),"f");Object.defineProperty(exports,"__esModule",{value:!0}),exports.VITE_FULL_RELOAD_PAYLOAD=void 0,exports.encodeWsTextFrame=encodeWsTextFrame,exports.registerHmrClient=registerHmrClient,exports.broadcastFullReload=broadcastFullReload,exports.VITE_FULL_RELOAD_PAYLOAD='{"type":"full-reload"}';function encodeWsTextFrame(n){const e=Buffer.from(n,"utf8"),t=e.length;if(t<126)return Buffer.concat([Buffer.from([129,t]),e]);if(t<=65535){const r=Buffer.alloc(4);return r[0]=129,r[1]=126,r.writeUInt16BE(t,2),Buffer.concat([r,e])}const o=Buffer.alloc(10);return o[0]=129,o[1]=127,o.writeBigUInt64BE(BigInt(t),2),Buffer.concat([o,e])}l(encodeWsTextFrame,"encodeWsTextFrame"),f(encodeWsTextFrame,"encodeWsTextFrame");function registerHmrClient(n,e,t){const o=n.hmrClients??=new Map;let r=o.get(e);r||o.set(e,r=new Set),r.add(t);const s=f(()=>{r.delete(t),r.size===0&&o.delete(e)},"drop");t.once("close",s),t.once("error",s)}l(registerHmrClient,"registerHmrClient"),f(registerHmrClient,"registerHmrClient");function broadcastFullReload(n,e){const t=n.hmrClients?.get(e);if(!t||t.size===0)return 0;const o=encodeWsTextFrame(exports.VITE_FULL_RELOAD_PAYLOAD);let r=0;for(const s of t)if(!s.destroyed)try{s.write(o),r+=1}catch{}return r}l(broadcastFullReload,"broadcastFullReload"),f(broadcastFullReload,"broadcastFullReload");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type SpawnChild } from '@lensmcp/nx-plugin/lens-frontend';
|
|
2
2
|
import type { WorkspaceFragment } from './route-registry';
|
|
3
|
-
import type { GatewayRuntime, GatewayRuntimeOptions } from './types';
|
|
3
|
+
import type { GatewayRuntime, GatewayRuntimeOptions, LensWatcherHealth } from './types';
|
|
4
4
|
import { type Observability } from './observability';
|
|
5
5
|
export declare function probePortFree(port: number): Promise<boolean>;
|
|
6
6
|
/** True when something is LISTENING on `port` on loopback — the liveness signal the exit handler cannot
|
|
@@ -17,9 +17,7 @@ export declare function probePortListening(port: number, timeoutMs?: number): Pr
|
|
|
17
17
|
* timeout, bad JSON): the caller treats `null` as "no testimony" and keeps the legacy kill-first path,
|
|
18
18
|
* so a mixed-version cluster degrades to exactly the old behavior rather than a stuck pod.
|
|
19
19
|
*/
|
|
20
|
-
export declare function probeWatcherHealth(port: number, timeoutMs?: number): Promise<
|
|
21
|
-
lastFsSetEventAt: number;
|
|
22
|
-
} | null>;
|
|
20
|
+
export declare function probeWatcherHealth(port: number, timeoutMs?: number): Promise<LensWatcherHealth | null>;
|
|
23
21
|
/**
|
|
24
22
|
* Poll until `port` is actually BINDABLE, up to `timeoutMs`. Resolves `true` when it frees, `false` on
|
|
25
23
|
* timeout (the caller then spawns anyway and lets the normal heal handle the fallout).
|
|
@@ -65,7 +63,5 @@ export interface LensChildren {
|
|
|
65
63
|
}
|
|
66
64
|
export declare function createLensChildren(rt: GatewayRuntime, obs: Observability, options: GatewayRuntimeOptions, deps?: {
|
|
67
65
|
isPortFree?: (port: number) => Promise<boolean>;
|
|
68
|
-
probeWatcherHealth?: (port: number) => Promise<
|
|
69
|
-
lastFsSetEventAt: number;
|
|
70
|
-
} | null>;
|
|
66
|
+
probeWatcherHealth?: (port: number) => Promise<LensWatcherHealth | null>;
|
|
71
67
|
}): LensChildren;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var Z=Object.defineProperty;var L=(n,p)=>Z(n,"name",{value:p,configurable:!0});var X=Object.defineProperty,l=L((n,p)=>X(n,"name",{value:p,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.probeWatcherHealth=probeWatcherHealth,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.createLensChildren=createLensChildren;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_child_process_1=require("node:child_process"),lens_frontend_1=require("@lensmcp/nx-plugin/lens-frontend"),hmr_reload_1=require("./hmr-reload"),pod_env_1=require("./pod-env"),scope_1=require("./scope"),workspace_registry_1=require("./workspace-registry"),lifecycle_1=require("./lifecycle"),types_1=require("./types"),observability_1=require("./observability"),HEAL_BASE_MS=1500,HEAL_MAX_MS=3e4,HEAL_HEALTHY_MS=2e4;function tryBind(n,p){return new Promise(c=>{const h=net.createServer();h.unref(),h.once("error",d=>{const E=d.code??"";c(E==="EADDRINUSE"||E==="EACCES"?"in-use":"unavailable")}),h.listen(p===void 0?{port:n}:{port:n,host:p},()=>h.close(()=>c("free")))})}L(tryBind,"tryBind"),l(tryBind,"tryBind");async function probePortFree(n){for(const p of[void 0,"0.0.0.0","127.0.0.1","::1"])if(await tryBind(n,p)==="in-use")return!1;return!0}L(probePortFree,"probePortFree"),l(probePortFree,"probePortFree");function probeConnect(n,p,c){return new Promise(h=>{let d=!1;const E=net.createConnection({host:n,port:p});E.unref();const C=setTimeout(()=>P(!1),c);C.unref?.();function P(R){d||(d=!0,clearTimeout(C),E.destroy(),h(R))}L(P,"y"),l(P,"finish"),E.once("connect",()=>P(!0)),E.once("error",()=>P(!1))})}L(probeConnect,"probeConnect"),l(probeConnect,"probeConnect");async function probePortListening(n,p=1e3){return await probeConnect("127.0.0.1",n,p)?!0:probeConnect("::1",n,p)}L(probePortListening,"probePortListening"),l(probePortListening,"probePortListening");async function probeWatcherHealth(n,p=1500){try{const c=new AbortController,h=setTimeout(()=>c.abort(),p);h.unref?.();try{const d=await fetch(`http://127.0.0.1:${n}/__lensmcp/watcher-health`,{signal:c.signal});if(!d.ok)return null;const E=await d.json();return typeof E?.lastFsSetEventAt=="number"?{lastFsSetEventAt:E.lastFsSetEventAt}:null}finally{clearTimeout(h)}}catch{return null}}L(probeWatcherHealth,"probeWatcherHealth"),l(probeWatcherHealth,"probeWatcherHealth");const sleepUnref=l(n=>new Promise(p=>{setTimeout(p,n).unref?.()}),"sleepUnref");async function waitForPortFree(n,p,c={}){const h=c.isPortFree??probePortFree,d=Math.max(1,c.intervalMs??types_1.PORT_FREE_POLL_MS),E=Date.now()+Math.max(0,p);for(;;){if(await h(n))return!0;const C=E-Date.now();if(C<=0)return!1;await sleepUnref(Math.min(d,C))}}L(waitForPortFree,"waitForPortFree"),l(waitForPortFree,"waitForPortFree");function viteArgPort(n){const p=l(c=>Number.isInteger(c)&&c>0&&c<65536?c:void 0,"valid");for(let c=0;c<n.length;c+=1){const h=n[c];if(h==="--port")return p(Number(n[c+1]));if(h.startsWith("--port="))return p(Number(h.slice(7)))}}L(viteArgPort,"viteArgPort"),l(viteArgPort,"viteArgPort");function createLensChildren(n,p,c,h){const{emit:d}=p,E=h?.isPortFree??probePortFree,C=h?.probeWatcherHealth??probeWatcherHealth,P=[],R=new Map,v=new Map,O=new Map,H=new Map,F=new Set;let $;const A=new Map,y=new Set,k=new Set,x=new Set,T=new Set,W=l((t,a,s)=>{if(T.has(t))return;T.add(t);const e=setInterval(()=>{(async()=>{if(n.stopped()||y.has(t)){clearInterval(e),T.delete(t);return}await E(a)&&(clearInterval(e),T.delete(t),console.log(`[gateway] lens child ${t}: :${a} freed \u2014 respawning.`),s())})().catch(()=>{})},types_1.LENS_PARKED_PORT_REPROBE_MS);e.unref?.()},"parkUntilPortFree"),N=l(async(t,a)=>{let s=t;for(;a.has(s)||!await E(s);)s+=1;return a.add(s),s!==t&&console.warn(`[gateway] port ${t} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${s} instead.`),s},"claimFreePort"),b=l((t,a,s,e,r=!1,o,i,u)=>{const w=Date.now(),g=(0,node_child_process_1.spawn)(a,s,{cwd:i??n.root,env:{...(0,pod_env_1.podBaseEnv)(process.env,n.root,i??n.root),...e??{},LENSMCP_PARENT_PID:String(process.pid)},stdio:"inherit"});P.push(g),A.set(t,g);const m=t==="vite"||t.endsWith(":vite"),_=m?viteArgPort(s):u;if(m){const M=typeof e?.LENSMCP_EVENT_FILE=="string"?e.LENSMCP_EVENT_FILE:void 0;v.set(g,{project:e?.LENSMCP_PROJECT??"app",spawnedAt:w,port:_,probeFails:0,listening:!1,...M&&M!==n.eventFile?{bus:M}:{}})}return g.on("exit",(M,S)=>{const j=P.indexOf(g);if(j>=0&&P.splice(j,1),v.delete(g),n.stopped()||y.has(t))return;const D=Date.now()-w,f=R.get(t)??{attempts:0,portConflicts:0};(async()=>{const K=_!==void 0&&!await E(_),I=(0,lifecycle_1.isPortConflictExit)({code:M,signal:S,ranMs:D,portHeld:K},{fastFailMs:types_1.CHILD_START_FAIL_MS});if(I&&f.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)f.portConflicts+=1,R.set(t,f),console.warn(`[gateway] lens child ${t} lost the :${_} bind race (EADDRINUSE, code=${M}); waiting for the port, then retrying (port-retry ${f.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else if(I){f.portConflicts=0,R.set(t,f),console.warn(`[gateway] lens child ${t}: :${_} persistently held after ${types_1.MAX_PORT_CONFLICT_RETRIES} bind races \u2014 another server owns it (an external/shared daemon?). Parking; re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s, will respawn if it frees.`),d("warning",`lens child parked: ${t} (:${_} held externally)`,`cluster-lens-child:${t}`,{kind:"lens-child-parked",label:t,port:_}),W(t,_,()=>b(t,a,s,e,r,o,i,u));return}else{f.portConflicts=0,D>=HEAL_HEALTHY_MS&&(f.attempts=0);const q=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**f.attempts);f.attempts+=1,R.set(t,f),console.warn(`[gateway] lens child ${t} exited (code=${M} sig=${S}); auto-healing in ${Math.round(q/1e3)}s (attempt ${f.attempts}).`),await sleepUnref(q)}n.stopped()||y.has(t)||(_!==void 0&&!await waitForPortFree(_,types_1.PORT_FREE_WAIT_MS,{isPortFree:E,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${t}: :${_} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(n.stopped()||y.has(t))&&b(t,a,s,e,r,o,i,u))})().catch(()=>{})}),g},"spawnManagedChild");let B=!1;const Y=l(async(t,a)=>{if(!B){B=!0;try{for(const[s,e]of[...v]){if(n.stopped())return;if(e.port===void 0||s.exitCode!==null||s.signalCode!==null)continue;if(await probePortListening(e.port,types_1.LENS_PROBE_TIMEOUT_MS)){e.probeFails=0,e.listening||(e.listening=!0,d("info",`lens app up: ${e.project} (vite :${e.port} listening)`,`cluster-lens-app:${e.project}`,{kind:"service-up",project:e.project,port:e.port},void 0,e.bus));continue}const r=e.listening?t.graceMs:Math.max(t.graceMs,t.neverListenedGraceMs);if(Date.now()-e.spawnedAt<r){e.probeFails=0;continue}if(!a)continue;e.probeFails+=1;const o=(0,lifecycle_1.lensChildWedged)({spawnedAt:e.spawnedAt,probeFails:e.probeFails,hasListened:e.listening},O.get(e.project)??0,Date.now(),t);if(!o)continue;const i=e.listening?"its listener VANISHED (was up, now refusing)":`it NEVER bound within the ${Math.round(r/1e3)}s boot allowance`;O.set(e.project,Date.now()),e.probeFails=0,e.listening=!1,console.error(`[gateway] wedge recycle: lens app ${e.project} is ALIVE but NOT LISTENING on :${e.port} after ${Math.max(2,t.failureThreshold)} probes \u2014 ${i} \u2014 SIGKILL; a fresh vite is coming.`),d("warning",`wedge recycle: ${e.project} (${o})`,`cluster-lens-app:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o,port:e.port},void 0,e.bus);try{s.kill("SIGKILL")}catch{}}}finally{B=!1}}},"probeLensChildren"),V=l(()=>{if($||!types_1.POD_RECYCLE_ENABLED&&!types_1.LENS_PROBE_ENABLED)return;const t={graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};$=setInterval(()=>{if(n.stopped()||v.size===0||(G(),!types_1.POD_RECYCLE_ENABLED))return;const a=n.sourceSetChangedAt??0;if(a<=0)return;const s=Date.now();for(const[e,r]of v){const o=n.sourceSetChangedFor?.get(r.project)??a,i=O.get(r.project)??0;(0,lifecycle_1.lensFrontendStale)(r.spawnedAt,i,o,s,t)&&((H.get(r.project)??0)>=o||F.has(r.project)||(F.add(r.project),(async()=>{try{const u=types_1.LENS_WATCHER_PROBE_ENABLED&&r.port!==void 0?(0,lifecycle_1.lensWatcherVerdict)(await C(r.port),o,Math.min(types_1.POD_STALE_SCAN_MS,15e3)+types_1.POD_RECYCLE_SETTLE_MS):"unknown";if(u==="heard"){H.set(r.project,o),console.log(`[gateway] stale source set: lens app ${r.project} watcher CONFIRMED the change \u2014 no recycle, HMR intact.`),d("info",`stale check: ${r.project} watcher confirmed \u2014 no recycle`,`cluster-lens-app:${r.project}`,{kind:"pod-stale-verified",project:r.project},void 0,r.bus);return}if(e.exitCode!==null||e.signalCode!==null)return;O.set(r.project,Date.now()),r.listening=!1,console.log(`[gateway] zombie recycle: lens app ${r.project} (stale-source-set) \u2014 watcher ${u==="missed"?"SILENT across the change":"unverifiable"}; restarting it`),d("warning",`zombie recycle: ${r.project} (stale-source-set)`,`cluster-lens-app:${r.project}`,{kind:"pod-recycle",project:r.project,reason:"stale-source-set",watcher:u},void 0,r.bus);const w=(0,hmr_reload_1.broadcastFullReload)(n,r.project);w>0&&console.log(`[gateway] \u2192 told ${w} open tab(s) to reload (stale optimize-dep generation)`);try{e.kill("SIGTERM")}catch{}setTimeout(()=>{if(e.exitCode===null&&e.signalCode===null){console.log(`[gateway] zombie recycle: lens app ${r.project} ignored SIGTERM for ${Math.round(types_1.LENS_RECYCLE_KILL_GRACE_MS/1e3)}s \u2014 escalating to SIGKILL.`);try{e.kill("SIGKILL")}catch{}}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}finally{F.delete(r.project)}})()))}},Math.min(types_1.POD_STALE_SCAN_MS,15e3)),$.unref?.()},"startLensSweeper"),z=l(async()=>{const t=[...new Map(n.routes.filter(e=>e.lens).map(e=>[e.project,e.lens])).values()],a=c.dashboard!==!1,s=c.mcp!==!1&&t.length>0;if(a){const e=(0,lens_frontend_1.findDashboardBundle)(n.root);if(!e)console.warn("[gateway] lens dashboard bundle not found \u2014 skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");else{const r=(0,scope_1.readLensScope)(n.root),o=await N(c.dashboardPort??r.dashboardPort,k),i=c.lensHost??"lensmcp.local",u=r.basePath;n.registry.appendRoutes(n.wsKey,[{host:i,prefix:u,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${o}`}]),n.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:r.key,root:n.root,base:u,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] starting lens dashboard \u2192 http://localhost:${o} (routed at https://${i}${u}/)`),d("info",`lens dashboard up: ${i}${u} \u2192 :${o}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:i,port:o,base:u}),b("lens-dashboard",process.execPath,[e],{LENSMCP_EVENT_FILE:n.eventFile,LENSMCP_DASHBOARD_PORT:String(o),LENSMCP_DASHBOARD_BASE:u},!1,void 0,void 0,o)}}if(s){const e=(0,lens_frontend_1.findMcpBundle)(n.root);if(!e)console.warn("[gateway] MCP server bundle not found \u2014 skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");else{const r=(0,scope_1.readLensScope)(n.root),o=Number(process.env.LENSMCP_PORT??r.mcpHttpPort),i=l(()=>{console.log(`[gateway] starting lens MCP server on 127.0.0.1:${o} (${e})`),b("lens-mcp",process.execPath,[e],{LENSMCP_EVENT_FILE:n.eventFile,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:String(o),LENSMCP_WORKSPACE_KEY:r.key,LENSMCP_WORKSPACE_ROOT:n.root},!1,void 0,void 0,o)},"spawnMcp");await E(o)?i():(console.warn(`[gateway] lens MCP port :${o} is already served \u2014 an external lensmcp server (a shared daemon?) owns this workspace's MCP; not spawning a second. Re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s; will take over if it frees.`),d("info",`lens MCP served externally on :${o} \u2014 gateway parked its own`,"cluster-lens-child:lens-mcp",{kind:"lens-mcp-external",port:o}),W("lens-mcp",o,i))}}for(const e of t){if(e.started)continue;e.started=!0;const r=await N(e.port,x);if(r!==e.port){for(const o of n.routes)o.lens?.project===e.project&&(o.lens.port=r,o.target=`http://localhost:${r}`);e.port=r}try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:e.projectRoot,workspaceRoot:n.root,project:e.project,eventFile:n.eventFile,port:e.port,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:e.project},log:l(o=>console.log(o),"log")},b),d("info",`lens app started: ${e.project} (vite :${e.port})`,`cluster-lens-app:${e.project}`,{kind:"lens-app-up",project:e.project,port:e.port})}catch(o){console.error(`[gateway] lens app ${e.project}: ${o.message}`)}}V()},"bootSingletonsAndApps"),U=l(async t=>{const a=(0,lens_frontend_1.findDashboardBundle)(n.root);if(!a)return;const s=(0,scope_1.readLensScope)(t.root),e=c.lensHost??"lensmcp.local",r=s.basePath,o=await N(s.dashboardPort,k),i=`lens-dashboard:${t.wsKey}`,u=path.join(t.root,".lensmcp","events.jsonl");return y.delete(i),n.registry.appendRoutes(t.wsKey,[{host:e,prefix:r,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${o}`}]),(0,workspace_registry_1.registerWorkspace)({key:t.wsKey,root:t.root,base:r,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] hosting dashboard for workspace '${t.wsKey}' \u2192 http://localhost:${o} (routed at https://${e}${r}/)`),d("info",`lens dashboard up: ${e}${r} \u2192 :${o}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:e,port:o,base:r}),b(i,process.execPath,[a],{LENSMCP_EVENT_FILE:u,LENSMCP_DASHBOARD_PORT:String(o),LENSMCP_DASHBOARD_BASE:r,LENSMCP_WS_KEY:t.wsKey},!1,void 0,t.root,o),()=>{y.add(i),k.delete(o),(0,workspace_registry_1.unregisterWorkspace)(t.wsKey);try{A.get(i)?.kill("SIGTERM")}catch{}A.delete(i)}},"hostWorkspaceDashboard"),J=l(async t=>{const a=new Map;for(const o of t.routes){if(!o.lens)continue;const i=a.get(o.lens.project)??[];i.push(o),a.set(o.lens.project,i)}if(a.size===0)return()=>{};const s=path.join(t.root,".lensmcp","events.jsonl"),e=[],r=[];for(const[o,i]of a){const u=i[0].lens;if(u.started)continue;const w=await N(u.port,x);r.push(w);for(const S of i)S.lens.started=!0,S.lens.port=w,S.target=`http://localhost:${w}`;const g=`${t.wsKey}:${o}:`,m=`${g}vite`,_=`${g}browser-capture`;e.push(m,_),y.delete(m),y.delete(_);const M=l((S,j,D,f,K,I)=>b(S,j,D,f,K,I,t.root),"spawnInRoot");console.log(`[gateway] hosting lens app '${o}' of '${t.wsKey}' \u2192 vite :${w}`);try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:u.projectRoot,workspaceRoot:t.root,project:o,eventFile:s,port:w,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:o},labelPrefix:g,log:l(S=>console.log(S),"log")},M),d("info",`lens app started: ${t.wsKey}/${o} (vite :${w})`,`cluster-lens-app:${t.wsKey}:${o}`,{kind:"lens-app-up",project:o,port:w},void 0,s)}catch(S){console.error(`[gateway] lens app ${t.wsKey}/${o}: ${S.message}`)}}return()=>{for(const o of e){y.add(o);try{A.get(o)?.kill("SIGTERM")}catch{}A.delete(o)}for(const o of r)x.delete(o)}},"hostWorkspaceApps"),Q=l(async(t=2500)=>{$&&(clearInterval($),$=void 0);const a=[...P];for(const r of a)try{r.kill("SIGTERM")}catch{}const s=l(r=>r.exitCode===null&&r.signalCode===null,"alive"),e=Date.now()+t;for(;Date.now()<e&&a.some(s);)await new Promise(r=>setTimeout(r,100));for(const r of a)if(s(r))try{r.kill("SIGKILL")}catch{}},"reapAll"),G=l(()=>Y({graceMs:types_1.LENS_PROBE_GRACE_MS,failureThreshold:types_1.LENS_PROBE_FAILS,cooldownMs:types_1.LENS_PROBE_COOLDOWN_MS,neverListenedGraceMs:types_1.LENS_PROBE_BOOT_GRACE_MS},types_1.LENS_PROBE_ENABLED),"probeLensFrontends");return{spawnManagedChild:b,bootSingletonsAndApps:z,hostWorkspaceDashboard:U,hostWorkspaceApps:J,probeLensFrontends:G,reapAll:Q}}L(createLensChildren,"createLensChildren"),l(createLensChildren,"createLensChildren");
|
|
1
|
+
"use strict";var ye=Object.defineProperty;var M=(s,E)=>ye(s,"name",{value:E,configurable:!0});var _e=Object.defineProperty,l=M((s,E)=>_e(s,"name",{value:E,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.probeWatcherHealth=probeWatcherHealth,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.createLensChildren=createLensChildren;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_child_process_1=require("node:child_process"),lens_frontend_1=require("@lensmcp/nx-plugin/lens-frontend"),hmr_reload_1=require("./hmr-reload"),pod_env_1=require("./pod-env"),scope_1=require("./scope"),workspace_registry_1=require("./workspace-registry"),lifecycle_1=require("./lifecycle"),types_1=require("./types"),observability_1=require("./observability"),HEAL_BASE_MS=1500,HEAL_MAX_MS=3e4,HEAL_HEALTHY_MS=2e4;function tryBind(s,E){return new Promise(u=>{const y=net.createServer();y.unref(),y.once("error",p=>{const f=p.code??"";u(f==="EADDRINUSE"||f==="EACCES"?"in-use":"unavailable")}),y.listen(E===void 0?{port:s}:{port:s,host:E},()=>y.close(()=>u("free")))})}M(tryBind,"tryBind"),l(tryBind,"tryBind");async function probePortFree(s){for(const E of[void 0,"0.0.0.0","127.0.0.1","::1"])if(await tryBind(s,E)==="in-use")return!1;return!0}M(probePortFree,"probePortFree"),l(probePortFree,"probePortFree");function probeConnect(s,E,u){return new Promise(y=>{let p=!1;const f=net.createConnection({host:s,port:E});f.unref();const C=setTimeout(()=>S(!1),u);C.unref?.();function S(T){p||(p=!0,clearTimeout(C),f.destroy(),y(T))}M(S,"S"),l(S,"finish"),f.once("connect",()=>S(!0)),f.once("error",()=>S(!1))})}M(probeConnect,"probeConnect"),l(probeConnect,"probeConnect");async function probePortListening(s,E=1e3){return await probeConnect("127.0.0.1",s,E)?!0:probeConnect("::1",s,E)}M(probePortListening,"probePortListening"),l(probePortListening,"probePortListening");async function probeWatcherHealth(s,E=1500){try{const u=new AbortController,y=setTimeout(()=>u.abort(),E);y.unref?.();try{const p=await fetch(`http://127.0.0.1:${s}/__lensmcp/watcher-health`,{signal:u.signal});if(!p.ok)return null;const f=await p.json();if(typeof f?.lastFsSetEventAt!="number")return null;const C=Array.isArray(f.recentEvents)?f.recentEvents.filter(S=>S&&typeof S.path=="string"&&typeof S.at=="number"&&typeof S.sequence=="number"&&["add","addDir","unlink","unlinkDir"].includes(S.kind)):[];return{schemaVersion:f.schemaVersion===2?2:1,ready:f.ready!==!1,sequence:typeof f.sequence=="number"?f.sequence:0,lastFsSetEventAt:f.lastFsSetEventAt,fsSetEvents:typeof f.fsSetEvents=="number"?f.fsSetEvents:0,recentEvents:C}}finally{clearTimeout(y)}}catch{return null}}M(probeWatcherHealth,"probeWatcherHealth"),l(probeWatcherHealth,"probeWatcherHealth");const sleepUnref=l(s=>new Promise(E=>{setTimeout(E,s).unref?.()}),"sleepUnref");async function waitForPortFree(s,E,u={}){const y=u.isPortFree??probePortFree,p=Math.max(1,u.intervalMs??types_1.PORT_FREE_POLL_MS),f=Date.now()+Math.max(0,E);for(;;){if(await y(s))return!0;const C=f-Date.now();if(C<=0)return!1;await sleepUnref(Math.min(p,C))}}M(waitForPortFree,"waitForPortFree"),l(waitForPortFree,"waitForPortFree");function viteArgPort(s){const E=l(u=>Number.isInteger(u)&&u>0&&u<65536?u:void 0,"valid");for(let u=0;u<s.length;u+=1){const y=s[u];if(y==="--port")return E(Number(s[u+1]));if(y.startsWith("--port="))return E(Number(y.slice(7)))}}M(viteArgPort,"viteArgPort"),l(viteArgPort,"viteArgPort");function createLensChildren(s,E,u,y){const{emit:p}=E,f=y?.isPortFree??probePortFree,C=y?.probeWatcherHealth??probeWatcherHealth,S=[],T=new Map,N=new Map,F=new Map,B=new Map,K=new Set;let O;const A=new Map,$=new Set,G=new Set,b=new Set,R=new Map,D=new WeakMap,x=new Set,z=l((e,r,n)=>{if(x.has(e))return;x.add(e);const t=setInterval(()=>{(async()=>{if(s.stopped()||$.has(e)){clearInterval(t),x.delete(e);return}await f(r)&&(clearInterval(t),x.delete(e),console.log(`[gateway] lens child ${e}: :${r} freed \u2014 respawning.`),n())})().catch(()=>{})},types_1.LENS_PARKED_PORT_REPROBE_MS);t.unref?.()},"parkUntilPortFree"),j=l(async(e,r)=>{let n=e;for(;r.has(n)||!await f(n);)n+=1;return r.add(n),n!==e&&console.warn(`[gateway] port ${e} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${n} instead.`),n},"claimFreePort"),ae=l((e,r,n)=>{const t=n?.LENSMCP_FRONTEND_KEY,o=Number(n?.LENSMCP_FRONTEND_GENERATION);if(!t||!Number.isFinite(o))return;const a=[...R.get(t)?.generations??[]].find(i=>i.generation===o);a&&(e.endsWith(":vite")?a.vite=r:e.endsWith(":browser-capture")&&(a.capture=r))},"attachRespawnedGenerationChild"),k=l((e,r,n,t,o=!1,a,i,c)=>{const g=Date.now(),d=(0,node_child_process_1.spawn)(r,n,{cwd:i??s.root,env:{...(0,pod_env_1.podBaseEnv)(process.env,s.root,i??s.root),...t??{},LENSMCP_PARENT_PID:String(process.pid)},stdio:"inherit"});S.push(d),A.set(e,d),ae(e,d,t);const h=e==="vite"||e.endsWith(":vite"),L=typeof t?.LENSMCP_EVENT_FILE=="string"?t.LENSMCP_EVENT_FILE:void 0,w=L&&L!==s.eventFile?L:void 0,_=h?viteArgPort(n):c;h&&N.set(d,{key:t?.LENSMCP_FRONTEND_KEY??`${i??s.root}:${t?.LENSMCP_PROJECT??"app"}`,project:t?.LENSMCP_PROJECT??"app",workspaceRoot:i??s.root,spawnedAt:g,port:_,probeFails:0,listening:!1,generation:Number(t?.LENSMCP_FRONTEND_GENERATION??0),role:t?.LENSMCP_FRONTEND_ROLE==="replacement"?"replacement":"active",...w?{bus:w}:{}});const v=e==="lens-mcp"?"lens-mcp":e==="lens-dashboard"||e.startsWith("lens-dashboard:")?"lens-dashboard":void 0;return v&&_!==void 0&&(p("info",`lens child starting: ${v} (:${_})`,`cluster-lens-child:${v}`,{kind:"lens-child-starting",project:v,label:e,pid:d.pid,port:_},void 0,w),(async()=>{const m=Date.now()+types_1.LENS_REPLACEMENT_READY_MS;for(;!s.stopped()&&Date.now()<m;){if(d.exitCode!==null||d.signalCode!==null||$.has(e))return;if(await probePortListening(_,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS))){p("info",`lens child up: ${v} (:${_})`,`cluster-lens-child:${v}`,{kind:"lens-child-up",project:v,label:e,pid:d.pid,port:_},void 0,w);return}await sleepUnref(150)}})().catch(()=>{})),d.on("exit",(m,V)=>{const te=S.indexOf(d);te>=0&&S.splice(te,1),A.get(e)===d&&A.delete(e);const oe=N.get(d);N.delete(d);const he=$.delete(e);if(s.stopped()||he||(v&&p("warning",`lens child exited: ${v}`,`cluster-lens-child:${v}`,{kind:"lens-child-exit",project:v,label:e,pid:d.pid,code:m,signal:V},void 0,w),oe&&le(d,oe)))return;const ne=Date.now()-g,P=T.get(e)??{attempts:0,portConflicts:0};(async()=>{const we=_!==void 0&&!await f(_),re=(0,lifecycle_1.isPortConflictExit)({code:m,signal:V,ranMs:ne,portHeld:we},{fastFailMs:types_1.CHILD_START_FAIL_MS});if(re&&P.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)P.portConflicts+=1,T.set(e,P),console.warn(`[gateway] lens child ${e} lost the :${_} bind race (EADDRINUSE, code=${m}); waiting for the port, then retrying (port-retry ${P.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else if(re){P.portConflicts=0,T.set(e,P),console.warn(`[gateway] lens child ${e}: :${_} persistently held after ${types_1.MAX_PORT_CONFLICT_RETRIES} bind races \u2014 another server owns it (an external/shared daemon?). Parking; re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s, will respawn if it frees.`),p("warning",`lens child parked: ${e} (:${_} held externally)`,`cluster-lens-child:${e}`,{kind:"lens-child-parked",label:e,port:_}),z(e,_,()=>k(e,r,n,t,o,a,i,c));return}else{P.portConflicts=0,ne>=HEAL_HEALTHY_MS&&(P.attempts=0);const se=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**P.attempts);P.attempts+=1,T.set(e,P),console.warn(`[gateway] lens child ${e} exited (code=${m} sig=${V}); auto-healing in ${Math.round(se/1e3)}s (attempt ${P.attempts}).`),await sleepUnref(se)}s.stopped()||$.has(e)||(_!==void 0&&!await waitForPortFree(_,types_1.PORT_FREE_WAIT_MS,{isPortFree:f,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${e}: :${_} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(s.stopped()||$.has(e))&&k(e,r,n,t,o,a,i,c))})().catch(()=>{})}),d},"spawnManagedChild"),J=l(e=>{const r=`${e.wsKey}:${e.project}`,n=R.get(r);if(n)return n;const t={key:r,...e,sourceDirs:e.frontend.sourceRoots?.length?e.frontend.sourceRoots:[e.projectRoot],generation:0,state:"starting",generations:new Set,disposed:!1};R.set(r,t);for(const o of e.routes)o.lens&&(o.lens.workspaceKey??=e.wsKey,D.set(o.lens,t));return e.frontend.workspaceKey??=e.wsKey,D.set(e.frontend,t),t},"registerFrontend"),X=l((e,r,n)=>{const t=++e.generation,o=`${e.key}:g${t}:`,a=`${o}vite`,i=`${o}browser-capture`;$.delete(a),$.delete(i);const c=l((h,L,w,_,v,m)=>k(h,L,w,_,v,m,e.workspaceRoot),"spawnInRoot"),g=(0,lens_frontend_1.spawnLensFrontend)({projectRoot:e.projectRoot,workspaceRoot:e.workspaceRoot,project:e.project,eventFile:e.eventFile,port:r,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:e.project,LENSMCP_FRONTEND_KEY:e.key,LENSMCP_FRONTEND_GENERATION:String(t),LENSMCP_FRONTEND_ROLE:n},labelPrefix:o,log:l(h=>console.log(h),"log")},c),d={ownerKey:e.key,generation:t,port:r,labelPrefix:o,vite:g?.vite??A.get(a),capture:A.get(i)};return e.generations.add(d),d},"spawnFrontendGeneration"),I=l((e,r)=>{if(!e)return;R.get(e.ownerKey)?.generations.delete(e);const n=[[`${e.labelPrefix}browser-capture`,e.capture],[`${e.labelPrefix}vite`,e.vite]];for(const[t,o]of n)if($.add(t),!(!o||o.exitCode!==null||o.signalCode!==null)){try{o.kill("SIGTERM")}catch{}setTimeout(()=>{if(o.exitCode===null&&o.signalCode===null)try{o.kill("SIGKILL")}catch{}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}r&&b.delete(e.port)},"terminateGeneration"),ie=l(async e=>{const r=Date.now()+types_1.LENS_REPLACEMENT_READY_MS;for(;!s.stopped()&&Date.now()<r;){const n=e.vite;if(!n||n.exitCode!==null||n.signalCode!==null)return!1;if(await probePortListening(e.port,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS))){const t=await C(e.port);if(!t||t.ready)return!0}await sleepUnref(150)}return!1},"waitForGenerationReady"),W=l(async e=>{const r=e?.vite;return!!(e&&r&&r.exitCode===null&&r.signalCode===null&&await probePortListening(e.port,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS)))},"generationIsServing"),H=l(e=>!e.disposed&&!s.stopped()&&R.get(e.key)===e,"frontendIsCurrent"),Y=l((e,r)=>{if(!H(e))return Promise.resolve(!1);if(e.replacement)return e.replacement;const n=e.active;e.state=n?"replacing":"starting",e.reason=r,e.startedAt=Date.now();const t=(async()=>{let o,a;const i=Date.now();try{const c=await j(e.desiredPort,b);if(a=c,!H(e))return b.delete(c),!1;o=X(e,c,"replacement"),e.candidate=o;const g=await W(n);console.log(`[gateway] lens handoff start: ${e.key} g${n?.generation??0}\u2192g${o.generation} :${n?.port??"down"}\u2192:${c} reason=${r}`),p("info",`lens handoff starting: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-start",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,oldPort:n?.port,newPort:c,oldServing:g},void 0,e.eventFile===s.eventFile?void 0:e.eventFile);const d=await ie(o);if(!H(e))return I(o,!0),e.candidate=void 0,!1;if(!d){I(o,!0),e.candidate=void 0;const w=await W(n);return e.state=w?"ready":"failed",w&&(e.reason=void 0,e.startedAt=void 0),console.error(`[gateway] lens handoff failed: ${e.key} g${o.generation} did not become ready within ${types_1.LENS_REPLACEMENT_READY_MS}ms; ${e.state==="ready"?"old generation kept serving":"no active generation"}.`),p("warning",`lens handoff failed: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-failed",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,port:c,durationMs:Date.now()-i,oldKept:w},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),!1}for(const w of e.routes)w.target=`http://localhost:${o.port}`,w.lens&&(w.lens.port=o.port);e.frontend.port=o.port,e.active=o,e.candidate=void 0,e.state="ready",e.reason=void 0,e.startedAt=void 0;const h=o.vite?N.get(o.vite):void 0;h&&(h.role="active",h.listening=!0);const L=(0,hmr_reload_1.broadcastFullReload)(s,e.key);if(console.log(`[gateway] lens handoff ready: ${e.key} g${o.generation} :${o.port} in ${Date.now()-i}ms; draining g${n?.generation??0}${L>0?`; reloading ${L} tab(s)`:""}.`),p("info",`lens handoff complete: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-complete",wsKey:e.wsKey,project:e.project,reason:r,generation:o.generation,oldPort:n?.port,newPort:o.port,durationMs:Date.now()-i,reloadedTabs:L},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),n){const w=`${n.labelPrefix}browser-capture`;$.add(w);try{n.capture?.kill("SIGTERM")}catch{}setTimeout(()=>I(n,!0),types_1.LENS_REPLACEMENT_DRAIN_MS).unref?.()}return!0}catch(c){I(o,!0),!o&&a!==void 0&&b.delete(a),e.candidate=void 0;const g=await W(n);e.state=g?"ready":"failed",g&&(e.reason=void 0,e.startedAt=void 0);const d=c.message;return console.error(`[gateway] lens handoff failed: ${e.key}: ${d}`),p("warning",`lens handoff failed: ${e.project}`,`cluster-lens-app:${e.project}`,{kind:"lens-handoff-failed",wsKey:e.wsKey,project:e.project,reason:r,generation:o?.generation??e.generation,port:o?.port??a,durationMs:Date.now()-i,oldKept:g,error:d},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),!1}finally{e.replacement=void 0}})();return e.replacement=t,t},"rollFrontend"),Q=l((e,r)=>{e.active=X(e,r,"active"),e.state="starting",e.startedAt=Date.now()},"spawnInitialFrontend"),le=l((e,r)=>{const n=R.get(r.key);return!n||types_1.LENS_RESTART_MODE==="in-place"?!1:(n.candidate?.vite===e||n.active?.vite!==e||(n.state="starting",n.reason="child-exit",n.startedAt=Date.now(),console.warn(`[gateway] lens frontend exited: ${n.key} g${r.generation} :${r.port}; starting an alternate-port replacement.`),Y(n,"child-exit")),!0)},"handleManagedViteExit");s.lensSupervisor=types_1.LENS_RESTART_MODE==="rolling"?{status(e){const r=D.get(e);return r?{state:r.state,generation:r.candidate?.generation??r.active?.generation??r.generation,reason:r.reason,startedAt:r.startedAt}:void 0},recover(e,r){const n=D.get(e);return n?Y(n,r):Promise.resolve(!1)}}:void 0;let q=!1,Z=0;const ce=l(()=>{q||Date.now()-Z<types_1.POD_STALE_SCAN_MS||(q=!0,Z=Date.now(),(async()=>{for(const e of[...R.values()]){if(s.stopped())return;const r=await(0,lifecycle_1.sourceSetSnapshot)(e.workspaceRoot,e.sourceDirs,(0,scope_1.readSourceSetExclude)(e.workspaceRoot));if(R.get(e.key)!==e)continue;const n=e.sourceSnapshot;if(e.sourceSnapshot=r,!n)continue;const t=(0,lifecycle_1.diffSourceSets)(n,r,Date.now());t&&(e.sourceChange=t,p("info",`source file set changed for ${e.key}`,`cluster-lens-app:${e.project}`,{kind:"source-set-changed",scoped:!0,wsKey:e.wsKey,project:e.project,added:t.added,removed:t.removed,truncated:t.truncated},void 0,e.eventFile===s.eventFile?void 0:e.eventFile))}})().catch(()=>{}).finally(()=>{q=!1}))},"scanFrontendSourceSets");let U=!1;const de=l(async(e,r)=>{if(!U){U=!0;try{for(const[n,t]of[...N]){if(s.stopped())return;if(t.port===void 0||n.exitCode!==null||n.signalCode!==null)continue;if(await probePortListening(t.port,types_1.LENS_PROBE_TIMEOUT_MS)){if(t.probeFails=0,!t.listening){t.listening=!0;const c=R.get(t.key);c?.active?.vite===n&&(c.state="ready",c.reason=void 0),p("info",`lens app up: ${t.project} (vite :${t.port} listening)`,`cluster-lens-app:${t.project}`,{kind:"service-up",project:t.project,port:t.port},void 0,t.bus)}continue}const o=t.listening?e.graceMs:Math.max(e.graceMs,e.neverListenedGraceMs);if(Date.now()-t.spawnedAt<o){t.probeFails=0;continue}if(!r)continue;t.probeFails+=1;const a=(0,lifecycle_1.lensChildWedged)({spawnedAt:t.spawnedAt,probeFails:t.probeFails,hasListened:t.listening},F.get(t.key)??0,Date.now(),e);if(!a)continue;const i=t.listening?"its listener VANISHED (was up, now refusing)":`it NEVER bound within the ${Math.round(o/1e3)}s boot allowance`;F.set(t.key,Date.now()),t.probeFails=0,t.listening=!1,console.error(`[gateway] wedge recycle: lens app ${t.project} is ALIVE but NOT LISTENING on :${t.port} after ${Math.max(2,e.failureThreshold)} probes \u2014 ${i} \u2014 SIGKILL; a fresh vite is coming.`),p("warning",`wedge recycle: ${t.project} (${a})`,`cluster-lens-app:${t.project}`,{kind:"pod-recycle",project:t.project,reason:a,port:t.port},void 0,t.bus);try{n.kill("SIGKILL")}catch{}}}finally{U=!1}}},"probeLensChildren"),pe=l(()=>{if(O||!types_1.POD_RECYCLE_ENABLED&&!types_1.LENS_PROBE_ENABLED)return;const e={graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};O=setInterval(()=>{if(s.stopped()||N.size===0||(ee(),!types_1.POD_RECYCLE_ENABLED))return;ce();const r=Date.now();for(const[n,t]of N){const o=R.get(t.key),a=o?.sourceChange?.changedAt??0,i=F.get(t.key)??0;(0,lifecycle_1.lensFrontendStale)(t.spawnedAt,i,a,r,e)&&((B.get(t.key)??0)>=a||K.has(t.key)||(K.add(t.key),(async()=>{try{const c=o?.sourceChange,g=c&&!c.truncated?[...c.added,...c.removed].map(w=>path.resolve(t.workspaceRoot,w)):[],d=types_1.LENS_WATCHER_PROBE_ENABLED&&t.port!==void 0?(0,lifecycle_1.lensWatcherVerdict)(await C(t.port),a,Math.min(types_1.POD_STALE_SCAN_MS,15e3)+types_1.POD_RECYCLE_SETTLE_MS,g):"unknown";if(d==="heard"){B.set(t.key,a),console.log(`[gateway] stale source set: lens app ${t.project} watcher CONFIRMED the change \u2014 no recycle, HMR intact.`),p("info",`stale check: ${t.project} watcher confirmed \u2014 no recycle`,`cluster-lens-app:${t.project}`,{kind:"pod-stale-verified",project:t.project},void 0,t.bus);return}if(n.exitCode!==null||n.signalCode!==null)return;F.set(t.key,Date.now());const h=R.get(t.key);if(types_1.LENS_RESTART_MODE==="rolling"&&h){console.log(`[gateway] stale refresh: lens app ${t.key} (stale-source-set) \u2014 watcher ${d==="missed"?"missed the exact path change":"unverifiable"}; preparing a standby without dropping traffic.`),p("warning",`stale refresh: ${t.project} (stale-source-set)`,`cluster-lens-app:${t.project}`,{kind:"pod-refresh",wsKey:h.wsKey,project:t.project,reason:"stale-source-set",watcher:d,added:c?.added,removed:c?.removed},void 0,t.bus),await Y(h,"stale-source-set");return}t.listening=!1,console.log(`[gateway] zombie recycle: lens app ${t.project} (stale-source-set) \u2014 watcher ${d==="missed"?"SILENT across the change":"unverifiable"}; restarting it`),p("warning",`zombie recycle: ${t.project} (stale-source-set)`,`cluster-lens-app:${t.project}`,{kind:"pod-recycle",project:t.project,reason:"stale-source-set",watcher:d},void 0,t.bus);const L=(0,hmr_reload_1.broadcastFullReload)(s,t.key);L>0&&console.log(`[gateway] \u2192 told ${L} open tab(s) to reload (stale optimize-dep generation)`);try{n.kill("SIGTERM")}catch{}setTimeout(()=>{if(n.exitCode===null&&n.signalCode===null){console.log(`[gateway] zombie recycle: lens app ${t.project} ignored SIGTERM for ${Math.round(types_1.LENS_RECYCLE_KILL_GRACE_MS/1e3)}s \u2014 escalating to SIGKILL.`);try{n.kill("SIGKILL")}catch{}}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}finally{K.delete(t.key)}})()))}},Math.min(types_1.POD_STALE_SCAN_MS,15e3)),O.unref?.()},"startLensSweeper"),ue=l(async()=>{const e=[...new Map(s.routes.filter(t=>t.lens).map(t=>[t.project,t.lens])).values()],r=u.dashboard!==!1,n=u.mcp!==!1&&e.length>0;if(r){const t=(0,lens_frontend_1.findDashboardBundle)(s.root);if(!t)console.warn("[gateway] lens dashboard bundle not found \u2014 skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");else{const o=(0,scope_1.readLensScope)(s.root),a=await j(u.dashboardPort??o.dashboardPort,G),i=u.lensHost??"lensmcp.local",c=o.basePath;s.registry.appendRoutes(s.wsKey,[{host:i,prefix:c,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${a}`}]),s.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:o.key,root:s.root,base:c,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] starting lens dashboard \u2192 http://localhost:${a} (routed at https://${i}${c}/)`),p("info",`lens dashboard up: ${i}${c} \u2192 :${a}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:i,port:a,base:c}),k("lens-dashboard",process.execPath,[t],{LENSMCP_EVENT_FILE:s.eventFile,LENSMCP_PROCESS_ROLE:"dashboard",LENSMCP_DASHBOARD_PORT:String(a),LENSMCP_DASHBOARD_BASE:c},!1,void 0,void 0,a)}}if(n){const t=(0,lens_frontend_1.findMcpBundle)(s.root);if(!t)console.warn("[gateway] MCP server bundle not found \u2014 skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");else{const o=(0,scope_1.readLensScope)(s.root),a=Number(process.env.LENSMCP_PORT??o.mcpHttpPort),i=l(()=>{console.log(`[gateway] starting lens MCP server on 127.0.0.1:${a} (${t})`),k("lens-mcp",process.execPath,[t],{LENSMCP_EVENT_FILE:s.eventFile,LENSMCP_PROCESS_ROLE:"mcp",LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:String(a),LENSMCP_WORKSPACE_KEY:o.key,LENSMCP_WORKSPACE_ROOT:s.root},!1,void 0,void 0,a)},"spawnMcp");await f(a)?i():(console.warn(`[gateway] lens MCP port :${a} is already served \u2014 an external lensmcp server (a shared daemon?) owns this workspace's MCP; not spawning a second. Re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s; will take over if it frees.`),p("info",`lens MCP served externally on :${a} \u2014 gateway parked its own`,"cluster-lens-child:lens-mcp",{kind:"lens-child-up",project:"lens-mcp",label:"lens-mcp",port:a,external:!0}),z("lens-mcp",a,i))}}for(const t of e){if(t.started)continue;t.started=!0;const o=t.port,a=(s.registry.get(s.wsKey)?.routes??s.routes).filter(g=>g.lens?.project===t.project),i=await j(t.port,b);for(const g of a)g.lens&&(g.lens.port=i),g.target=`http://localhost:${i}`;t.port=i;const c=J({wsKey:s.wsKey,project:t.project,workspaceRoot:s.root,projectRoot:t.projectRoot,eventFile:s.eventFile,routes:a,frontend:t,desiredPort:o});try{Q(c,i),p("info",`lens app started: ${t.project} (vite :${t.port})`,`cluster-lens-app:${t.project}`,{kind:"lens-app-up",project:t.project,port:t.port})}catch(g){b.delete(i),c.state="failed",console.error(`[gateway] lens app ${t.project}: ${g.message}`)}}pe()},"bootSingletonsAndApps"),fe=l(async e=>{const r=(0,lens_frontend_1.findDashboardBundle)(s.root);if(!r)return;const n=(0,scope_1.readLensScope)(e.root),t=u.lensHost??"lensmcp.local",o=n.basePath,a=await j(n.dashboardPort,G),i=`lens-dashboard:${e.wsKey}`,c=path.join(e.root,".lensmcp","events.jsonl");return $.delete(i),s.registry.appendRoutes(e.wsKey,[{host:t,prefix:o,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${a}`}]),(0,workspace_registry_1.registerWorkspace)({key:e.wsKey,root:e.root,base:o,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] hosting dashboard for workspace '${e.wsKey}' \u2192 http://localhost:${a} (routed at https://${t}${o}/)`),p("info",`lens dashboard up: ${t}${o} \u2192 :${a}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:t,port:a,base:o}),k(i,process.execPath,[r],{LENSMCP_EVENT_FILE:c,LENSMCP_PROCESS_ROLE:"dashboard",LENSMCP_DASHBOARD_PORT:String(a),LENSMCP_DASHBOARD_BASE:o,LENSMCP_WS_KEY:e.wsKey},!1,void 0,e.root,a),()=>{$.add(i),G.delete(a),(0,workspace_registry_1.unregisterWorkspace)(e.wsKey);try{A.get(i)?.kill("SIGTERM")}catch{}A.delete(i)}},"hostWorkspaceDashboard"),Ee=l(async e=>{const r=new Map;for(const o of e.routes){if(!o.lens)continue;const a=r.get(o.lens.project)??[];a.push(o),r.set(o.lens.project,a)}if(r.size===0)return()=>{};const n=path.join(e.root,".lensmcp","events.jsonl"),t=[];for(const[o,a]of r){const i=a[0].lens;if(i.started)continue;const c=await j(i.port,b),g=i.port;for(const h of a)h.lens.started=!0,h.lens.port=c,h.target=`http://localhost:${c}`;const d=J({wsKey:e.wsKey,project:o,workspaceRoot:e.root,projectRoot:i.projectRoot,eventFile:n,routes:a,frontend:i,desiredPort:g});t.push(d),console.log(`[gateway] hosting lens app '${o}' of '${e.wsKey}' \u2192 vite :${c}`);try{Q(d,c),p("info",`lens app started: ${e.wsKey}/${o} (vite :${c})`,`cluster-lens-app:${e.wsKey}:${o}`,{kind:"lens-app-up",project:o,port:c},void 0,n)}catch(h){b.delete(c),d.state="failed",console.error(`[gateway] lens app ${e.wsKey}/${o}: ${h.message}`)}}return()=>{for(const o of t){o.disposed=!0;for(const a of[...o.generations])I(a,!0);for(const a of o.routes)a.lens&&D.delete(a.lens);D.delete(o.frontend),F.delete(o.key),B.delete(o.key),K.delete(o.key),R.delete(o.key)}}},"hostWorkspaceApps"),ge=l(async(e=2500)=>{O&&(clearInterval(O),O=void 0);const r=[...S];for(const o of r)try{o.kill("SIGTERM")}catch{}const n=l(o=>o.exitCode===null&&o.signalCode===null,"alive"),t=Date.now()+e;for(;Date.now()<t&&r.some(n);)await new Promise(o=>setTimeout(o,100));for(const o of r)if(n(o))try{o.kill("SIGKILL")}catch{}},"reapAll"),ee=l(()=>de({graceMs:types_1.LENS_PROBE_GRACE_MS,failureThreshold:types_1.LENS_PROBE_FAILS,cooldownMs:types_1.LENS_PROBE_COOLDOWN_MS,neverListenedGraceMs:types_1.LENS_PROBE_BOOT_GRACE_MS},types_1.LENS_PROBE_ENABLED),"probeLensFrontends");return{spawnManagedChild:k,bootSingletonsAndApps:ue,hostWorkspaceDashboard:fe,hostWorkspaceApps:Ee,probeLensFrontends:ee,reapAll:ge}}M(createLensChildren,"createLensChildren"),l(createLensChildren,"createLensChildren");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GatewayRuntime, ServiceCtl, SockPool } from './types';
|
|
1
|
+
import type { GatewayRuntime, LensWatcherHealth, ServiceCtl, SockPool, SourceSetChange } from './types';
|
|
2
2
|
import type { Observability } from './observability';
|
|
3
3
|
/** Bounded, async, non-blocking walk of the workspace source tree → a signature of the file SET
|
|
4
4
|
* (`<count>:<xor-of-path-hashes>`). Skips node_modules/dist/build/dotdirs + `*-devserver` sock dirs + every
|
|
@@ -9,7 +9,15 @@ import type { Observability } from './observability';
|
|
|
9
9
|
* actually resolve, instead of the whole monorepo — see that function for WHY. Omitted ⇒ the whole root
|
|
10
10
|
* (the original workspace-wide behavior, still used when a child's project root is unknown). Root-level
|
|
11
11
|
* loose files are always counted, so a `package.json`/`tsconfig.base.json` add reaches every scope. */
|
|
12
|
+
export interface SourceSetSnapshot {
|
|
13
|
+
signature: string;
|
|
14
|
+
paths: Set<string>;
|
|
15
|
+
truncated: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function sourceSetSnapshot(root: string, scopeDirs?: readonly string[], extraExclude?: ReadonlySet<string>): Promise<SourceSetSnapshot>;
|
|
12
18
|
export declare function sourceSetSignature(root: string, scopeDirs?: readonly string[], extraExclude?: ReadonlySet<string>): Promise<string>;
|
|
19
|
+
/** Exact, bounded evidence for a source-file-set transition. */
|
|
20
|
+
export declare function diffSourceSets(previous: SourceSetSnapshot, next: SourceSetSnapshot, changedAt: number): SourceSetChange | null;
|
|
13
21
|
/**
|
|
14
22
|
* PURE: which subtrees a child of `projectRoot` must watch for a "file set changed" verdict.
|
|
15
23
|
*
|
|
@@ -77,9 +85,9 @@ export type LensWatcherVerdict = 'heard' | 'missed' | 'unknown';
|
|
|
77
85
|
* one — accepted: the failure mode is exactly the pre-sweeper world for one scan cycle, the next set change
|
|
78
86
|
* re-arms the check, and the alternative (kill-first) taxes every healthy add to cover it.
|
|
79
87
|
*/
|
|
80
|
-
export declare function lensWatcherVerdict(probe: {
|
|
88
|
+
export declare function lensWatcherVerdict(probe: Pick<LensWatcherHealth, 'schemaVersion' | 'lastFsSetEventAt' | 'recentEvents'> | {
|
|
81
89
|
lastFsSetEventAt: number;
|
|
82
|
-
} | null, changedAt: number, windowMs: number): LensWatcherVerdict;
|
|
90
|
+
} | null, changedAt: number, windowMs: number, changedPaths?: readonly string[]): LensWatcherVerdict;
|
|
83
91
|
/** Config for the lens-child LIVENESS decision ({@link lensChildWedged}). */
|
|
84
92
|
export interface LensChildProbeConfig {
|
|
85
93
|
/** Spawn grace: no verdict until the child has had this long to BIND its port (a cold vite boot). */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`)){const
|
|
3
|
-
`)){if(/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(
|
|
1
|
+
"use strict";var q=Object.defineProperty;var v=(t,s)=>q(t,"name",{value:s,configurable:!0});var W=Object.defineProperty,a=v((t,s)=>W(t,"name",{value:s,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),types_1=require("./types");function eastWestEnv(t,s){const l={},i=t.serviceKeys[s];if(i&&(l.LENSMCP_SERVICE_KEY=i),t.https){const c=require("../../../basic-ssl").caCertPath();fs.existsSync(c)&&(l.NODE_EXTRA_CA_CERTS=c)}return l}v(eastWestEnv,"eastWestEnv"),a(eastWestEnv,"eastWestEnv");const sleep=a(t=>new Promise(s=>setTimeout(s,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let s=2166136261;for(let l=0;l<t.length;l+=1)s^=t.charCodeAt(l),s=Math.imul(s,16777619);return s>>>0}v(fnv1a,"fnv1a"),a(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:s}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(s.split("\0").filter(Boolean))}catch{return new Set}}v(gitIgnoredPaths,"gitIgnoredPaths"),a(gitIgnoredPaths,"gitIgnoredPaths");async function sourceSetSnapshot(t,s,l){let i=0,c=0,S=!1;const u=new Set,E=await gitIgnoredPaths(t),w=a(k=>path.relative(t,k).split(path.sep).join("/"),"rel"),_=a(async(k,A=!0)=>{if(i>=SCAN_FILE_CAP){S=!0;return}let b;try{b=await fs.promises.readdir(k,{withFileTypes:!0})}catch{return}for(const T of b){if(i>=SCAN_FILE_CAP){S=!0;return}const y=T.name,M=path.join(k,y);if(T.isDirectory()){if(!A||y.startsWith(".")||SRC_EXCLUDE.has(y)||l?.has(y)||y.endsWith("-devserver")||E.has(`${w(M)}/`))continue;await _(M)}else if(T.isFile()){const x=y.lastIndexOf(".");if(x<=0||!SRC_EXT.has(y.slice(x))||E.has(w(M)))continue;const C=w(M);if(u.has(C))continue;u.add(C),i+=1,c^=fnv1a(C)}}},"walk");if(s&&s.length>0){await _(t,!1);for(const k of s)await _(k)}else await _(t);return{signature:`${i}:${(c>>>0).toString(36)}`,paths:u,truncated:S}}v(sourceSetSnapshot,"sourceSetSnapshot"),a(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,s,l){return(await sourceSetSnapshot(t,s,l)).signature}v(sourceSetSignature,"sourceSetSignature"),a(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,s,l){if(t.paths.size===s.paths.size){let u=!0;for(const E of t.paths)if(!s.paths.has(E)){u=!1;break}if(u)return null}const i=[],c=[];let S=t.truncated||s.truncated;for(const u of s.paths)t.paths.has(u)||(i.length+c.length<SOURCE_DIFF_PATH_CAP?i.push(u):S=!0);for(const u of t.paths)s.paths.has(u)||(i.length+c.length<SOURCE_DIFF_PATH_CAP?c.push(u):S=!0);return{changedAt:l,added:i,removed:c,truncated:S}}v(diffSourceSets,"diffSourceSets"),a(diffSourceSets,"diffSourceSets");function sourceScopeDirs(t,s,l,i,c){const S=a(w=>{const _=path.relative(t,w).split(path.sep).filter(Boolean);return _.length===0||_[0].startsWith("..")?"":_[0]},"bucketOf")(s);if(!S)return[];const u=new Set(l.filter(Boolean)),E=new Set([S]);for(const w of i)w.startsWith(".")||SRC_EXCLUDE.has(w)||c?.has(w)||w.endsWith("-devserver")||u.has(w)||E.add(w);return[...E].sort().map(w=>path.join(t,w))}v(sourceScopeDirs,"sourceScopeDirs"),a(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,s,l,i){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||s-t.lastUsed<i.settleMs||t.lastStaleRecycleAt!=null&&s-t.lastStaleRecycleAt<i.cooldownMs?null:l>0&&l-t.spawnAt>i.graceMs?"stale-source-set":i.maxAgeMs>0&&s-t.spawnAt>i.maxAgeMs?"max-age":null}v(podRecycleReason,"podRecycleReason"),a(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,s,l,i,c){return!(i-s<c.cooldownMs||l<=0||l-t<=c.graceMs||i-l<c.settleMs)}v(lensFrontendStale,"lensFrontendStale"),a(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,s,l,i=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&i.length>0){const c=s-l,S=new Set(t.recentEvents.filter(u=>u.at>=c).map(u=>path.resolve(u.path)));return i.every(u=>S.has(path.resolve(u)))?"heard":"missed"}return t.lastFsSetEventAt>=s-l?"heard":"missed"}v(lensWatcherVerdict,"lensWatcherVerdict"),a(lensWatcherVerdict,"lensWatcherVerdict");function lensChildWedged(t,s,l,i){const c=t.hasListened?i.graceMs:Math.max(i.graceMs,i.neverListenedGraceMs);return l-t.spawnedAt<c||l-s<i.cooldownMs||t.probeFails<Math.max(2,i.failureThreshold)?null:"wedged-no-listener"}v(lensChildWedged,"lensChildWedged"),a(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,s){return t.signal||t.code===null||t.code===0||t.ranMs>s.fastFailMs?!1:t.portHeld}v(isPortConflictExit,"isPortConflictExit"),a(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,s){const{emit:l}=s,i=a(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),c=a((e,o,r,n,d)=>l(o,r,n,{wsKey:e.wsKey,...d},void 0,i(e)),"emitSvc"),S=a(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),u=a((e,o)=>{let r;try{r=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const n=e.proc?.pid;let d=0;for(const h of r.split(`
|
|
2
|
+
`)){const f=h.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!f)continue;const $=Number(f[1]),g=Number(f[2]),p=f[3];if(p.includes(`run ${o}`)&&/\bnx(\.js)?\b/.test(p)&&!(p.includes("/node_modules/nx/")&&!p.includes(e.root))&&!($===process.pid||$===n)&&!(n!==void 0&&g===n)){try{process.kill(-g,"SIGKILL")}catch{try{process.kill($,"SIGKILL")}catch{}}d+=1}}return d>0&&(console.log(`[gateway] ${o}: reaped ${d} stale runner tree(s) \u2014 a foreign generation held the nx task lock`),c(e,"warning",`stale-runner reap: ${e.project} (${d} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:d})),d},"reapForeignRunners"),E=a(e=>{if(t.stopped())return;const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,r=Math.max(1,e.decl.minPods??1);u(e,o),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${o} (pods=${r})\u2026`),c(e,"info",`service starting: ${e.project} (${r} pod${r>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:r}),e.state="starting",e.spawnAt=Date.now();const n=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),[d,h]=fs.existsSync(n)?[process.execPath,[n,"run",o,`--childCount=${r}`]]:["yarn",["nx","run",o,`--childCount=${r}`]],f=(0,node_child_process_1.spawn)(d,h,{cwd:e.root,detached:!0,env:{...(0,pod_env_1.podBaseEnv)(process.env,t.root,e.root),NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false",...e.wsKey?{LENSMCP_WS_KEY:e.wsKey}:{},LENSMCP_EVENT_FILE:e.wsKey===t.wsKey?t.eventFile:path.join(e.root,".lensmcp","events.jsonl"),LENSMCP_GATEWAY_PID:String(process.pid),...eastWestEnv(t,e.project)},stdio:["ignore","pipe","pipe"]});e.lastErrors=[],e.buildFailed=!1;const $=a(g=>{const p=g.toString();for(const m of p.split(`
|
|
3
|
+
`)){if(/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(m)){const j=m.replace(/\x1b\[[0-9;]*m/g,"").trim();e.lastErrors.push(j),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const R=Date.now();(j!==e.lastErrorEmitSig||R-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=j,e.lastErrorEmitAt=R,c(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:j}))}/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(m)&&(e.buildFailed=!1),!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(m)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${o}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),c(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(A(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),u(e,o),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&E(e)},1500).unref?.())},250).unref?.())}},"sniff");f.stdout?.on("data",g=>{t.stopped()||($(g),process.stdout.write(`[${e.project}] ${g}`))}),f.stderr?.on("data",g=>{t.stopped()||($(g),process.stderr.write(`[${e.project}] ${g}`))}),f.on("exit",g=>{if(e.proc=void 0,e.state="down",t.stopped()||(console.log(`[gateway] ${o} exited (${g})`),c(e,g===0?"info":"warning",`service down: ${e.project} (exit ${g})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:g}),g===0||e.buildFailed))return;const p=Date.now();if(e.restarts=(e.restarts??[]).filter(j=>p-j<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){c(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(p);const m=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),c(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),E(e))},m)}),e.proc=f},"spawnService"),w=a((e,o)=>{if(e.state==="up")return!1;e.state="up";const r=e.pool.socks.length;return c(e,"info",`service up: ${e.project} (${r} pods, ${o})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:r,adopted:o==="adopted"}),!0},"markUp"),_=a(async e=>{if(S(e.pool)>0)return w(e,"adopted"),!0;if(t.stopped())return!1;e.state!=="starting"&&!e.proc&&E(e);const o=Date.now()+t.startupTimeoutMs,r=Date.now()+12e3;for(;!t.stopped()&&Date.now()<o;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>r)return!1;if(S(e.pool)>0){e.state="up",e.buildFailed=!1;const n=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${n?`, cold start ${n}ms`:""})`),c(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${n??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:n}),!0}if(e.state==="down")return!1}return!1},"ensureUp"),k=a(e=>{const o=e.decl.maxPods??e.decl.minPods??1,r=e.pool.socks.length,n=e.decl.adminUrl??(e.decl.port?`http://localhost:${e.decl.port}`:void 0);if(!n||r===0||r>=o||e.inflight<=r||Date.now()-e.lastScaleAt<5e3)return;e.lastScaleAt=Date.now();const d=(n.startsWith("https")?require("node:https"):http).request(`${n}/webpack/scale`,{method:"POST",rejectUnauthorized:!1},h=>h.resume());d.on("error",()=>{}),d.end(JSON.stringify({pods:r+1})),console.log(`[gateway] autoscale ${e.project}: ${r} \u2192 ${r+1} pods (inflight ${e.inflight})`),c(e,"info",`autoscale: ${e.project} ${r} \u2192 ${r+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:r,to:r+1,inflight:e.inflight})},"scaleUp"),A=a((e,o,r)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),r?.announce!==!1&&c(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const n=e.proc;try{n.stdout?.removeAllListeners("data"),n.stderr?.removeAllListeners("data"),n.removeAllListeners("exit")}catch{}try{process.kill(-n.pid,"SIGTERM")}catch{try{n.kill("SIGTERM")}catch{}}e.proc=void 0,e.state="down",setTimeout(()=>{try{for(const d of fs.readdirSync(e.pool.dir)){if(!d.endsWith(".sock"))continue;const h=path.join(e.pool.dir,d);try{const f=Number(fs.readFileSync(`${h}.owner`,"utf8").trim());if(Number.isFinite(f)&&f>0)try{process.kill(f,0);continue}catch{}}catch{}try{fs.unlinkSync(h)}catch{}try{fs.unlinkSync(`${h}.owner`)}catch{}}}catch{}e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),b=a(e=>{const o=Date.now();if(e.restarts=(e.restarts??[]).filter(r=>o-r<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){c(e,"error",`service crash-looping: ${e.project} \u2014 wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project}),A(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.restarts.push(o),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),c(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:e.restarts.length}),A(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&E(e)},1500).unref?.()},"recycleWedged"),T={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let y;const M=new Map;let x=!1,C=0;const P=a(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(n=>n.project));for(const n of t.routes)n.lens&&o.add(n.lens.project);const r=new Set;for(const n of o){const d=e[n];if(!d)continue;const h=path.relative(t.root,d).split(path.sep).filter(Boolean)[0];h&&!h.startsWith("..")&&r.add(h)}return[...r]},"managedBuckets"),I=a((e,o,r,n)=>{const d=t.projectSourceRoots?.[e];if(d?.length)return d;const h=t.projectRoots?.[e];if(!h)return;const f=sourceScopeDirs(t.root,h,r,o,n);return f.length>0?f:void 0},"scopeFor"),N=a(()=>{x||Date.now()-C<types_1.POD_STALE_SCAN_MS||(x=!0,C=Date.now(),(async()=>{const e=(0,scope_1.readSourceSetExclude)(t.root),o=await sourceSetSnapshot(t.root,void 0,e),r=Date.now(),n=y?diffSourceSets(y,o,r):null;if(n&&(t.sourceSetChangedAt=r,t.sourceSetChange=n,l("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:n.added,removed:n.removed,truncated:n.truncated})),y=o,!t.projectRoots)return;let d;try{d=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(p=>p.isDirectory()).map(p=>p.name)}catch{return}const h=P(),f=new Set(t.services.map(p=>p.project));for(const p of t.routes)p.lens&&f.add(p.lens.project);const $=new Map;for(const p of f){const m=I(p,d,h,e);if(!m)continue;const j=m.join("|"),R=$.get(j)??{dirs:m,projects:[]};R.projects.push(p),$.set(j,R)}const g=Date.now();for(const[p,{dirs:m,projects:j}]of $){const R=await sourceSetSnapshot(t.root,m,e),L=M.get(p);if(M.set(p,R),!L)continue;const D=diffSourceSets(L,R,g);if(D){t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const F of j)t.sourceSetChangedFor.set(F,g),t.sourceSetChangesFor.set(F,D);l("info",`source file set changed in ${j.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:j,added:D.added,removed:D.removed,truncated:D.truncated})}}})().catch(()=>{}).finally(()=>{x=!1}))},"maybeScanSourceSet"),O=a((e,o)=>{e.lastStaleRecycleAt=Date.now(),console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),c(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const r=!!e.decl.eager;A(e,`${o} \u2014 recycling stale pod`,{announce:!1}),r&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&E(e)},1500).unref?.()},"recyclePod");return{scanNow:S,spawnService:E,ensureUp:_,scaleUp:k,killSpawned:A,markUp:w,startSweeper:a(()=>{const e=setInterval(()=>{if(!t.stopped()){types_1.POD_RECYCLE_ENABLED&&N();for(const o of t.services){if(t.stopped()||!o.proc){o.wedgedSince=void 0;continue}const r=o.decl.idleKillSec;if(r&&o.inflight===0&&Date.now()-o.lastUsed>r*1e3){A(o,`idle ${r}s`),o.wedgedSince=void 0;continue}const n=o.state==="starting"&&o.spawnAt!==void 0&&Date.now()-o.spawnAt<t.startupTimeoutMs,d=S(o.pool)>0;if(d||n){if(o.wedgedSince=void 0,d&&w(o,"observed"),d&&!n&&types_1.POD_RECYCLE_ENABLED){const h=t.sourceSetChangedFor?.get(o.project)??t.sourceSetChangedAt??0,f=podRecycleReason(o,Date.now(),h,T);f&&O(o,f)}continue}o.wedgedSince??=Date.now(),Date.now()-o.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(o.wedgedSince=void 0,b(o))}}},t.sweepMs);return e.unref(),e},"startSweeper")}}v(createServiceLayer,"createServiceLayer"),a(createServiceLayer,"createServiceLayer");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var q=Object.defineProperty;var b=(E,u)=>q(E,"name",{value:u,configurable:!0});var C=Object.defineProperty,s=b((E,u)=>C(E,"name",{value:u,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),types_1=require("./types");function isConnError(E){const u=E.code??"";return u==="ECONNREFUSED"||u==="ECONNRESET"||u==="ETIMEDOUT"||u==="EPIPE"||u==="EHOSTUNREACH"||u==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(E.message)}b(isConnError,"isConnError"),s(isConnError,"isConnError");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),httpProxy=require("http-proxy");function createProxy(E,u,S){const{recordEdge:D,finishTrace:N,traceStep:T}=u,k={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},w=new http.Agent(k),$=new(require("node:https")).Agent(k),x=s(r=>typeof r=="string"&&r.startsWith("https:")?$:w,"agentForTarget"),P=httpProxy.createProxyServer({xfwd:!0,secure:!1,proxyTimeout:12e4,agent:w}),O=s((r,o,e)=>{const i=s(()=>{e()||r.destroyed||r.destroy()},"onEdgeClose");o.once("close",i),r.once("close",()=>o.removeListener("close",i))},"propagateEdgeCancel");P.on("proxyReq",(r,o,e)=>{O(r,e,()=>e.writableEnded)}),P.on("proxyRes",(r,o,e)=>{const i=o.__lensmcpPod;i&&(r.headers["x-lensmcp-pod"]=i),delete r.headers["x-powered-by"],r.headers.server="LensMCP",e&&O(r,e,()=>e.writableEnded)});const M=s((r,o,e,i)=>{const d={};for(const[t,p]of Object.entries(o.headers))p===void 0||t.startsWith(":")||HOP_BY_HOP.has(t)||(d[t]=p);const m=o.socket?.remoteAddress??"",f=o.headers["x-forwarded-for"];d["x-forwarded-for"]=f?`${String(f)}, ${m}`:m,d["x-forwarded-proto"]="https",d["x-forwarded-host"]||(d["x-forwarded-host"]=o.headers.host??"");let a=http,l;if(typeof r=="object")l={socketPath:r.socketPath,path:o.url,method:o.method,headers:d,agent:w};else{const t=new URL(r),p=t.protocol==="https:";a=p?require("node:https"):http,d.host=t.host,l={protocol:t.protocol,hostname:t.hostname,port:t.port||(p?443:80),path:o.url,method:o.method,headers:d,agent:x(r)}}const n=e;n.__lensmcpErrContained||(n.__lensmcpErrContained=!0,e.on("error",()=>{}));let c;const y=s(()=>{g.destroy()},"onReqError"),h=s(()=>{c&&!c.destroyed&&c.destroy()},"onEdgeClose"),_=s(()=>{o.removeListener("error",y),e.removeListener("close",h)},"detach"),g=a.request(l,t=>{if(c=t,i.onResponse(),e.headersSent||e.writableEnded||e.destroyed){t.destroy();return}const p={};for(const[v,R]of Object.entries(t.headers))R===void 0||HOP_BY_HOP.has(v)||(p[v]=R);delete p["x-powered-by"],p.server="LensMCP";const U=o.__lensmcpPod;U&&(p["x-lensmcp-pod"]=U);try{e.writeHead(t.statusCode??502,p)}catch(v){console.error("[gateway] h2 relay: bad response header, resetting stream:",v.message),t.destroy(),e.destroyed||e.destroy(v);return}t.pipe(e);const j=s(()=>{!e.writableEnded&&!e.destroyed&&e.destroy()},"abortEdge");t.once("error",j),t.once("close",j),e.on("close",h)});g.once("error",t=>i.onError(t)),g.once("close",_),o.on("error",y),o.method==="GET"||o.method==="HEAD"||o.method==="OPTIONS"?g.end():o.pipe(g)},"pipeUpstream"),A=s(async(r,o,e)=>{const i=o.__lensmcpTrace;if(r.pool){const a=r.svc;a&&(a.lastUsed=Date.now(),a.inflight+=1,e.on("close",()=>{a.inflight-=1,a.lastUsed=Date.now()}));let l=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs);if(!l&&a&&(T(i,"cold-start",{project:r.project}),await S.ensureUp(a)&&(l=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs))),a&&l&&S.markUp(a,"observed"),a&&S.scaleUp(a),!l){const _=a?.lastErrors?.length?` Service errors: ${a.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(e,o,503,"unavailable",`Service ${r.project} is not reachable (no pods in ${r.pool.dir}).${_} The watcher recovers and the next request retries automatically.`);return}const n=path.basename(l,".sock");o.__lensmcpPod=n,T(i,"pod-select",{pod:n,pods:r.pool.socks.length,strategy:"round-robin"});let c=!1;const y=s(_=>{r.pool.socks=r.pool.socks.filter(g=>g!==l),r.pool.idx=-1;try{fs.unlinkSync(l)}catch{}console.error(`[gateway] pod ${n} of ${r.project}: ${_}`)},"evictPod"),h=setTimeout(()=>{c||e.headersSent||e.writableEnded||(c=!0,y(`no response within ${types_1.POD_RESPONSE_TIMEOUT_MS}ms (wedged socket) \u2014 evicted + respawning`),(0,edge_1.sendError)(e,o,502,"unavailable",`Pod ${n} of ${r.project} was unresponsive \u2014 evicted, retry.`))},types_1.POD_RESPONSE_TIMEOUT_MS);h.unref?.(),e.on("close",()=>clearTimeout(h)),M({socketPath:l},o,e,{onResponse:s(()=>{clearTimeout(h),c=!0},"onResponse"),onError:s(_=>{clearTimeout(h),!c&&(c=!0,y(_.message),(0,edge_1.sendError)(e,o,502,"unavailable",`Pod ${n} of ${r.project} unavailable \u2014 retry.`))},"onError")});return}const d=r.target,m=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS,f=s(()=>{M(d,o,e,{onResponse:s(()=>{},"onResponse"),onError:s(a=>{if(isConnError(a)&&Date.now()<m&&!e.headersSent&&!e.writableEnded&&!e.destroyed){setTimeout(f,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(e.headersSent||e.writableEnded||e.destroyed){e.destroyed||e.destroy();return}console.error(`[gateway] upstream ${r.project} (${String(d)}):`,a.message),(0,edge_1.sendError)(e,o,502,"unavailable",`Upstream ${r.project} unavailable.`)},"onError")})},"tryTcp");f()},"forwardNative");return{proxy:P,forward:s(async(r,o,e)=>{const i=Date.now(),d=o.__lensmcpTrace;if(e.on("close",()=>{D(r,Date.now()-i,e.statusCode??0,o.__lensmcpCaller),N(d,r.project,e.statusCode??0,{...o.__lensmcpPod?{pod:o.__lensmcpPod}:{},...o.__lensmcpCaller?{caller:o.__lensmcpCaller}:{}})}),r.prependPrefix&&!o.url?.startsWith(r.prependPrefix)&&(o.url=r.prependPrefix+(o.url??"/")),(o.httpVersionMajor??1)>=2){await A(r,o,e);return}if(r.pool){const n=r.svc;n&&(n.lastUsed=Date.now(),n.inflight+=1,e.on("close",()=>{n.inflight-=1,n.lastUsed=Date.now()}));let c=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs);if(!c&&n&&(T(d,"cold-start",{project:r.project}),await S.ensureUp(n)&&(c=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs))),n&&c&&S.markUp(n,"observed"),n&&S.scaleUp(n),!c){const t=n?.lastErrors?.length?` Service errors: ${n.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(e,o,503,"unavailable",`Service ${r.project} is not reachable (no pods in ${r.pool.dir}).${t} The watcher recovers and the next request retries automatically.`);return}const y=path.basename(c,".sock");o.__lensmcpPod=y,T(d,"pod-select",{pod:y,pods:r.pool.socks.length,strategy:"round-robin"});let h=!1;const _=s(t=>{r.pool.socks=r.pool.socks.filter(p=>p!==c),r.pool.idx=-1;try{fs.unlinkSync(c)}catch{}console.error(`[gateway] pod ${y} of ${r.project}: ${t}`)},"evictPod"),g=setTimeout(()=>{h||e.headersSent||e.writableEnded||(h=!0,_(`no response within ${types_1.POD_RESPONSE_TIMEOUT_MS}ms (wedged socket) \u2014 evicted + respawning`),(0,edge_1.sendError)(e,o,502,"unavailable",`Pod ${y} of ${r.project} was unresponsive \u2014 evicted, retry.`))},types_1.POD_RESPONSE_TIMEOUT_MS);g.unref?.(),e.on("close",()=>clearTimeout(g)),P.web(o,e,{target:{socketPath:c},agent:w},t=>{clearTimeout(g),!h&&(h=!0,_(t.message),(0,edge_1.sendError)(e,o,502,"unavailable",`Pod ${y} of ${r.project} unavailable \u2014 retry.`))});return}const m=r.target,f={target:m,autoRewrite:typeof m=="string",changeOrigin:!0,agent:x(m)},a=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS,l=s(()=>{P.web(o,e,f,n=>{if(isConnError(n)&&Date.now()<a&&!e.headersSent&&!e.writableEnded&&!e.destroyed){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}e.destroyed||(console.error(`[gateway] upstream ${r.project} (${String(m)}):`,n.message),(0,edge_1.sendError)(e,o,502,"unavailable",`Upstream ${r.project} unavailable.`))})},"tryTcp");l()},"forward"),agentForTarget:x,destroyAgents:s(()=>{w.destroy(),$.destroy()},"destroyAgents"),closeProxy:s(()=>{try{P.close?.()}catch{}},"closeProxy")}}b(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
1
|
+
"use strict";var L=Object.defineProperty;var x=(E,f)=>L(E,"name",{value:f,configurable:!0});var H=Object.defineProperty,c=x((E,f)=>H(E,"name",{value:f,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),types_1=require("./types");function isConnError(E){const f=E.code??"";return f==="ECONNREFUSED"||f==="ECONNRESET"||f==="ETIMEDOUT"||f==="EPIPE"||f==="EHOSTUNREACH"||f==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(E.message)}x(isConnError,"isConnError"),c(isConnError,"isConnError");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),httpProxy=require("http-proxy");function createProxy(E,f,w){const{recordEdge:C,finishTrace:N,traceStep:T,emit:q}=f,k=c(async r=>{if(!r.lens||!E.lensSupervisor)return"unsupported";let t;const e=new Promise(l=>{t=setTimeout(()=>l("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),t.unref?.()}),p=E.lensSupervisor.recover(r.lens,"proxy-connection").then(l=>l?"ready":"failed").catch(()=>"failed"),i=await Promise.race([p,e]);t&&clearTimeout(t);const g=E.lensSupervisor.status(r.lens);return i==="failed"&&(!g||g.state==="ready")?"unsupported":i},"waitForManagedRecovery"),b=c((r,t,e,p,i,g)=>{if(e.headersSent||e.writableEnded||e.destroyed){e.destroyed||e.destroy();return}const l=r.lens?E.lensSupervisor?.status(r.lens):void 0,u=g==="timeout"||l?.state==="starting"||l?.state==="replacing",s=(0,gateway_errors_1.ensureRequestId)(t),o=r.lens?{project:r.project,reason:l?.reason??"connection-failure",generation:l?.generation??0,...u?{retryAfterMs:2e3}:{}}:void 0,a=u?503:502,d=u?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${s} project=${r.project} target=${String(p)} state=${l?.state??"unmanaged"} generation=${l?.generation??0}: ${i.message}`),q("warning",`upstream unavailable: ${r.project}`,`cluster-upstream:${r.project}`,{kind:"upstream-error",traceId:s,project:r.project,target:String(p),state:l?.state??"unmanaged",generation:l?.generation,error:i.message}),(0,edge_1.sendError)(e,t,a,d,u?`Upstream ${r.project} is restarting.`:`Upstream ${r.project} unavailable.`,o,u?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),M={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},P=new http.Agent(M),O=new(require("node:https")).Agent(M),$=c(r=>typeof r=="string"&&r.startsWith("https:")?O:P,"agentForTarget"),S=httpProxy.createProxyServer({xfwd:!0,secure:!1,proxyTimeout:12e4,agent:P}),j=c((r,t,e)=>{const p=c(()=>{e()||r.destroyed||r.destroy()},"onEdgeClose");t.once("close",p),r.once("close",()=>t.removeListener("close",p))},"propagateEdgeCancel");S.on("proxyReq",(r,t,e)=>{j(r,e,()=>e.writableEnded)}),S.on("proxyRes",(r,t,e)=>{const p=t.__lensmcpPod;p&&(r.headers["x-lensmcp-pod"]=p),delete r.headers["x-powered-by"],r.headers.server="LensMCP",e&&j(r,e,()=>e.writableEnded)});const R=c((r,t,e,p)=>{const i={};for(const[n,m]of Object.entries(t.headers))m===void 0||n.startsWith(":")||HOP_BY_HOP.has(n)||(i[n]=m);const g=t.socket?.remoteAddress??"",l=t.headers["x-forwarded-for"];i["x-forwarded-for"]=l?`${String(l)}, ${g}`:g,i["x-forwarded-proto"]="https",i["x-forwarded-host"]||(i["x-forwarded-host"]=t.headers.host??"");let u=http,s;if(typeof r=="object")s={socketPath:r.socketPath,path:t.url,method:t.method,headers:i,agent:P};else{const n=new URL(r),m=n.protocol==="https:";u=m?require("node:https"):http,i.host=n.host,s={protocol:n.protocol,hostname:n.hostname,port:n.port||(m?443:80),path:t.url,method:t.method,headers:i,agent:$(r)}}const o=e;o.__lensmcpErrContained||(o.__lensmcpErrContained=!0,e.on("error",()=>{}));let a;const d=c(()=>{h.destroy()},"onReqError"),y=c(()=>{a&&!a.destroyed&&a.destroy()},"onEdgeClose"),_=c(()=>{t.removeListener("error",d),e.removeListener("close",y)},"detach"),h=u.request(s,n=>{if(a=n,p.onResponse(),e.headersSent||e.writableEnded||e.destroyed){n.destroy();return}const m={};for(const[v,D]of Object.entries(n.headers))D===void 0||HOP_BY_HOP.has(v)||(m[v]=D);delete m["x-powered-by"],m.server="LensMCP";const U=t.__lensmcpPod;U&&(m["x-lensmcp-pod"]=U);try{e.writeHead(n.statusCode??502,m)}catch(v){console.error("[gateway] h2 relay: bad response header, resetting stream:",v.message),n.destroy(),e.destroyed||e.destroy(v);return}n.pipe(e);const A=c(()=>{!e.writableEnded&&!e.destroyed&&e.destroy()},"abortEdge");n.once("error",A),n.once("close",A),e.on("close",y)});h.once("error",n=>p.onError(n)),h.once("close",_),t.on("error",d),t.method==="GET"||t.method==="HEAD"||t.method==="OPTIONS"?h.end():t.pipe(h)},"pipeUpstream"),I=c(async(r,t,e)=>{const p=t.__lensmcpTrace;if(r.pool){const s=r.svc;s&&(s.lastUsed=Date.now(),s.inflight+=1,e.on("close",()=>{s.inflight-=1,s.lastUsed=Date.now()}));let o=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs);if(!o&&s&&(T(p,"cold-start",{project:r.project}),await w.ensureUp(s)&&(o=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs))),s&&o&&w.markUp(s,"observed"),s&&w.scaleUp(s),!o){const h=s?.lastErrors?.length?` Service errors: ${s.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(e,t,503,"unavailable",`Service ${r.project} is not reachable (no pods in ${r.pool.dir}).${h} The watcher recovers and the next request retries automatically.`);return}const a=path.basename(o,".sock");t.__lensmcpPod=a,T(p,"pod-select",{pod:a,pods:r.pool.socks.length,strategy:"round-robin"});let d=!1;const y=c(h=>{r.pool.socks=r.pool.socks.filter(n=>n!==o),r.pool.idx=-1;try{fs.unlinkSync(o)}catch{}console.error(`[gateway] pod ${a} of ${r.project}: ${h}`)},"evictPod"),_=setTimeout(()=>{d||e.headersSent||e.writableEnded||(d=!0,y(`no response within ${types_1.POD_RESPONSE_TIMEOUT_MS}ms (wedged socket) \u2014 evicted + respawning`),(0,edge_1.sendError)(e,t,502,"unavailable",`Pod ${a} of ${r.project} was unresponsive \u2014 evicted, retry.`))},types_1.POD_RESPONSE_TIMEOUT_MS);_.unref?.(),e.on("close",()=>clearTimeout(_)),R({socketPath:o},t,e,{onResponse:c(()=>{clearTimeout(_),d=!0},"onResponse"),onError:c(h=>{clearTimeout(_),!d&&(d=!0,y(h.message),(0,edge_1.sendError)(e,t,502,"unavailable",`Pod ${a} of ${r.project} unavailable \u2014 retry.`))},"onError")});return}const i=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let g=0,l=!1;const u=c(()=>{const s=r.target;R(s,t,e,{onResponse:c(()=>{},"onResponse"),onError:c(o=>{if(isConnError(o)&&(g+=1),r.lens&&isConnError(o)&&g>=2&&!l&&!e.headersSent&&!e.writableEnded&&!e.destroyed){l=!0,k(r).then(a=>{if(!(e.headersSent||e.writableEnded||e.destroyed)){if(a==="ready"){u();return}if(a==="unsupported"&&Date.now()<i){setTimeout(u,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}b(r,t,e,r.target,o,a)}});return}if(isConnError(o)&&Date.now()<i&&!e.headersSent&&!e.writableEnded&&!e.destroyed){setTimeout(u,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(e.headersSent||e.writableEnded||e.destroyed){e.destroyed||e.destroy();return}b(r,t,e,s,o)},"onError")})},"tryTcp");u()},"forwardNative");return{proxy:S,forward:c(async(r,t,e)=>{const p=Date.now(),i=t.__lensmcpTrace;if(e.on("close",()=>{C(r,Date.now()-p,e.statusCode??0,t.__lensmcpCaller),N(i,r.project,e.statusCode??0,{...t.__lensmcpPod?{pod:t.__lensmcpPod}:{},...t.__lensmcpCaller?{caller:t.__lensmcpCaller}:{}})}),r.prependPrefix&&!t.url?.startsWith(r.prependPrefix)&&(t.url=r.prependPrefix+(t.url??"/")),(t.httpVersionMajor??1)>=2){await I(r,t,e);return}if(r.pool){const o=r.svc;o&&(o.lastUsed=Date.now(),o.inflight+=1,e.on("close",()=>{o.inflight-=1,o.lastUsed=Date.now()}));let a=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs);if(!a&&o&&(T(i,"cold-start",{project:r.project}),await w.ensureUp(o)&&(a=(0,discovery_1.pickSock)(r.pool,E.scanTtlMs))),o&&a&&w.markUp(o,"observed"),o&&w.scaleUp(o),!a){const n=o?.lastErrors?.length?` Service errors: ${o.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(e,t,503,"unavailable",`Service ${r.project} is not reachable (no pods in ${r.pool.dir}).${n} The watcher recovers and the next request retries automatically.`);return}const d=path.basename(a,".sock");t.__lensmcpPod=d,T(i,"pod-select",{pod:d,pods:r.pool.socks.length,strategy:"round-robin"});let y=!1;const _=c(n=>{r.pool.socks=r.pool.socks.filter(m=>m!==a),r.pool.idx=-1;try{fs.unlinkSync(a)}catch{}console.error(`[gateway] pod ${d} of ${r.project}: ${n}`)},"evictPod"),h=setTimeout(()=>{y||e.headersSent||e.writableEnded||(y=!0,_(`no response within ${types_1.POD_RESPONSE_TIMEOUT_MS}ms (wedged socket) \u2014 evicted + respawning`),(0,edge_1.sendError)(e,t,502,"unavailable",`Pod ${d} of ${r.project} was unresponsive \u2014 evicted, retry.`))},types_1.POD_RESPONSE_TIMEOUT_MS);h.unref?.(),e.on("close",()=>clearTimeout(h)),S.web(t,e,{target:{socketPath:a},agent:P},n=>{clearTimeout(h),!y&&(y=!0,_(n.message),(0,edge_1.sendError)(e,t,502,"unavailable",`Pod ${d} of ${r.project} unavailable \u2014 retry.`))});return}const g=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let l=0,u=!1;const s=c(()=>{const o=r.target,a={target:o,autoRewrite:typeof o=="string",changeOrigin:!0,agent:$(o)};S.web(t,e,a,d=>{if(isConnError(d)&&(l+=1),r.lens&&isConnError(d)&&l>=2&&!u&&!e.headersSent&&!e.writableEnded&&!e.destroyed){u=!0,k(r).then(y=>{if(!(e.headersSent||e.writableEnded||e.destroyed)){if(y==="ready"){s();return}if(y==="unsupported"&&Date.now()<g){setTimeout(s,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}b(r,t,e,r.target,d,y)}});return}if(isConnError(d)&&Date.now()<g&&!e.headersSent&&!e.writableEnded&&!e.destroyed){setTimeout(s,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}e.destroyed||b(r,t,e,o,d)})},"tryTcp");s()},"forward"),agentForTarget:$,destroyAgents:c(()=>{P.destroy(),O.destroy()},"destroyAgents"),closeProxy:c(()=>{try{S.close?.()}catch{}},"closeProxy")}}x(createProxy,"createProxy"),c(createProxy,"createProxy");
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var re=Object.defineProperty;var _=(r,t)=>re(r,"name",{value:t,configurable:!0});var ee=Object.defineProperty,n=_((r,t)=>ee(r,"name",{value:t,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),control_1=require("./control"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),types_1=require("./types");function insecureLocalFetch(r,t){return new Promise((y,g)=>{const h=(r.startsWith("https:")?require("node:https"):require("node:http")).request(r,{method:"GET",headers:t?.headers,rejectUnauthorized:!1},E=>{const l=[];E.on("data",u=>l.push(u)),E.on("end",()=>{const u=E.statusCode??0,a=Buffer.concat(l).toString("utf8");y({ok:u>=200&&u<300,status:u,json:n(async()=>JSON.parse(a),"json")})})});h.on("error",g),h.end()})}_(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(r){const t=process.env.LENSMCP_GW_JWKS_URL,y=process.env.LENSMCP_GW_JWT_ISS,g=new Map;let h;const E=n((l,u)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${l}${u}`);const a=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:l,...y?{issuer:y}:{},fetchImpl:insecureLocalFetch});return a.refresh().catch(w=>console.warn("[gateway] edge JWKS warm failed (will retry):",w.message)),a},"make");return{for(l){if(t)return h??=E(t,"");const u=l.split(":")[0].toLowerCase();let a;for(const v of r()){const m=v.host;if(typeof m!="string"||!/^auth\./.test(m)||m.startsWith("internal."))continue;const S=m.slice(5);(u===S||u.endsWith(`.${S}`))&&(!a||S.length>a.length)&&(a=S)}if(!a)return;let w=g.get(a);return w||(w=E(`https://auth.${a}/jwks.json`,` (auto-derived for apex ${a})`),g.set(a,w)),w},stop(){h?.stop();for(const l of g.values())l.stop()}}}_(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(r,t){return r&&exports.BENIGN_SOCKET_ERRNOS.has(r)?{status:null,report:!1,cause:"client-abort"}:r==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:r==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:r==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:r?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:r??t??"unknown"}}_(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(r=1e4){const t=new Map;return y=>{const g=Date.now(),h=t.get(y)??0;return g-h<r?!1:(t.set(y,g),!0)}}_(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(r){r.on("error",t=>{exports.BENIGN_SOCKET_ERRNOS.has(t.code??"")||console.warn(`[gateway] inbound socket error (${t.code??t.message}) \u2014 dropping this connection only`),r.destroy()})}_(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");async function startGateway(r,t){const y=r.https!==!1,g=r.ports?.length?r.ports:[443],h=r.scanTtlMs??1500,E=r.startupTimeoutMs??12e4,l=r.sweepMs??1e4,u=r.trafficFlushMs??5e3;let a=!1;const w=process.env.LENSMCP_EVENT_FILE??path.join(t.root,".lensmcp","events.jsonl"),v=(0,scope_1.readLensScope)(t.root).key,m=t.projectsConfigurations?.projects??{},S=(0,discovery_1.projectSourceRoots)(t.root,m,t.projectDependencies),{routes:N,services:T}=(0,discovery_1.discoverRoutes)(t.root,m,void 0,v,S);if(N.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const q=new route_registry_1.RouteRegistry;q.register({wsKey:v,root:t.root,routes:N,services:T});const k=[];let I=n(()=>{},"refreshCert");const L=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(k,q),I()},"rebuildRoutes");L();const{serviceKeys:B,keyToProject:J}=(0,service_keys_1.loadServiceKeys)(t.root,T),f={root:t.root,wsKey:v,eventFile:w,routes:k,registry:q,rebuildRoutes:L,services:T,serviceKeys:B,keyToProject:J,scanTtlMs:h,startupTimeoutMs:E,sweepMs:l,trafficFlushMs:u,https:y,projectRoots:Object.fromEntries(Object.entries(t.projectsConfigurations?.projects??{}).map(([e,o])=>[e,path.resolve(t.root,o.root)])),projectSourceRoots:S,stopped:n(()=>a,"stopped")},p=(0,observability_1.createObservability)(w,{trafficFlushMs:u}),Y=createClientErrorLogGate(),b=(0,lifecycle_1.createServiceLayer)(f,p),C=(0,lens_children_1.createLensChildren)(f,p,r),A=new Map;let W;try{W=(0,control_1.startControlServer)({rt:f,killService:n((e,o)=>b.killSpawned(e,o),"killService"),onRegister:n(async e=>{const o=await C.hostWorkspaceDashboard(e),i=await C.hostWorkspaceApps(e);A.set(e.wsKey,()=>{o?.(),i()})},"onRegister"),onUnregister:n(e=>{A.get(e.wsKey)?.(),A.delete(e.wsKey)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const R=(0,proxy_1.createProxy)(f,p,b),G=buildEdgeVerifierPool(()=>f.routes),K=(0,auth_1.createEdgeAuth)(f,p,G);let M;if(r.middleware){const e=require(path.resolve(t.root,r.middleware));M=e.default??e}const z=n(e=>{if(M)try{M(e.req)}catch(o){p.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:o.message}),e.route&&p.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${o.message}`)}},"builtinMiddleware"),U=r.hooks??{},F=(0,hooks_1.createHooks)({...U,afterAuth:[z,...U.afterAuth??[]]}),H=(0,handler_1.createHandler)({rt:f,obs:p,auth:K,proxy:R,hooks:F}),Q=(0,upgrade_1.createUpgrade)({rt:f,auth:K,proxy:R,hooks:F}),X=p.startEdgeFlusher(),Z=b.startSweeper();await C.bootSingletonsAndApps();for(const e of T)e.decl.eager&&b.ensureUp(e);let P,O,$;if(y){const e=require("../../../basic-ssl"),o=(0,service_keys_1.gatewayCacheDir)(t.root);$=n(()=>e.getCertificateSync(o,"gateway",f.routes.map(i=>i.host).filter(i=>!!i)),"mintCert"),P=$(),O=e.caCertPath()}const x=[],j=[];for(const e of g){const o=P?require("node:http2").createSecureServer({key:P,cert:P,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512},(i,s)=>H(i,s)):http.createServer(H);if(o.on("upgrade",Q),o.on("error",i=>console.error(`[gateway] port ${e}: ${i.code}`)),o.on("connection",guardInboundSocket),o.on("secureConnection",guardInboundSocket),o.on("tlsClientError",(i,s)=>s.destroy()),o.on("clientError",(i,s)=>{const d=i,c=classifyClientError(d.code,d.message);if(c.report&&Y(c.cause)){const V=s.remoteAddress??"unknown",D=c.status===null?"dropped":String(c.status);console.warn(`[gateway] inbound H1 clientError from ${V}: ${c.cause} (${d.code??d.message}${d.bytesParsed!==void 0?`, bytesParsed=${d.bytesParsed}`:""}) \u2192 ${D}. This status is the GATEWAY's, not the upstream service's.`),p.emit("warning",`inbound clientError: ${c.cause} \u2192 ${D}`,`gateway-client-error:${c.cause}`,{kind:"gateway-client-error",cause:c.cause,code:d.code??null,status:c.status,peer:V,bytesParsed:d.bytesParsed??null})}c.status!==null&&s.writable&&s.end(`HTTP/1.1 ${c.status} ${CLIENT_ERROR_REASON[c.status]}\r
|
|
2
2
|
\r
|
|
3
|
-
`),s.destroy()}),
|
|
3
|
+
`),s.destroy()}),o.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,o.headersTimeout<types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS+5e3&&(o.headersTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS+5e3),P!=null){const i=o;i.on("session",s=>{s.on("error",d=>{const c=d.code??d.message;console.warn(`[gateway] h2 session error (${c}) \u2014 dropping this session only`),s.destroyed||s.destroy()})}),i.on("sessionError",s=>{console.warn(`[gateway] h2 sessionError: ${s.code??s.message}`)})}await new Promise(i=>{o.listen(e,()=>{const s=o.address()?.port??e;j.push(s),console.log(`[gateway] Listening on ${P?"https":"http"}://localhost:${s}`),i()})}),x.push(o)}$&&(I=n(()=>{const e=$();for(const o of x)o.setSecureContext?.({key:e,cert:e})},"refreshCert")),console.log("[gateway] Routes:");for(const e of k){const o=e.pool?`pods@${e.pool.dir}`:e.target;console.log(`[gateway] ${e.host??"(default)"} \u2192 ${o}${e.prependPrefix?` (+${e.prependPrefix})`:""}${e.internal?" [internal: no middleware]":""} [${e.project}]`)}return O&&console.log(`[gateway] CA: ${O} \u2014 one-time setup via the trust executor.`),p.emit("info",`gateway up: ${k.length} routes on ${j.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:j,routes:k.map(e=>({host:e.host??"(default)",project:e.project,mode:e.pool?"pods":"tcp",internal:!!e.internal,prependPrefix:e.prependPrefix}))}),{ports:j,routes:k,services:T,stop:n(async()=>{if(!a){a=!0,clearInterval(X),clearInterval(Z),p.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),G.stop(),W?.close(),(0,workspace_registry_1.unregisterWorkspace)(v);for(const e of T)b.killSpawned(e,"gateway shutdown");await C.reapAll(),await Promise.all(x.map(e=>new Promise(o=>e.close(()=>o())))),R.closeProxy(),R.destroyAgents()}},"stop")}}_(startGateway,"startGateway"),n(startGateway,"startGateway");
|
|
@@ -21,6 +21,8 @@ export interface GatewayContext {
|
|
|
21
21
|
root: string;
|
|
22
22
|
}>;
|
|
23
23
|
};
|
|
24
|
+
/** Nx project-graph adjacency, populated by the executor wrapper when available. */
|
|
25
|
+
projectDependencies?: Record<string, string[]>;
|
|
24
26
|
}
|
|
25
27
|
/** Per-project declaration: top-level `cluster` field in project.json (`davnx` legacy). */
|
|
26
28
|
export interface ClusterProjectDecl {
|
|
@@ -102,13 +104,51 @@ export interface Route {
|
|
|
102
104
|
* the gateway owns). */
|
|
103
105
|
export interface LensFrontend {
|
|
104
106
|
project: string;
|
|
107
|
+
/** Owning workspace, used to isolate same-named frontends in the shared daemon. */
|
|
108
|
+
workspaceKey?: string;
|
|
105
109
|
/** Absolute project root (holds vite.config.*). */
|
|
106
110
|
projectRoot: string;
|
|
107
111
|
/** The pinned Vite dev port (also the upstream target's port). */
|
|
108
112
|
port: number;
|
|
113
|
+
/** Project root plus transitive Nx dependency roots used for source-set health checks. */
|
|
114
|
+
sourceRoots?: string[];
|
|
109
115
|
/** Set once the Vite child has been spawned (idempotency guard). */
|
|
110
116
|
started?: boolean;
|
|
111
117
|
}
|
|
118
|
+
export interface SourceSetChange {
|
|
119
|
+
changedAt: number;
|
|
120
|
+
/** Repository-relative, slash-normalized source paths. */
|
|
121
|
+
added: string[];
|
|
122
|
+
removed: string[];
|
|
123
|
+
/** More paths changed than the diagnostic payload retains. */
|
|
124
|
+
truncated: boolean;
|
|
125
|
+
}
|
|
126
|
+
export interface LensWatcherEvent {
|
|
127
|
+
sequence: number;
|
|
128
|
+
kind: 'add' | 'addDir' | 'unlink' | 'unlinkDir';
|
|
129
|
+
/** Absolute, normalized path reported by Vite's watcher. */
|
|
130
|
+
path: string;
|
|
131
|
+
at: number;
|
|
132
|
+
}
|
|
133
|
+
export interface LensWatcherHealth {
|
|
134
|
+
schemaVersion: 1 | 2;
|
|
135
|
+
ready: boolean;
|
|
136
|
+
sequence: number;
|
|
137
|
+
lastFsSetEventAt: number;
|
|
138
|
+
fsSetEvents: number;
|
|
139
|
+
recentEvents: LensWatcherEvent[];
|
|
140
|
+
}
|
|
141
|
+
export interface LensFrontendRecoveryStatus {
|
|
142
|
+
state: 'starting' | 'ready' | 'replacing' | 'failed';
|
|
143
|
+
generation: number;
|
|
144
|
+
reason?: string;
|
|
145
|
+
startedAt?: number;
|
|
146
|
+
}
|
|
147
|
+
export interface LensFrontendSupervisor {
|
|
148
|
+
status(frontend: LensFrontend): LensFrontendRecoveryStatus | undefined;
|
|
149
|
+
/** Ensure a replacement is running and resolve when its route is ready. */
|
|
150
|
+
recover(frontend: LensFrontend, reason: string): Promise<boolean>;
|
|
151
|
+
}
|
|
112
152
|
export interface ServiceCtl {
|
|
113
153
|
project: string;
|
|
114
154
|
decl: ClusterProjectDecl;
|
|
@@ -317,6 +357,14 @@ export declare const LENS_PARKED_PORT_REPROBE_MS: number;
|
|
|
317
357
|
* does honor SIGTERM exits first and the escalation no-ops. Override
|
|
318
358
|
* `LENSMCP_RECYCLE_KILL_GRACE_MS`. Default 5s. */
|
|
319
359
|
export declare const LENS_RECYCLE_KILL_GRACE_MS: number;
|
|
360
|
+
/** Rolling is the safe default. `in-place` is an emergency compatibility escape hatch. */
|
|
361
|
+
export declare const LENS_RESTART_MODE: 'rolling' | 'in-place';
|
|
362
|
+
/** Maximum cold-start allowance for a standby Vite while the active generation keeps serving. */
|
|
363
|
+
export declare const LENS_REPLACEMENT_READY_MS: number;
|
|
364
|
+
/** Grace for requests already forwarded to the old generation after the route flips. */
|
|
365
|
+
export declare const LENS_REPLACEMENT_DRAIN_MS: number;
|
|
366
|
+
/** Per-request wait for an unplanned managed-frontend recovery. */
|
|
367
|
+
export declare const LENS_REQUEST_RECOVERY_WAIT_MS: number;
|
|
320
368
|
/** Inbound keep-alive idle window for the front door. MUST exceed every client's pooled-socket idle timeout
|
|
321
369
|
* so the SERVER never closes a keep-alive socket a client is about to reuse — otherwise both sides race the
|
|
322
370
|
* same deadline and the truncated request surfaces as a spurious front-door `400` (see
|
|
@@ -402,11 +450,19 @@ export interface GatewayRuntime {
|
|
|
402
450
|
* consumer prefers its own entry and falls back to the wide clock when absent (project root unknown, or
|
|
403
451
|
* the scan not yet warmed). Mutable, written only by the scanner. */
|
|
404
452
|
sourceSetChangedFor?: Map<string, number>;
|
|
453
|
+
/** Exact path evidence paired with `sourceSetChangedFor`. */
|
|
454
|
+
sourceSetChangesFor?: Map<string, SourceSetChange>;
|
|
455
|
+
/** Workspace-wide fallback evidence when a project scope is unavailable. */
|
|
456
|
+
sourceSetChange?: SourceSetChange;
|
|
405
457
|
/** Absolute project root per project name (from `projectsConfigurations`) — lets the scanner derive each
|
|
406
458
|
* child's watch scope, and tells it which top-level buckets are OWNED by a managed project. Absent ⇒
|
|
407
459
|
* every consumer falls back to the workspace-wide clock (previous behavior). */
|
|
408
460
|
readonly projectRoots?: Readonly<Record<string, string>>;
|
|
409
|
-
/**
|
|
461
|
+
/** Project root plus transitive Nx dependency roots. */
|
|
462
|
+
readonly projectSourceRoots?: Readonly<Record<string, string[]>>;
|
|
463
|
+
/** Managed lens frontend handoff state, installed by the child supervisor. */
|
|
464
|
+
lensSupervisor?: LensFrontendSupervisor;
|
|
465
|
+
/** frontend identity (`workspace:project`) → live HMR sockets, registered by the WS upgrade pipeline. A
|
|
410
466
|
* recycle pushes `full-reload` to these before killing the child ({@link ./hmr-reload}): the dying vite
|
|
411
467
|
* cannot announce its own death and its replacement never knew these tabs, so the gateway — which owns
|
|
412
468
|
* the decision AND proxied the sockets — is the only place the handoff can happen. Without it every open
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var O=Object.defineProperty;var L=(E,_)=>O(E,"name",{value:_,configurable:!0});var M=Object.defineProperty,R=L((E,_)=>M(E,"name",{value:_,configurable:!0}),"R");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=exports.LENS_REQUEST_RECOVERY_WAIT_MS=exports.LENS_REPLACEMENT_DRAIN_MS=exports.LENS_REPLACEMENT_READY_MS=exports.LENS_RESTART_MODE=exports.LENS_RECYCLE_KILL_GRACE_MS=exports.LENS_PARKED_PORT_REPROBE_MS=exports.MAX_PORT_CONFLICT_RETRIES=exports.CHILD_START_FAIL_MS=exports.PORT_FREE_POLL_MS=exports.PORT_FREE_WAIT_MS=exports.LENS_PROBE_BOOT_GRACE_MS=exports.LENS_PROBE_COOLDOWN_MS=exports.LENS_PROBE_TIMEOUT_MS=exports.LENS_PROBE_FAILS=exports.LENS_PROBE_GRACE_MS=exports.LENS_WATCHER_PROBE_ENABLED=exports.LENS_PROBE_ENABLED=exports.POD_STALE_RECYCLE_COOLDOWN_MS=exports.POD_STALE_SCAN_MS=exports.POD_RECYCLE_SETTLE_MS=exports.POD_STALE_GRACE_MS=exports.POD_MAX_AGE_MS=exports.POD_RECYCLE_ENABLED=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;function envInt(E,_,e=!1){const S=Number(process.env[E]);return!Number.isFinite(S)||S<0?_:S===0?e?0:_:S}L(envInt,"envInt"),R(envInt,"envInt"),exports.SERVICE_ERROR_THROTTLE_MS=5e3,exports.SERVICE_MAX_RESTARTS=5,exports.SERVICE_RESTART_WINDOW_MS=6e4,exports.UPSTREAM_HEAL_WINDOW_MS=8e3,exports.UPSTREAM_HEAL_STEP_MS=300,exports.POD_RESPONSE_TIMEOUT_MS=(()=>{const E=Number(process.env.LENSMCP_POD_RESPONSE_TIMEOUT_MS);return Number.isFinite(E)&&E>0?E:3e4})(),exports.WEDGE_RECYCLE_GRACE_MS=(()=>{const E=Number(process.env.LENSMCP_WEDGE_RECYCLE_GRACE_MS);return Number.isFinite(E)&&E>0?E:15e3})(),exports.POD_RECYCLE_ENABLED=process.env.LENSMCP_POD_RECYCLE!=="0",exports.POD_MAX_AGE_MS=envInt("LENSMCP_POD_MAX_AGE_MS",240*6e4,!0),exports.POD_STALE_GRACE_MS=envInt("LENSMCP_POD_STALE_GRACE_MS",1e4),exports.POD_RECYCLE_SETTLE_MS=envInt("LENSMCP_POD_RECYCLE_SETTLE_MS",8e3),exports.POD_STALE_SCAN_MS=envInt("LENSMCP_POD_STALE_SCAN_MS",3e4),exports.POD_STALE_RECYCLE_COOLDOWN_MS=envInt("LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS",6e4),exports.LENS_PROBE_ENABLED=process.env.LENSMCP_LENS_PROBE!=="0",exports.LENS_WATCHER_PROBE_ENABLED=process.env.LENSMCP_LENS_WATCHER_PROBE!=="0",exports.LENS_PROBE_GRACE_MS=envInt("LENSMCP_LENS_PROBE_GRACE_MS",45e3),exports.LENS_PROBE_FAILS=envInt("LENSMCP_LENS_PROBE_FAILS",2),exports.LENS_PROBE_TIMEOUT_MS=envInt("LENSMCP_LENS_PROBE_TIMEOUT_MS",1e3),exports.LENS_PROBE_COOLDOWN_MS=envInt("LENSMCP_LENS_PROBE_COOLDOWN_MS",3e4),exports.LENS_PROBE_BOOT_GRACE_MS=envInt("LENSMCP_LENS_PROBE_BOOT_GRACE_MS",24e4),exports.PORT_FREE_WAIT_MS=envInt("LENSMCP_PORT_FREE_WAIT_MS",1e4),exports.PORT_FREE_POLL_MS=envInt("LENSMCP_PORT_FREE_POLL_MS",250),exports.CHILD_START_FAIL_MS=envInt("LENSMCP_CHILD_START_FAIL_MS",1e4),exports.MAX_PORT_CONFLICT_RETRIES=envInt("LENSMCP_MAX_PORT_CONFLICT_RETRIES",5),exports.LENS_PARKED_PORT_REPROBE_MS=envInt("LENSMCP_PARKED_PORT_REPROBE_MS",3e5),exports.LENS_RECYCLE_KILL_GRACE_MS=envInt("LENSMCP_RECYCLE_KILL_GRACE_MS",5e3),exports.LENS_RESTART_MODE=process.env.LENSMCP_LENS_RESTART_MODE==="in-place"?"in-place":"rolling",exports.LENS_REPLACEMENT_READY_MS=envInt("LENSMCP_LENS_REPLACEMENT_READY_MS",3e4),exports.LENS_REPLACEMENT_DRAIN_MS=envInt("LENSMCP_LENS_REPLACEMENT_DRAIN_MS",2e3,!0),exports.LENS_REQUEST_RECOVERY_WAIT_MS=envInt("LENSMCP_LENS_REQUEST_RECOVERY_WAIT_MS",15e3),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=envInt("LENSMCP_KEEPALIVE_TIMEOUT_MS",76e3);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var m=Object.defineProperty;var u=(
|
|
1
|
+
"use strict";var m=Object.defineProperty;var u=(i,s)=>m(i,"name",{value:s,configurable:!0});var g=Object.defineProperty,d=u((i,s)=>g(i,"name",{value:s,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createUpgrade=createUpgrade;const manifest_1=require("../manifest"),discovery_1=require("./discovery"),edge_1=require("./edge"),hmr_reload_1=require("./hmr-reload");function createUpgrade(i){const{rt:s,auth:f,proxy:h,hooks:n}=i,y=d(r=>(0,manifest_1.matchRoute)(s.routes,(r.headers.host||"").split(":")[0],r.url??"/"),"match"),v=d(async(r,o,l)=>{for(const t of edge_1.GATEWAY_OWNED_HEADERS)delete r.headers[t];const e=y(r);if(!e){o.destroy();return}const a=n.upgradeContext(r,o,void 0);if(a.route=e,e.internal){const t=f.internal(r,e,void 0);if(!t.ok){o.destroy();return}a.caller=t.caller}else{const t=f.public(r,e,void 0);if(!t.ok){o.destroy();return}if(a.claims=t.claims,n.has("afterAuth")&&(await n.run("afterAuth",a),a.responded()))return}if(e.pool&&s.serviceKeys[e.project]&&(r.headers["x-internal-token"]=s.serviceKeys[e.project]),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),n.has("onUpgrade")&&(await n.run("onUpgrade",a),a.responded()))return;const p=e.pool?(()=>{const t=(0,discovery_1.pickSock)(e.pool,s.scanTtlMs);return t?{socketPath:t}:void 0})():e.target;if(!p){o.destroy();return}if(e.lens){const t=String(r.headers["sec-websocket-protocol"]??"").split(",").map(c=>c.trim());if(t.length===0||t.includes("vite-hmr")||t.every(c=>c==="")){const c=e.lens.workspaceKey?`${e.lens.workspaceKey}:${e.lens.project}`:e.lens.project;(0,hmr_reload_1.registerHmrClient)(s,c,o)}}h.proxy.ws(r,o,l,{target:p,changeOrigin:!e.pool,agent:h.agentForTarget(p)},()=>o.destroy())},"run");return(r,o,l)=>{o.on("error",()=>o.destroy()),v(r,o,l).catch(()=>o.destroy())}}u(createUpgrade,"createUpgrade"),d(createUpgrade,"createUpgrade");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.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,9 +65,9 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@lensmcp/core": "1.
|
|
69
|
-
"@lensmcp/node-instrumentation": "1.
|
|
70
|
-
"@lensmcp/nx-plugin": "1.
|
|
68
|
+
"@lensmcp/core": "1.20.1",
|
|
69
|
+
"@lensmcp/node-instrumentation": "1.20.1",
|
|
70
|
+
"@lensmcp/nx-plugin": "1.20.1",
|
|
71
71
|
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
72
72
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
73
73
|
"glob": "^11.0.0",
|