@lensmcp/cluster 1.21.3 → 1.21.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/create-webpack-dev.d.ts +45 -0
- package/create-webpack-dev.js +1 -1
- package/executors/gateway/body-stall.d.ts +37 -0
- package/executors/gateway/body-stall.js +1 -0
- package/executors/gateway/canonical-path.d.ts +65 -0
- package/executors/gateway/canonical-path.js +1 -0
- package/executors/gateway/client-ip.d.ts +44 -0
- package/executors/gateway/client-ip.js +1 -0
- package/executors/gateway/jwks-verify.js +1 -1
- package/executors/gateway/main.prod-gateway.js +2 -2
- package/executors/gateway/manifest.d.ts +27 -3
- package/executors/gateway/manifest.js +1 -1
- package/executors/gateway/prod-config.d.ts +74 -0
- package/executors/gateway/prod-config.js +1 -0
- package/executors/gateway/prod-runtime/access-log.js +1 -1
- package/executors/gateway/prod-runtime/app.d.ts +6 -0
- package/executors/gateway/prod-runtime/app.js +1 -1
- package/executors/gateway/prod-runtime/auth.d.ts +1 -1
- package/executors/gateway/prod-runtime/auth.js +1 -1
- package/executors/gateway/prod-runtime/body-stall.d.ts +5 -0
- package/executors/gateway/prod-runtime/body-stall.js +1 -0
- package/executors/gateway/prod-runtime/cors.js +1 -1
- package/executors/gateway/prod-runtime/edge.d.ts +19 -3
- package/executors/gateway/prod-runtime/edge.js +1 -1
- package/executors/gateway/prod-runtime/handler.js +1 -1
- package/executors/gateway/prod-runtime/rollout.d.ts +11 -4
- package/executors/gateway/prod-runtime/rollout.js +1 -1
- package/executors/gateway/prod-runtime/routing.js +1 -1
- package/executors/gateway/prod-runtime/server.js +1 -1
- package/executors/gateway/prod-runtime/types.d.ts +30 -3
- package/executors/gateway/prod-runtime/upgrade.d.ts +25 -0
- package/executors/gateway/prod-runtime/upgrade.js +10 -2
- package/executors/gateway/prod-runtime/upstream.d.ts +3 -1
- package/executors/gateway/prod-runtime/upstream.js +1 -1
- package/executors/gateway/providers-prod.d.ts +13 -4
- package/executors/gateway/providers-prod.js +1 -1
- package/executors/gateway/registry-source.js +1 -1
- package/executors/gateway/rollout-ops.js +2 -2
- package/executors/gateway/runtime/auth.d.ts +6 -1
- package/executors/gateway/runtime/auth.js +1 -1
- package/executors/gateway/runtime/control.js +2 -1
- package/executors/gateway/runtime/dev-auth.d.ts +6 -2
- package/executors/gateway/runtime/dev-auth.js +1 -1
- package/executors/gateway/runtime/discovery.js +1 -1
- package/executors/gateway/runtime/edge.d.ts +19 -0
- package/executors/gateway/runtime/edge.js +1 -1
- package/executors/gateway/runtime/handler.js +1 -1
- package/executors/gateway/runtime/lens-children.d.ts +7 -0
- package/executors/gateway/runtime/lens-children.js +1 -1
- package/executors/gateway/runtime/lifecycle.d.ts +69 -7
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/pod-probe.d.ts +37 -0
- package/executors/gateway/runtime/pod-probe.js +1 -0
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/scope.d.ts +15 -0
- package/executors/gateway/runtime/scope.js +1 -1
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/service-keys.d.ts +42 -1
- package/executors/gateway/runtime/service-keys.js +1 -1
- package/executors/gateway/runtime/types.d.ts +193 -1
- package/executors/gateway/runtime/types.js +1 -1
- package/executors/gateway/runtime/upgrade.js +1 -1
- package/executors/gateway/runtime/workspace-log.d.ts +7 -0
- package/executors/gateway/runtime/workspace-log.js +1 -0
- package/main.devserver.js +7 -7
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var y=Object.defineProperty;var c=(i,n)=>y(i,"name",{value:n,configurable:!0});var f=Object.defineProperty,l=c((i,n)=>f(i,"name",{value:n,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),dev_auth_1=require("./dev-auth"),edge_1=require("./edge");function createEdgeAuth(i,n,k){const{traceStep:a}=n;return{internal:l((e,h,t)=>{const u=String(e.headers["x-api-key"]??""),r=i.keyToProject.get(u);if(!r)return a(t,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."};e.headers["x-api-key-id"]=r;const o=i.keyToWorkspace?.get(u);return o?e.headers["x-api-key-workspace"]=o:delete e.headers["x-api-key-workspace"],e.__lensmcpCaller=r,delete e.headers["x-api-key"],a(t,"auth",{mode:"x-api-key",ok:!0,caller:r,...o?{callerWorkspace:o}:{}}),{ok:!0,caller:r}},"internal"),public:l((e,h,t,u)=>{const r=h.auth;if(r){const o=u??(0,edge_1.canonicalPath)(e.url??"/")??(e.url??"/").split("?")[0],p=(e.method??"GET").toUpperCase(),s=(0,manifest_1.authRuleFor)(r,o,p);if(s.mode==="jwt"){const m=String(e.headers.host??"").split(":")[0].toLowerCase(),d=(0,dev_auth_1.verifyDevJwt)(e,k?.for(m));return d?s.rule&&!(0,manifest_1.evalRule)(s.rule,(0,dev_auth_1.devAttrs)(e,d,o))?(a(t,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):((0,dev_auth_1.stampIdentity)(e,d),a(t,"auth",{mode:"jwt",ok:!0}),{ok:!0,claims:d}):(a(t,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"})}return a(t,"auth",{mode:s.mode}),{ok:!0}}return a(t,"auth",{mode:"public",unconfigured:!0}),{ok:!0}},"publicAuth")}}c(createEdgeAuth,"createEdgeAuth"),l(createEdgeAuth,"createEdgeAuth");
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
"use strict";var h=Object.defineProperty;var
|
|
1
|
+
"use strict";var h=Object.defineProperty;var p=(s,e)=>h(s,"name",{value:e,configurable:!0});var f=Object.defineProperty,l=p((s,e)=>f(s,"name",{value:e,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.controlSocketPath=controlSocketPath,exports.clusterVersion=clusterVersion,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"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log");function controlSocketPath(){const s=process.env.LENSMCP_HOME||os.homedir();return path.join(s,".lensmcp","control.sock")}p(controlSocketPath,"controlSocketPath"),l(controlSocketPath,"controlSocketPath");function clusterVersion(){let s=__dirname;for(let e=0;e<6;e++){const n=path.join(s,"package.json");if(fs.existsSync(n))try{const c=JSON.parse(fs.readFileSync(n,"utf8"));if(c.name==="@lensmcp/cluster"&&typeof c.version=="string")return c.version}catch{}const o=path.dirname(s);if(o===s)break;s=o}}p(clusterVersion,"clusterVersion"),l(clusterVersion,"clusterVersion");function readJsonBody(s){return new Promise((e,n)=>{let o="";s.on("data",c=>{o+=c,o.length>1e6&&n(new Error("body too large"))}),s.on("end",()=>{try{e(o?JSON.parse(o):{})}catch(c){n(c)}}),s.on("error",n)})}p(readJsonBody,"readJsonBody"),l(readJsonBody,"readJsonBody");function startControlServer(s){const{rt:e}=s,n=controlSocketPath();fs.mkdirSync(path.dirname(n),{recursive:!0});try{fs.unlinkSync(n)}catch{}const o=l((i,a,d)=>{i.writeHead(a,{"content-type":"application/json"}),i.end(JSON.stringify(d))},"send"),c=http.createServer((i,a)=>{(async()=>{try{const d=(i.url??"").split("?")[0];if(i.method==="GET"&&d==="/list")return o(a,200,{workspaces:e.registry.keys().map(r=>{const t=e.registry.get(r);return{key:r,root:t?.root,routes:t?.routes.length??0,services:t?.services.length??0}})});if(i.method==="GET"&&d==="/status"){const r=clusterVersion();return o(a,200,{daemon:{pid:process.pid,wsKey:e.wsKey,...r?{version:r}:{}},workspaces:e.registry.keys().map(t=>{const y=e.registry.get(t);return{key:t,root:y?.root,routes:y?.routes.length??0,services:y?.services.length??0,isDaemon:t===e.wsKey}}),services:e.registry.allServices().map(t=>({project:t.project,wsKey:t.wsKey,host:t.decl.host,state:t.state}))})}if(i.method==="POST"&&d==="/register"){const r=await readJsonBody(i);if(!r.wsKey||!r.root)return o(a,400,{error:"wsKey and root are required"});const t=r.projects??{},y=(0,discovery_1.projectSourceRoots)(r.root,t,r.projectDependencies),{routes:w,services:u}=(0,discovery_1.discoverRoutes)(r.root,t,void 0,r.wsKey,y),v={wsKey:r.wsKey,root:r.root,routes:w,services:u};try{const g=(0,service_keys_1.adoptServiceKeys)(e,r.root,u,r.wsKey);g.added.length>0&&console.log(`[gateway] adopted east-west keys for ${r.wsKey}: ${g.added.join(", ")}`)}catch(g){console.error(`[gateway] could not adopt east-west keys for ${r.wsKey}: ${g.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(r.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${r.wsKey}') is served by the daemon at ${e.root} (pid ${process.pid}); its gateway-level log is ${path.join(e.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
|
|
2
|
+
`)}catch{}return e.registry.register(v),await s.onRegister?.(v),e.rebuildRoutes(),o(a,200,{ok:!0,wsKey:r.wsKey,routes:w.length,services:u.length,workspaces:e.registry.keys()})}if(i.method==="POST"&&d==="/unregister"){const r=await readJsonBody(i);if(!r.wsKey)return o(a,400,{error:"wsKey is required"});const t=e.registry.unregister(r.wsKey);if(t){for(const y of t.services){const w=(0,service_keys_1.serviceKeyId)(y.wsKey??r.wsKey,y.project),u=e.serviceKeys[w];u&&(delete e.serviceKeys[w],e.keyToProject.delete(u),e.keyToWorkspace?.delete(u))}for(const y of t.services)s.killService(y,"workspace unregistered");s.onUnregister?.(t)}return e.rebuildRoutes(),o(a,200,{ok:!0,removed:!!t,workspaces:e.registry.keys()})}o(a,404,{error:"not found"})}catch(d){o(a,500,{error:d.message})}})()});return c.listen(n),{socketPath:n,close:l(()=>{c.close();try{fs.unlinkSync(n)}catch{}},"close")}}p(startControlServer,"startControlServer"),l(startControlServer,"startControlServer");
|
|
@@ -11,5 +11,9 @@ import type { JwtVerifier } from '../jwks-verify';
|
|
|
11
11
|
export declare function verifyDevJwt(req: http.IncomingMessage, verifier?: JwtVerifier): Record<string, unknown> | undefined;
|
|
12
12
|
/** Stamp verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
|
|
13
13
|
export declare function stampIdentity(req: http.IncomingMessage, claims: Record<string, unknown>): void;
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Build the ABAC attribute bag from the verified claims + request envelope. `ip` is the address the edge
|
|
16
|
+
* RESOLVED (the socket peer, or a trusted proxy's assertion — `../client-ip`), and `path` the DECODED
|
|
17
|
+
* canonical path, so a rule judges the same values routing and forwarding used (issues/dev-gateway/11).
|
|
18
|
+
*/
|
|
19
|
+
export declare const devAttrs: (req: http.IncomingMessage, claims: Record<string, unknown>, cpath?: string) => Attrs;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var h=Object.defineProperty;var a=(e,r)=>h(e,"name",{value:r,configurable:!0});var l=Object.defineProperty,u=a((e,r)=>l(e,"name",{value:r,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.devAttrs=void 0,exports.verifyDevJwt=verifyDevJwt,exports.stampIdentity=stampIdentity;const node_crypto_1=require("node:crypto"),edge_1=require("./edge");function bearerToken(e){const r=Array.isArray(e.headers.authorization)?e.headers.authorization[0]:e.headers.authorization,t=String(r??"");return t.startsWith("Bearer ")?t.slice(7):void 0}a(bearerToken,"bearerToken"),u(bearerToken,"bearerToken");function verifyDevJwt(e,r){const t=bearerToken(e);if(!t)return;const[s,n,f]=t.split(".");if(!s||!n||!f)return;if(r)return r.verify(t);let o;try{o=JSON.parse(Buffer.from(s,"base64url").toString()).alg}catch{return}if(typeof o=="string"&&o!=="HS256"){if(process.env.LENSMCP_GW_DEV_JWT_INSECURE_DECODE!=="1")return;try{const i=JSON.parse(Buffer.from(n,"base64url").toString());return typeof i.exp=="number"&&Date.now()/1e3>i.exp?void 0:i}catch{return}}const d=process.env.AUTH_JWT_SECRET??"tetros-dev-secret",y=(0,node_crypto_1.createHmac)("sha256",d).update(`${s}.${n}`).digest("base64url"),p=Buffer.from(f),c=Buffer.from(y);if(!(p.length!==c.length||!(0,node_crypto_1.timingSafeEqual)(p,c)))try{const i=JSON.parse(Buffer.from(n,"base64url").toString());return typeof i.exp=="number"&&Date.now()/1e3>i.exp?void 0:i}catch{return}}a(verifyDevJwt,"verifyDevJwt"),u(verifyDevJwt,"verifyDevJwt");function stampIdentity(e,r){const t=u((f,o)=>{o!=null&&o!==""&&(e.headers[f]=String(o))},"set");t("x-user-id",r.sub),t("x-tenant-id",r.tid);const s=r.roles;Array.isArray(s)&&s.length&&(e.headers["x-user-roles"]=s.map(String).join(","));const n=r.perms;Array.isArray(n)&&n.length&&(e.headers["x-user-permissions"]=n.map(String).join(","))}a(stampIdentity,"stampIdentity"),u(stampIdentity,"stampIdentity");const devAttrs=u((e,r,t)=>({...r,ip:(0,edge_1.clientIpOf)(e),method:(e.method??"GET").toUpperCase(),path:t??(0,edge_1.canonicalPath)(e.url??"/")??(e.url??"/").split("?")[0]}),"devAttrs");exports.devAttrs=devAttrs;
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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+(?:-g\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");
|
|
@@ -5,6 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type * as http from 'node:http';
|
|
7
7
|
import { type GatewayErrorReason } from '../gateway-errors';
|
|
8
|
+
import type { GatewayRuntime } from './types';
|
|
9
|
+
export { ENCODED_STRUCTURE, canonicalize, canonicalPath, RAW_TARGET, rawTargetOf, rememberRawTarget, type CanonicalTarget, } from '../canonical-path';
|
|
10
|
+
/** Where the handler / upgrade park the ONE resolved client address for the auth layer (`devAttrs` → `ip`). */
|
|
11
|
+
export declare const CLIENT_IP: unique symbol;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the authoritative client address ONCE per request (the socket peer, or what a TRUSTED front
|
|
14
|
+
* proxy asserted — see `../client-ip`) and park it on the request for the ABAC `ip` attribute. The HTTP
|
|
15
|
+
* handler and the WS upgrade both call this, so a rule sees one value however the client connected.
|
|
16
|
+
*/
|
|
17
|
+
export declare const resolveClientIp: (rt: Pick<GatewayRuntime, "clientIp">, req: http.IncomingMessage) => string;
|
|
18
|
+
/** The address {@link resolveClientIp} parked, or the raw socket peer when nothing resolved it yet. */
|
|
19
|
+
export declare const clientIpOf: (req: http.IncomingMessage) => string;
|
|
20
|
+
/**
|
|
21
|
+
* Stamp the `X-Forwarded-*` the UPSTREAM should see (idempotent per request): `X-Forwarded-For` is the
|
|
22
|
+
* trusted chain + peer, or JUST the peer when no front proxy is trusted — a client can no longer hand the
|
|
23
|
+
* service an arbitrary chain (h1 forwarded it verbatim; h2 appended to it). `X-Forwarded-Proto` is the
|
|
24
|
+
* edge's own scheme, `X-Forwarded-Host` the routed host. Prod's #11 twin (issues/dev-gateway/11).
|
|
25
|
+
*/
|
|
26
|
+
export declare const stampForwardingHeaders: (rt: Pick<GatewayRuntime, "clientIp" | "https">, req: http.IncomingMessage) => void;
|
|
8
27
|
/**
|
|
9
28
|
* Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
|
|
10
29
|
* `text/plain`) + an `x-request-id` header — the SAME localized contract a service returns, so a browser
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var l=Object.defineProperty;var c=(r,e)=>l(r,"name",{value:e,configurable:!0});var p=Object.defineProperty,t=c((r,e)=>p(r,"name",{value:e,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.PREFLIGHT_METHODS_ROUTED=exports.PREFLIGHT_METHODS_NOROUTE=exports.corsPreflight=exports.cors=exports.GATEWAY_OWNED_HEADERS=exports.sendError=exports.stampForwardingHeaders=exports.clientIpOf=exports.resolveClientIp=exports.CLIENT_IP=exports.rememberRawTarget=exports.rawTargetOf=exports.RAW_TARGET=exports.canonicalPath=exports.canonicalize=exports.ENCODED_STRUCTURE=void 0;const gateway_errors_1=require("../gateway-errors"),client_ip_1=require("../client-ip");var canonical_path_1=require("../canonical-path");Object.defineProperty(exports,"ENCODED_STRUCTURE",{enumerable:!0,get:t(function(){return canonical_path_1.ENCODED_STRUCTURE},"get")}),Object.defineProperty(exports,"canonicalize",{enumerable:!0,get:t(function(){return canonical_path_1.canonicalize},"get")}),Object.defineProperty(exports,"canonicalPath",{enumerable:!0,get:t(function(){return canonical_path_1.canonicalPath},"get")}),Object.defineProperty(exports,"RAW_TARGET",{enumerable:!0,get:t(function(){return canonical_path_1.RAW_TARGET},"get")}),Object.defineProperty(exports,"rawTargetOf",{enumerable:!0,get:t(function(){return canonical_path_1.rawTargetOf},"get")}),Object.defineProperty(exports,"rememberRawTarget",{enumerable:!0,get:t(function(){return canonical_path_1.rememberRawTarget},"get")}),exports.CLIENT_IP=Symbol.for("lensmcp.gateway.clientIp");const FORWARDED=Symbol.for("lensmcp.gateway.forwarded"),peerOnly=(0,client_ip_1.createClientIpResolver)({}),resolverOf=t(r=>r.clientIp??peerOnly,"resolverOf"),peerOf=t(r=>r.socket?.remoteAddress??"","peerOf"),resolveClientIp=t((r,e)=>{const o=resolverOf(r).clientIpOf(e,peerOf(e));return e[exports.CLIENT_IP]=o,o},"resolveClientIp");exports.resolveClientIp=resolveClientIp;const clientIpOf=t(r=>r[exports.CLIENT_IP]??peerOf(r),"clientIpOf");exports.clientIpOf=clientIpOf;const stampForwardingHeaders=t((r,e)=>{const o=e;if(o[FORWARDED])return;o[FORWARDED]=!0;const n=peerOf(e);e.headers["x-forwarded-for"]=resolverOf(r).forwardedFor(e,n),e.headers["x-forwarded-proto"]=r.https?"https":"http";const s=e.headers.host;(resolverOf(r).trustProxyHops===0||!e.headers["x-forwarded-host"])&&(s?e.headers["x-forwarded-host"]=s:delete e.headers["x-forwarded-host"])},"stampForwardingHeaders");exports.stampForwardingHeaders=stampForwardingHeaders;const sendError=t((r,e,o,n,s,i,d)=>{const a=(0,gateway_errors_1.ensureRequestId)(e);return r.headersSent||r.writeHead(o,{"content-type":gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE,"x-request-id":a,...d??{}}),r.end((0,gateway_errors_1.gatewayErrorBody)(o,n,a,s,i)),a},"sendError");exports.sendError=sendError,exports.GATEWAY_OWNED_HEADERS=["x-internal-token","x-api-key-id","x-api-key-workspace","x-user-id","x-tenant-id","x-user-roles","x-user-permissions"];const cors=t((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=t((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";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var O=Object.defineProperty;var w=(p,d)=>O(p,"name",{value:d,configurable:!0});var E=Object.defineProperty,h=w((p,d)=>E(p,"name",{value:d,configurable:!0}),"h");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHandler=createHandler;const manifest_1=require("../manifest"),service_keys_1=require("./service-keys"),gateway_errors_1=require("../gateway-errors"),discovery_1=require("./discovery"),edge_1=require("./edge"),observability_1=require("./observability"),chooser_1=require("./chooser");function createHandler(p){const{rt:d,obs:u,auth:y,proxy:m,hooks:i}=p,g=h(e=>(e.headers.host||"").split(":")[0],"hostOf"),_=h((e,r)=>{if((0,edge_1.cors)(e,r),e.method==="OPTIONS"){r.writeHead(204,{"access-control-allow-methods":edge_1.PREFLIGHT_METHODS_NOROUTE,"access-control-allow-headers":String(e.headers["access-control-request-headers"]??"*")}),r.end();return}const s=(e.headers.host||"").split(":")[0],t=d.routes.filter(o=>o.host&&(0,manifest_1.hostMatches)(o.host,s)&&o.prefix),n=(e.url??"/").split("?")[0];if((n==="/"||n==="/index.html"||n==="")&&d.routes.some(o=>o.host&&(0,manifest_1.hostMatches)(o.host,s)&&o.project===observability_1.LENS_DASHBOARD_PROJECT)){(0,chooser_1.serveChooser)(r,d.wsKey);return}if(t.length>0&&(n==="/health"||n==="/healthz")){const o=t.map(l=>({project:l.project,prefix:l.prefix,pods:l.pool?((0,discovery_1.pickSock)(l.pool,0),l.pool.socks.length):void 0,state:l.svc?.state??(l.target?"up":"unknown")})),f=o.every(l=>l.state!=="down"||!0);r.writeHead(f?200:503,{"content-type":"application/json"}),r.end(JSON.stringify({status:"ok",host:s,services:o},null,2));return}if(t.length>0&&n==="/.well-known/services"){r.writeHead(200,{"content-type":"application/json"}),r.end(JSON.stringify({host:s,services:t.map(o=>({prefix:o.prefix,project:o.project,internal:!!o.internal}))},null,2));return}(0,edge_1.sendError)(r,e,404,"not_found",t.length>0?`No service mounted at ${n}. Mounted on ${s}: ${[...new Set(t.map(o=>o.prefix))].join(", ")} (+ /health, /.well-known/services)`:"No gateway route for this hostname.")},"handleNoRoute"),S=h(async(e,r)=>{if(!e.headers.host){const n=e.authority??e.headers[":authority"];n&&(e.headers.host=n)}for(const n of edge_1.GATEWAY_OWNED_HEADERS)delete e.headers[n];(0,gateway_errors_1.ensureRequestId)(e);const s=u.beginTrace(e);s&&(u.traceStep(s,"received",{host:s.host,method:s.method,url:s.url}),e.__lensmcpTrace=s);const t=i.httpContext(e,r,s);try{const n=e.url??"/",o=(0,edge_1.canonicalize)(n);if(o===void 0){u.traceStep(s,"bad-path",{}),u.finishTrace(s,"gateway",400),(0,edge_1.sendError)(r,e,400,"bad_request","Bad Request");return}const f=o.policy,l=o.path+o.query;if(l!==n&&(0,edge_1.rememberRawTarget)(e,n),e.url=l,(0,edge_1.resolveClientIp)(d,e),i.has("onRequest")&&(await i.run("onRequest",t),t.responded()))return;const a=(0,manifest_1.matchRoute)(d.routes,g(e),f+o.query);if(!a){_(e,r);return}if(t.route=a,u.traceStep(s,"route-match",{project:a.project,...a.prefix?{prefix:a.prefix}:{},...a.host?{routeHost:a.host}:{},...a.internal?{internal:!0}:{}}),e.method==="OPTIONS"){(0,edge_1.cors)(e,r),r.writeHead(204,{"access-control-allow-methods":edge_1.PREFLIGHT_METHODS_ROUTED,"access-control-allow-headers":String(e.headers["access-control-request-headers"]??"*")}),r.end();return}if((0,edge_1.cors)(e,r),i.has("beforeAuth")&&(await i.run("beforeAuth",t),t.responded()))return;if(a.internal){const c=y.internal(e,a,s);if(!c.ok){u.finishTrace(s,a.project,c.status),(0,edge_1.sendError)(r,e,c.status,c.reason,c.detail);return}t.caller=c.caller}else{const c=y.public(e,a,s,f);if(!c.ok){u.finishTrace(s,a.project,c.status),(0,edge_1.sendError)(r,e,c.status,c.reason,c.detail);return}if(t.claims=c.claims,i.has("afterAuth")&&(await i.run("afterAuth",t),t.responded()))return}const v=a.pool?d.serviceKeys[(0,service_keys_1.serviceKeyId)(a.svc?.wsKey,a.project)]:void 0;if(v&&(e.headers["x-internal-token"]=v),(0,edge_1.stampForwardingHeaders)(d,e),i.has("beforeForward")&&(await i.run("beforeForward",t),t.responded()))return;i.has("onResponse")&&r.on("close",()=>{t.status=r.statusCode??0,i.run("onResponse",t)}),m.forward(a,e,r)}catch(n){t.error=n,console.error(`[gateway] ${new Date().toISOString()} pipeline error traceId=${(0,gateway_errors_1.ensureRequestId)(e)}: ${n?.stack??String(n)}`),i.has("onError")&&await i.run("onError",t),t.responded()||(0,edge_1.sendError)(r,e,500,"internal","Internal gateway error.")}},"runPipeline");return(e,r)=>{S(e,r)}}w(createHandler,"createHandler"),h(createHandler,"createHandler");
|
|
@@ -61,6 +61,13 @@ export interface LensChildren {
|
|
|
61
61
|
* (vite ignores SIGTERM → would orphan when the daemon exits). Async so `stop()` awaits the escalation. */
|
|
62
62
|
reapAll(): Promise<void>;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* PURE (given the probe): the first port at/after `desired` that is not claimed in-process, not `reserved`
|
|
66
|
+
* (another lens app's DECLARED port — free at the OS level only because that app has rolled away from it;
|
|
67
|
+
* stepping onto it made two apps trade ports for a day, issues/dev-gateway/05), and free at the OS level.
|
|
68
|
+
* Records the claim. Exported for the spec; `createLensChildren` wraps it with the log line.
|
|
69
|
+
*/
|
|
70
|
+
export declare function claimPort(desired: number, claimed: Set<number>, isPortFree: (port: number) => Promise<boolean>, reserved?: ReadonlySet<number>): Promise<number>;
|
|
64
71
|
export declare function createLensChildren(rt: GatewayRuntime, obs: Observability, options: GatewayRuntimeOptions, deps?: {
|
|
65
72
|
isPortFree?: (port: number) => Promise<boolean>;
|
|
66
73
|
probeWatcherHealth?: (port: number) => Promise<LensWatcherHealth | null>;
|
|
@@ -1 +1 @@
|
|
|
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
|
+
"use strict";var ye=Object.defineProperty;var P=(s,f)=>ye(s,"name",{value:f,configurable:!0});var Se=Object.defineProperty,d=P((s,f)=>Se(s,"name",{value:f,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.probeWatcherHealth=probeWatcherHealth,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.claimPort=claimPort,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,f){return new Promise(u=>{const _=net.createServer();_.unref(),_.once("error",p=>{const E=p.code??"";u(E==="EADDRINUSE"||E==="EACCES"?"in-use":"unavailable")}),_.listen(f===void 0?{port:s}:{port:s,host:f},()=>_.close(()=>u("free")))})}P(tryBind,"tryBind"),d(tryBind,"tryBind");async function probePortFree(s){for(const f of[void 0,"0.0.0.0","127.0.0.1","::1"])if(await tryBind(s,f)==="in-use")return!1;return!0}P(probePortFree,"probePortFree"),d(probePortFree,"probePortFree");function probeConnect(s,f,u){return new Promise(_=>{let p=!1;const E=net.createConnection({host:s,port:f});E.unref();const M=setTimeout(()=>y(!1),u);M.unref?.();function y(k){p||(p=!0,clearTimeout(M),E.destroy(),_(k))}P(y,"g"),d(y,"finish"),E.once("connect",()=>y(!0)),E.once("error",()=>y(!1))})}P(probeConnect,"probeConnect"),d(probeConnect,"probeConnect");async function probePortListening(s,f=1e3){return await probeConnect("127.0.0.1",s,f)?!0:probeConnect("::1",s,f)}P(probePortListening,"probePortListening"),d(probePortListening,"probePortListening");async function probeWatcherHealth(s,f=1500){try{const u=new AbortController,_=setTimeout(()=>u.abort(),f);_.unref?.();try{const p=await fetch(`http://127.0.0.1:${s}/__lensmcp/watcher-health`,{signal:u.signal});if(!p.ok)return null;const E=await p.json();if(typeof E?.lastFsSetEventAt!="number")return null;const M=Array.isArray(E.recentEvents)?E.recentEvents.filter(y=>y&&typeof y.path=="string"&&typeof y.at=="number"&&typeof y.sequence=="number"&&["add","addDir","unlink","unlinkDir"].includes(y.kind)):[];return{schemaVersion:E.schemaVersion===2?2:1,ready:E.ready!==!1,sequence:typeof E.sequence=="number"?E.sequence:0,lastFsSetEventAt:E.lastFsSetEventAt,fsSetEvents:typeof E.fsSetEvents=="number"?E.fsSetEvents:0,recentEvents:M}}finally{clearTimeout(_)}}catch{return null}}P(probeWatcherHealth,"probeWatcherHealth"),d(probeWatcherHealth,"probeWatcherHealth");const sleepUnref=d(s=>new Promise(f=>{setTimeout(f,s).unref?.()}),"sleepUnref");async function waitForPortFree(s,f,u={}){const _=u.isPortFree??probePortFree,p=Math.max(1,u.intervalMs??types_1.PORT_FREE_POLL_MS),E=Date.now()+Math.max(0,f);for(;;){if(await _(s))return!0;const M=E-Date.now();if(M<=0)return!1;await sleepUnref(Math.min(p,M))}}P(waitForPortFree,"waitForPortFree"),d(waitForPortFree,"waitForPortFree");function viteArgPort(s){const f=d(u=>Number.isInteger(u)&&u>0&&u<65536?u:void 0,"valid");for(let u=0;u<s.length;u+=1){const _=s[u];if(_==="--port")return f(Number(s[u+1]));if(_.startsWith("--port="))return f(Number(_.slice(7)))}}P(viteArgPort,"viteArgPort"),d(viteArgPort,"viteArgPort");async function claimPort(s,f,u,_){let p=s;for(;f.has(p)||_?.has(p)||!await u(p);)p+=1;return f.add(p),p}P(claimPort,"claimPort"),d(claimPort,"claimPort");function createLensChildren(s,f,u,_){const{emit:p}=f,E=_?.isPortFree??probePortFree,M=_?.probeWatcherHealth??probeWatcherHealth,y=[],k=new Map,m=new Map,j=new Map,B=new Map,K=new Set;let O;const A=new Map,$=new Set,W=new Set,N=new Set,v=new Map,D=new WeakMap,x=new Set,z=d((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 E(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"),F=d(async(e,r,n)=>{const t=await claimPort(e,r,E,n?.reserved);return t!==e&&(n?.heldBy?console.log(`[gateway] handoff: :${e} is held by ${n.heldBy} \u2014 replacement on :${t}.`):console.warn(`[gateway] port ${e} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${t} instead.`)),t},"claimFreePort"),ae=d((e,r,n)=>{const t=n?.LENSMCP_FRONTEND_KEY,o=Number(n?.LENSMCP_FRONTEND_GENERATION);if(!t||!Number.isFinite(o))return;const a=[...v.get(t)?.generations??[]].find(i=>i.generation===o);a&&(e.endsWith(":vite")?a.vite=r:e.endsWith(":browser-capture")&&(a.capture=r))},"attachRespawnedGenerationChild"),T=d((e,r,n,t,o=!1,a,i,l)=>{const w=Date.now(),c=(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"});y.push(c),A.set(e,c),ae(e,c,t);const S=e==="vite"||e.endsWith(":vite"),C=typeof t?.LENSMCP_EVENT_FILE=="string"?t.LENSMCP_EVENT_FILE:void 0,R=C&&C!==s.eventFile?C:void 0,g=S?viteArgPort(n):l;S&&m.set(c,{key:t?.LENSMCP_FRONTEND_KEY??`${i??s.root}:${t?.LENSMCP_PROJECT??"app"}`,project:t?.LENSMCP_PROJECT??"app",workspaceRoot:i??s.root,spawnedAt:w,port:g,probeFails:0,listening:!1,generation:Number(t?.LENSMCP_FRONTEND_GENERATION??0),role:t?.LENSMCP_FRONTEND_ROLE==="replacement"?"replacement":"active",...R?{bus:R}:{}});const h=e==="lens-mcp"?"lens-mcp":e==="lens-dashboard"||e.startsWith("lens-dashboard:")?"lens-dashboard":void 0;return h&&g!==void 0&&(p("info",`lens child starting: ${h} (:${g})`,`cluster-lens-child:${h}`,{kind:"lens-child-starting",project:h,label:e,pid:c.pid,port:g},void 0,R),(async()=>{const b=Date.now()+types_1.LENS_REPLACEMENT_READY_MS;for(;!s.stopped()&&Date.now()<b;){if(c.exitCode!==null||c.signalCode!==null||$.has(e))return;if(await probePortListening(g,Math.min(500,types_1.LENS_PROBE_TIMEOUT_MS))){p("info",`lens child up: ${h} (:${g})`,`cluster-lens-child:${h}`,{kind:"lens-child-up",project:h,label:e,pid:c.pid,port:g},void 0,R);return}await sleepUnref(150)}})().catch(()=>{})),c.on("exit",(b,V)=>{const te=y.indexOf(c);te>=0&&y.splice(te,1),A.get(e)===c&&A.delete(e);const oe=m.get(c);m.delete(c);const we=$.delete(e);if(s.stopped()||we||(h&&p("warning",`lens child exited: ${h}`,`cluster-lens-child:${h}`,{kind:"lens-child-exit",project:h,label:e,pid:c.pid,code:b,signal:V},void 0,R),oe&&ce(c,oe)))return;const ne=Date.now()-w,L=k.get(e)??{attempts:0,portConflicts:0};(async()=>{const _e=g!==void 0&&!await E(g),re=(0,lifecycle_1.isPortConflictExit)({code:b,signal:V,ranMs:ne,portHeld:_e},{fastFailMs:types_1.CHILD_START_FAIL_MS});if(re&&L.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)L.portConflicts+=1,k.set(e,L),console.warn(`[gateway] lens child ${e} lost the :${g} bind race (EADDRINUSE, code=${b}); waiting for the port, then retrying (port-retry ${L.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else if(re){L.portConflicts=0,k.set(e,L),console.warn(`[gateway] lens child ${e}: :${g} 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} (:${g} held externally)`,`cluster-lens-child:${e}`,{kind:"lens-child-parked",label:e,port:g}),z(e,g,()=>T(e,r,n,t,o,a,i,l));return}else{L.portConflicts=0,ne>=HEAL_HEALTHY_MS&&(L.attempts=0);const se=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**L.attempts);L.attempts+=1,k.set(e,L),console.warn(`[gateway] lens child ${e} exited (code=${b} sig=${V}); auto-healing in ${Math.round(se/1e3)}s (attempt ${L.attempts}).`),await sleepUnref(se)}s.stopped()||$.has(e)||(g!==void 0&&!await waitForPortFree(g,types_1.PORT_FREE_WAIT_MS,{isPortFree:E,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${e}: :${g} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(s.stopped()||$.has(e))&&T(e,r,n,t,o,a,i,l))})().catch(()=>{})}),c},"spawnManagedChild"),X=d(e=>{const r=`${e.wsKey}:${e.project}`,n=v.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};v.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"),ie=d(e=>{if(!types_1.LENS_WATCH_SCOPE_ENABLED)return{};const r=path.resolve(e.projectRoot),n=e.sourceDirs.map(t=>path.resolve(t)).filter(t=>t!==r);return n.length>0?{LENSMCP_WATCH_DIRS:n.join(path.delimiter)}:{}},"lensWatchScopeEnv"),J=d((e,r,n)=>{const t=++e.generation,o=`${e.key}:g${t}:`,a=`${o}vite`,i=`${o}browser-capture`;$.delete(a),$.delete(i);const l=d((S,C,R,g,h,b)=>T(S,C,R,g,h,b,e.workspaceRoot),"spawnInRoot"),w=(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,...ie(e),NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false"},labelPrefix:o,log:d(S=>console.log(S),"log")},l),c={ownerKey:e.key,generation:t,port:r,labelPrefix:o,vite:w?.vite??A.get(a),capture:A.get(i)};return e.generations.add(c),c},"spawnFrontendGeneration"),I=d((e,r)=>{if(!e)return;v.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&&N.delete(e.port)},"terminateGeneration"),le=d(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 M(e.port);if(!t||t.ready)return!0}await sleepUnref(150)}return!1},"waitForGenerationReady"),G=d(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=d(e=>!e.disposed&&!s.stopped()&&v.get(e.key)===e,"frontendIsCurrent"),Y=d((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 l=new Set;for(const h of v.values())h!==e&&l.add(h.desiredPort);const w=n&&n.port===e.desiredPort?`g${n.generation}`:void 0,c=await F(e.desiredPort,N,{reserved:l,heldBy:w});if(a=c,!H(e))return N.delete(c),!1;o=J(e,c,"replacement"),e.candidate=o;const S=await G(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:S},void 0,e.eventFile===s.eventFile?void 0:e.eventFile);const C=await le(o);if(!H(e))return I(o,!0),e.candidate=void 0,!1;if(!C){I(o,!0),e.candidate=void 0;const h=await G(n);return e.state=h?"ready":"failed",h&&(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:h},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),!1}for(const h of e.routes)h.target=`http://localhost:${o.port}`,h.lens&&(h.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 R=o.vite?m.get(o.vite):void 0;R&&(R.role="active",R.listening=!0);const g=(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}${g>0?`; reloading ${g} 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:g},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),n){const h=`${n.labelPrefix}browser-capture`;$.add(h);try{n.capture?.kill("SIGTERM")}catch{}setTimeout(()=>I(n,!0),types_1.LENS_REPLACEMENT_DRAIN_MS).unref?.()}return!0}catch(l){I(o,!0),!o&&a!==void 0&&N.delete(a),e.candidate=void 0;const w=await G(n);e.state=w?"ready":"failed",w&&(e.reason=void 0,e.startedAt=void 0);const c=l.message;return console.error(`[gateway] lens handoff failed: ${e.key}: ${c}`),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:w,error:c},void 0,e.eventFile===s.eventFile?void 0:e.eventFile),!1}finally{e.replacement=void 0}})();return e.replacement=t,t},"rollFrontend"),Q=d((e,r)=>{e.active=J(e,r,"active"),e.state="starting",e.startedAt=Date.now()},"spawnInitialFrontend"),ce=d((e,r)=>{const n=v.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 de=d(()=>{q||Date.now()-Z<types_1.POD_STALE_SCAN_MS||(q=!0,Z=Date.now(),(async()=>{for(const e of[...v.values()]){if(s.stopped())return;const r=Date.now(),n=(0,scope_1.readSourceSetOptions)(e.workspaceRoot),t=await(0,lifecycle_1.sourceSetSnapshot)(e.workspaceRoot,e.sourceDirs,n.exclude,{ignoreTests:n.ignoreTests});if(v.get(e.key)!==e)continue;const o=e.sourceSnapshot,a=e.sourceScanStartedAt;if(e.sourceSnapshot=t,e.sourceScanStartedAt=r,!o)continue;const i=Date.now(),l=(0,lifecycle_1.diffSourceSets)(o,t,i,(0,lifecycle_1.sourceChangeWindowMs)(i,a,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS));l&&(e.sourceChange=l,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:l.added,removed:l.removed,truncated:l.truncated},void 0,e.eventFile===s.eventFile?void 0:e.eventFile))}})().catch(()=>{}).finally(()=>{q=!1}))},"scanFrontendSourceSets");let U=!1;const pe=d(async(e,r)=>{if(!U){U=!0;try{for(const[n,t]of[...m]){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 l=v.get(t.key);l?.active?.vite===n&&(l.state="ready",l.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},j.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`;j.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"),ue=d(()=>{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()||m.size===0||(ee(),!types_1.POD_RECYCLE_ENABLED))return;de();const r=Date.now();for(const[n,t]of m){const o=v.get(t.key),a=o?.sourceChange?.changedAt??0,i=j.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 l=o?.sourceChange,w=l&&!l.truncated?[...l.added,...l.removed].map(R=>path.resolve(t.workspaceRoot,R)):[],c=types_1.LENS_WATCHER_PROBE_ENABLED&&t.port!==void 0?(0,lifecycle_1.lensWatcherVerdict)(await M(t.port),a,l?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,w):"unknown";if(c==="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(c==="unknown"&&types_1.LENS_WATCHER_UNVERIFIED==="keep"){console.log(`[gateway] stale source set: lens app ${t.project} unverifiable (no watcher testimony) \u2014 keeping it (LENSMCP_LENS_WATCHER_UNVERIFIED=keep).`);return}if(n.exitCode!==null||n.signalCode!==null)return;j.set(t.key,Date.now());const S=v.get(t.key);if(types_1.LENS_RESTART_MODE==="rolling"&&S){console.log(`[gateway] stale refresh: lens app ${t.key} (stale-source-set) \u2014 watcher ${c==="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:S.wsKey,project:t.project,reason:"stale-source-set",watcher:c,added:l?.added,removed:l?.removed},void 0,t.bus),await Y(S,"stale-source-set");return}t.listening=!1,console.log(`[gateway] zombie recycle: lens app ${t.project} (stale-source-set) \u2014 watcher ${c==="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:c},void 0,t.bus);const C=(0,hmr_reload_1.broadcastFullReload)(s,t.key);C>0&&console.log(`[gateway] \u2192 told ${C} 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"),fe=d(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 F(u.dashboardPort??o.dashboardPort,W),i=u.lensHost??"lensmcp.local",l=o.basePath;s.registry.appendRoutes(s.wsKey,[{host:i,prefix:l,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${a}`}]),s.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:o.key,root:s.root,base:l,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}${l}/)`),p("info",`lens dashboard up: ${i}${l} \u2192 :${a}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:i,port:a,base:l}),T("lens-dashboard",process.execPath,[t],{LENSMCP_EVENT_FILE:s.eventFile,LENSMCP_PROCESS_ROLE:"dashboard",LENSMCP_DASHBOARD_PORT:String(a),LENSMCP_DASHBOARD_BASE:l},!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=d(()=>{console.log(`[gateway] starting lens MCP server on 127.0.0.1:${a} (${t})`),T("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 E(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(w=>w.lens?.project===t.project),i=await F(t.port,N);for(const w of a)w.lens&&(w.lens.port=i),w.target=`http://localhost:${i}`;t.port=i;const l=X({wsKey:s.wsKey,project:t.project,workspaceRoot:s.root,projectRoot:t.projectRoot,eventFile:s.eventFile,routes:a,frontend:t,desiredPort:o});try{Q(l,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(w){N.delete(i),l.state="failed",console.error(`[gateway] lens app ${t.project}: ${w.message}`)}}ue()},"bootSingletonsAndApps"),Ee=d(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 F(n.dashboardPort,W),i=`lens-dashboard:${e.wsKey}`,l=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}),T(i,process.execPath,[r],{LENSMCP_EVENT_FILE:l,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),W.delete(a),(0,workspace_registry_1.unregisterWorkspace)(e.wsKey);try{A.get(i)?.kill("SIGTERM")}catch{}A.delete(i)}},"hostWorkspaceDashboard"),he=d(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 l=await F(i.port,N),w=i.port;for(const S of a)S.lens.started=!0,S.lens.port=l,S.target=`http://localhost:${l}`;const c=X({wsKey:e.wsKey,project:o,workspaceRoot:e.root,projectRoot:i.projectRoot,eventFile:n,routes:a,frontend:i,desiredPort:w});t.push(c),console.log(`[gateway] hosting lens app '${o}' of '${e.wsKey}' \u2192 vite :${l}`);try{Q(c,l),p("info",`lens app started: ${e.wsKey}/${o} (vite :${l})`,`cluster-lens-app:${e.wsKey}:${o}`,{kind:"lens-app-up",project:o,port:l},void 0,n)}catch(S){N.delete(l),c.state="failed",console.error(`[gateway] lens app ${e.wsKey}/${o}: ${S.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),j.delete(o.key),B.delete(o.key),K.delete(o.key),v.delete(o.key)}}},"hostWorkspaceApps"),ge=d(async(e=2500)=>{O&&(clearInterval(O),O=void 0);const r=[...y];for(const o of r)try{o.kill("SIGTERM")}catch{}const n=d(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=d(()=>pe({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:T,bootSingletonsAndApps:fe,hostWorkspaceDashboard:Ee,hostWorkspaceApps:he,probeLensFrontends:ee,reapAll:ge}}P(createLensChildren,"createLensChildren"),d(createLensChildren,"createLensChildren");
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Service lifecycle: scale-from-zero, autoscale, idle kill. The gateway is the
|
|
3
|
+
* load balancer in front of each pod service's unix-socket pool and owns the
|
|
4
|
+
* pods' serverless lifecycle (eager/minPods/maxPods/idleKillSec).
|
|
5
|
+
*/
|
|
6
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
7
|
+
import { probePodAlive } from './pod-probe';
|
|
8
|
+
import type { GatewayRuntime, LensWatcherHealth, PodWatcherTestimony, PodWatcherVerdict, ServiceCtl, SockPool, SourceSetChange } from './types';
|
|
2
9
|
import type { Observability } from './observability';
|
|
3
10
|
/** Bounded, async, non-blocking walk of the workspace source tree → a signature of the file SET
|
|
4
11
|
* (`<count>:<xor-of-path-hashes>`). Skips node_modules/dist/build/dotdirs + `*-devserver` sock dirs + every
|
|
@@ -14,10 +21,35 @@ export interface SourceSetSnapshot {
|
|
|
14
21
|
paths: Set<string>;
|
|
15
22
|
truncated: boolean;
|
|
16
23
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
/**
|
|
25
|
+
* PURE: is this (slash-normalized, repo-relative) path a TEST or STORY file — one no running app can import?
|
|
26
|
+
*
|
|
27
|
+
* The staleness signal exists to catch a dev server whose watcher missed a file it might need to resolve.
|
|
28
|
+
* A spec cannot be that file: Vite serves only what the browser graph imports and webpack only what the
|
|
29
|
+
* entry reaches, and neither reaches `*.spec.ts`. Yet the scan counted them, so every spec an agent session
|
|
30
|
+
* wrote flipped the set — the three `pod-refresh` events that survived rotation on foodguard (2026-09-06)
|
|
31
|
+
* were ALL specs (`shared/contracts/src/haccp/__tests__/zz-r3probe.spec.ts` rolled both lens vites;
|
|
32
|
+
* `web/apps/dashboard/src/i18n/knowledge-admin-registry.spec.ts` rolled the dashboard), and the same
|
|
33
|
+
* signal scaled all six backend pods to zero (issues/dev-gateway/01).
|
|
34
|
+
*/
|
|
35
|
+
export declare function isTestSourcePath(relPath: string): boolean;
|
|
36
|
+
export interface SourceSetScanOptions {
|
|
37
|
+
/** Skip test/story files ({@link isTestSourcePath}). Default `true`; `.lensmcp/config.json`
|
|
38
|
+
* `sourceSet.ignoreTests: false` restores count-everything. */
|
|
39
|
+
ignoreTests?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function sourceSetSnapshot(root: string, scopeDirs?: readonly string[], extraExclude?: ReadonlySet<string>, opts?: SourceSetScanOptions): Promise<SourceSetSnapshot>;
|
|
42
|
+
export declare function sourceSetSignature(root: string, scopeDirs?: readonly string[], extraExclude?: ReadonlySet<string>, opts?: SourceSetScanOptions): Promise<string>;
|
|
43
|
+
/** Exact, bounded evidence for a source-file-set transition. `windowMs` (see {@link SourceSetChange.windowMs})
|
|
44
|
+
* is how far before `changedAt` the fs event may have happened — the caller knows when its previous scan started. */
|
|
45
|
+
export declare function diffSourceSets(previous: SourceSetSnapshot, next: SourceSetSnapshot, changedAt: number, windowMs?: number): SourceSetChange | null;
|
|
46
|
+
/**
|
|
47
|
+
* PURE: how far before a scan-stamped `changedAt` the underlying fs event may lie. The change was not in the
|
|
48
|
+
* PREVIOUS snapshot, so it happened after that scan started (a file created mid-walk may or may not have been
|
|
49
|
+
* seen — hence "started", not "completed"); plus the settle margin the callers already reason in. With no
|
|
50
|
+
* previous scan start (first diff) fall back to one scan period.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sourceChangeWindowMs(now: number, previousScanStartedAt: number | undefined, scanPeriodMs: number, settleMs: number): number;
|
|
21
53
|
/**
|
|
22
54
|
* PURE: which subtrees a child of `projectRoot` must watch for a "file set changed" verdict.
|
|
23
55
|
*
|
|
@@ -53,7 +85,7 @@ export interface PodRecycleConfig {
|
|
|
53
85
|
* it must be left alone. Kept pure (no `ServiceCtl` mutation, no fs) so the sweeper's logic is unit-tested
|
|
54
86
|
* without spawning a process. `sourceSetChangedAt` is 0 until the gateway first observes a set change.
|
|
55
87
|
*/
|
|
56
|
-
export declare function podRecycleReason(svc: Pick<ServiceCtl, 'proc' | 'state' | 'spawnAt' | 'inflight' | 'lastUsed' | 'lastStaleRecycleAt'>, now: number, sourceSetChangedAt: number, cfg: PodRecycleConfig): 'stale-source-set' | 'max-age' | null;
|
|
88
|
+
export declare function podRecycleReason(svc: Pick<ServiceCtl, 'proc' | 'state' | 'spawnAt' | 'inflight' | 'lastUsed' | 'lastStaleRecycleAt' | 'activeWork'>, now: number, sourceSetChangedAt: number, cfg: PodRecycleConfig): 'stale-source-set' | 'max-age' | null;
|
|
57
89
|
/**
|
|
58
90
|
* PURE decision for a LENS-FRONTEND vite child (a `lens:true` app's dev server — the reported zombie). It
|
|
59
91
|
* has no in-flight/idle signal like a pod, so it recycles ONLY on a settled source-file-SET change that
|
|
@@ -88,6 +120,19 @@ export type LensWatcherVerdict = 'heard' | 'missed' | 'unknown';
|
|
|
88
120
|
export declare function lensWatcherVerdict(probe: Pick<LensWatcherHealth, 'schemaVersion' | 'lastFsSetEventAt' | 'recentEvents'> | {
|
|
89
121
|
lastFsSetEventAt: number;
|
|
90
122
|
} | null, changedAt: number, windowMs: number, changedPaths?: readonly string[]): LensWatcherVerdict;
|
|
123
|
+
/**
|
|
124
|
+
* PURE verdict for a POD (webpack) — the twin of {@link lensWatcherVerdict}, over {@link PodWatcherTestimony}.
|
|
125
|
+
*
|
|
126
|
+
* webpack cannot say "I heard THAT add" (watchpack reports only the files/dirs the compilation asked for), so
|
|
127
|
+
* the question is proof of LIFE across the change window: a watcher that woke (`watchRun`) or built (`done`)
|
|
128
|
+
* at/after `changedAt - windowMs` is alive, and a live watchpack is never wedged by an add — it already
|
|
129
|
+
* watches every `missingDependency`, so the file an importer was waiting for triggers its own rebuild.
|
|
130
|
+
* - `heard` — a stamp inside the window → keep, HMR/hot-swap intact.
|
|
131
|
+
* - `degraded` — the watcher errored / closed → recycle regardless of policy: its view IS stale.
|
|
132
|
+
* - `silent` — healthy testimony, nothing inside the window → `POD_WATCHER_SILENT` (default keep).
|
|
133
|
+
* - `unknown` — no testimony (older bundle / unreachable) → `POD_WATCHER_UNVERIFIED` (default recycle).
|
|
134
|
+
*/
|
|
135
|
+
export declare function podWatcherVerdict(testimony: PodWatcherTestimony | undefined, changedAt: number, windowMs: number): PodWatcherVerdict;
|
|
91
136
|
/** Config for the lens-child LIVENESS decision ({@link lensChildWedged}). */
|
|
92
137
|
export interface LensChildProbeConfig {
|
|
93
138
|
/** Spawn grace: no verdict until the child has had this long to BIND its port (a cold vite boot). */
|
|
@@ -171,5 +216,22 @@ export interface ServiceLayer {
|
|
|
171
216
|
markUp(svc: ServiceCtl, how: 'adopted' | 'observed'): boolean;
|
|
172
217
|
/** Start the idle sweeper; returns the interval (already `.unref()`'d). */
|
|
173
218
|
startSweeper(): NodeJS.Timeout;
|
|
219
|
+
/** Rolling recycle: standby generation first, retire the old once the new answers (issues/dev-gateway/09). */
|
|
220
|
+
rollPod(svc: ServiceCtl, reason: string): Promise<boolean>;
|
|
221
|
+
}
|
|
222
|
+
/** Injectable seams for the process-level paths (unit tests stub them; real spawns are workspace-dependent). */
|
|
223
|
+
export interface ServiceLayerDeps {
|
|
224
|
+
/** The process launcher. Default `node:child_process` spawn. */
|
|
225
|
+
spawnProcess?: typeof spawn;
|
|
226
|
+
/** The pod liveness probe. Default {@link probePodAlive}. */
|
|
227
|
+
probeAlive?: typeof probePodAlive;
|
|
228
|
+
/** Kill a spawned tree (the process GROUP, falling back to the pid). */
|
|
229
|
+
killTree?: (proc: ChildProcess, signal?: NodeJS.Signals) => void;
|
|
230
|
+
/** How long a standby pod generation may take to answer alive. Default {@link POD_ROLL_READY_MS}. */
|
|
231
|
+
rollReadyMs?: number;
|
|
232
|
+
/** Poll interval while waiting for the standby. Default 750ms. */
|
|
233
|
+
rollPollMs?: number;
|
|
234
|
+
/** Skip the nx foreign-runner reap before a spawn (tests). */
|
|
235
|
+
reapForeign?: boolean;
|
|
174
236
|
}
|
|
175
|
-
export declare function createServiceLayer(rt: GatewayRuntime, obs: Observability): ServiceLayer;
|
|
237
|
+
export declare function createServiceLayer(rt: GatewayRuntime, obs: Observability, deps?: ServiceLayerDeps): ServiceLayer;
|
|
@@ -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(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
|
+
"use strict";var ae=Object.defineProperty;var v=(t,n)=>ae(t,"name",{value:n,configurable:!0});var ie=Object.defineProperty,i=v((t,n)=>ie(t,"name",{value:n,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isTestSourcePath=isTestSourcePath,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),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"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),types_1=require("./types");function eastWestEnv(t,n){const a={},l=t.serviceKeys[(0,service_keys_1.serviceKeyId)(n.wsKey,n.project)];if(l&&(a.LENSMCP_SERVICE_KEY=l),t.https){const u=require("../../../basic-ssl").caCertPath();fs.existsSync(u)&&(a.NODE_EXTRA_CA_CERTS=u)}return a}v(eastWestEnv,"eastWestEnv"),i(eastWestEnv,"eastWestEnv");const sleep=i(t=>new Promise(n=>setTimeout(n,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 n=2166136261;for(let a=0;a<t.length;a+=1)n^=t.charCodeAt(a),n=Math.imul(n,16777619);return n>>>0}v(fnv1a,"fnv1a"),i(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:n}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(n.split("\0").filter(Boolean))}catch{return new Set}}v(gitIgnoredPaths,"gitIgnoredPaths"),i(gitIgnoredPaths,"gitIgnoredPaths");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const n=t.split("/"),a=n[n.length-1]??"";if(TEST_FILE_RE.test(a))return!0;for(let l=0;l<n.length-1;l+=1)if(TEST_DIR_SEGMENTS.has(n[l]))return!0;return!1}v(isTestSourcePath,"isTestSourcePath"),i(isTestSourcePath,"isTestSourcePath");async function sourceSetSnapshot(t,n,a,l){const u=l?.ignoreTests??!0;let S=0,y=0,E=!1;const w=new Set,T=await gitIgnoredPaths(t),h=i(M=>path.relative(t,M).split(path.sep).join("/"),"rel"),k=i(async(M,K=!0)=>{if(S>=SCAN_FILE_CAP){E=!0;return}let R;try{R=await fs.promises.readdir(M,{withFileTypes:!0})}catch{return}for(const O of R){if(S>=SCAN_FILE_CAP){E=!0;return}const C=O.name,F=path.join(M,C);if(O.isDirectory()){if(!K||C.startsWith(".")||SRC_EXCLUDE.has(C)||a?.has(C)||C.endsWith("-devserver")||T.has(`${h(F)}/`))continue;await k(F)}else if(O.isFile()){const b=C.lastIndexOf(".");if(b<=0||!SRC_EXT.has(C.slice(b)))continue;const P=h(F);if(T.has(P)||u&&isTestSourcePath(P)||w.has(P))continue;w.add(P),S+=1,y^=fnv1a(P)}}},"walk");if(n&&n.length>0){await k(t,!1);for(const M of n)await k(M)}else await k(t);return{signature:`${S}:${(y>>>0).toString(36)}`,paths:w,truncated:E}}v(sourceSetSnapshot,"sourceSetSnapshot"),i(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,n,a,l){return(await sourceSetSnapshot(t,n,a,l)).signature}v(sourceSetSignature,"sourceSetSignature"),i(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,n,a,l){if(t.paths.size===n.paths.size){let E=!0;for(const w of t.paths)if(!n.paths.has(w)){E=!1;break}if(E)return null}const u=[],S=[];let y=t.truncated||n.truncated;for(const E of n.paths)t.paths.has(E)||(u.length+S.length<SOURCE_DIFF_PATH_CAP?u.push(E):y=!0);for(const E of t.paths)n.paths.has(E)||(u.length+S.length<SOURCE_DIFF_PATH_CAP?S.push(E):y=!0);return{changedAt:a,added:u,removed:S,truncated:y,...l!==void 0?{windowMs:l}:{}}}v(diffSourceSets,"diffSourceSets"),i(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,n,a,l){const u=n&&n>0?t-n:a;return Math.max(u,a)+l}v(sourceChangeWindowMs,"sourceChangeWindowMs"),i(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,n,a,l,u){const S=i(w=>{const T=path.relative(t,w).split(path.sep).filter(Boolean);return T.length===0||T[0].startsWith("..")?"":T[0]},"bucketOf")(n);if(!S)return[];const y=new Set(a.filter(Boolean)),E=new Set([S]);for(const w of l)w.startsWith(".")||SRC_EXCLUDE.has(w)||u?.has(w)||w.endsWith("-devserver")||y.has(w)||E.add(w);return[...E].sort().map(w=>path.join(t,w))}v(sourceScopeDirs,"sourceScopeDirs"),i(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,n,a,l){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||n-t.lastUsed<l.settleMs||t.lastStaleRecycleAt!=null&&n-t.lastStaleRecycleAt<l.cooldownMs?null:a>0&&a-t.spawnAt>l.graceMs?"stale-source-set":l.maxAgeMs>0&&n-t.spawnAt>l.maxAgeMs?"max-age":null}v(podRecycleReason,"podRecycleReason"),i(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,n,a,l,u){return!(l-n<u.cooldownMs||a<=0||a-t<=u.graceMs||l-a<u.settleMs)}v(lensFrontendStale,"lensFrontendStale"),i(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,n,a,l=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&l.length>0){const u=n-a,S=new Set(t.recentEvents.filter(y=>y.at>=u).map(y=>path.resolve(y.path)));return l.every(y=>S.has(path.resolve(y)))?"heard":"missed"}return t.lastFsSetEventAt>=n-a?"heard":"missed"}v(lensWatcherVerdict,"lensWatcherVerdict"),i(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,n,a){if(!t)return"unknown";if(t.degraded)return"degraded";const l=n-a;return t.lastWatchRunAt>=l||t.lastBuildAt>=l?"heard":"silent"}v(podWatcherVerdict,"podWatcherVerdict"),i(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,n,a,l){const u=t.hasListened?l.graceMs:Math.max(l.graceMs,l.neverListenedGraceMs);return a-t.spawnedAt<u||a-n<l.cooldownMs||t.probeFails<Math.max(2,l.failureThreshold)?null:"wedged-no-listener"}v(lensChildWedged,"lensChildWedged"),i(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,n){return t.signal||t.code===null||t.code===0||t.ranMs>n.fastFailMs?!1:t.portHeld}v(isPortConflictExit,"isPortConflictExit"),i(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,n,a={}){const l=a.spawnProcess??node_child_process_1.spawn,u=a.probeAlive??pod_probe_1.probePodAlive,S=a.killTree??((e,o="SIGTERM")=>{try{process.kill(-e.pid,o)}catch{try{e.kill(o)}catch{}}}),y=a.rollReadyMs??types_1.POD_ROLL_READY_MS,E=a.rollPollMs??750,{emit:w}=n,T=i(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),h=i((e,o,r,s,c)=>w(o,r,s,{wsKey:e.wsKey,...c},void 0,T(e)),"emitSvc"),k=i(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),M=i((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 s=e.proc?.pid;let c=0;for(const d of r.split(`
|
|
2
|
+
`)){const p=d.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!p)continue;const _=Number(p[1]),$=Number(p[2]),m=p[3];if(m.includes(`run ${o}`)&&/\bnx(\.js)?\b/.test(m)&&!(m.includes("/node_modules/nx/")&&!m.includes(e.root))&&!(_===process.pid||_===s)&&!(s!==void 0&&$===s)){try{process.kill(-$,"SIGKILL")}catch{try{process.kill(_,"SIGKILL")}catch{}}c+=1}}return c>0&&(console.log(`[gateway] ${o}: reaped ${c} stale runner tree(s) \u2014 a foreign generation held the nx task lock`),h(e,"warning",`stale-runner reap: ${e.project} (${c} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:c})),c},"reapForeignRunners"),K=i((e,o,r)=>{const s=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,c=Math.max(1,e.decl.minPods??1),d=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),[p,_]=fs.existsSync(d)?[process.execPath,[d,"run",s,`--childCount=${c}`]]:["yarn",["nx","run",s,`--childCount=${c}`]],$=l(p,_,{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),LENSMCP_POD_GENERATION:String(o),...eastWestEnv(t,e)},stdio:["ignore","pipe","pipe"]});r==="active"&&(e.lastErrors=[],e.buildFailed=!1);const m=i(g=>{const j=g.toString();for(const D of j.split(`
|
|
3
|
+
`)){if(/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(D)){const f=D.replace(/\x1b\[[0-9;]*m/g,"").trim();e.lastErrors.push(f),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const x=Date.now();(f!==e.lastErrorEmitSig||x-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=f,e.lastErrorEmitAt=x,h(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:f}))}/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(D)&&(e.buildFailed=!1),!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(D)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${s}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),h(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||(L(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),M(e,s),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&R(e)},1500).unref?.())},250).unref?.())}},"sniff"),A=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0;return $.stdout?.on("data",g=>{if(t.stopped())return;m(g);const j=`[${e.project}] ${g}`;process.stdout.write(j),A?.(j)}),$.stderr?.on("data",g=>{if(t.stopped())return;m(g);const j=`[${e.project}] ${g}`;process.stderr.write(j),A?.(j)}),$.on("exit",g=>{if(e.proc!==$||(e.proc=void 0,e.state="down",t.stopped())||(console.log(`[gateway] ${s} exited (${g})`),h(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 j=Date.now();if(e.restarts=(e.restarts??[]).filter(f=>j-f<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(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(j);const D=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})`),h(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),R(e))},D)}),$},"launchServiceProcess"),R=i(e=>{if(t.stopped())return;const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,r=Math.max(1,e.decl.minPods??1);a.reapForeign!==!1&&M(e,o),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${o} (pods=${r})\u2026`),h(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(),e.generation=(e.generation??0)+1,e.proc=K(e,e.generation,"active")},"spawnService"),O=i(async e=>{const o=e.pool.socks[0];if(!o)return;const r=await(0,pod_probe_1.probePodAlive)(o,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);r&&(e.activeWork=r.activeWork,r.watcher&&(e.watcher=r.watcher))},"refreshActiveWork"),C=i(async e=>{const o=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!o)return[];const r=(0,node_crypto_1.createHash)("sha256").update(o).digest("hex").slice(0,8),s=[];for(const c of e.pool.socks){const d=await(0,pod_probe_1.probePodAlive)(c,void 0,o);d&&(!d.keyFingerprint||d.keyFingerprint!==r)&&s.push(c)}return s},"staleAdoptedSocks"),F=i((e,o)=>{for(const r of o){try{const s=Number(fs.readFileSync(`${r}.owner`,"utf8").trim());if(Number.isFinite(s)&&s>1)try{process.kill(s,"SIGTERM")}catch{}}catch{}try{fs.unlinkSync(r)}catch{}try{fs.unlinkSync(`${r}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(r=>!o.includes(r)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),b=i((e,o)=>{if(e.state==="up")return!1;e.state="up";const r=e.pool.socks.length;return h(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"),P=i(async e=>{if(k(e.pool)>0){const s=await C(e);if(s.length>0&&(console.error(`[gateway] ${e.project}: ${s.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),h(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:s.length}),e.proc&&L(e,"stale adoption (foreign east-west key)"),F(e,s)),e.pool.socks.length>0)return b(e,"adopted"),!0}if(t.stopped())return!1;e.state!=="starting"&&!e.proc&&R(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(k(e.pool)>0){e.state="up",e.buildFailed=!1;const s=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${s?`, cold start ${s}ms`:""})`),h(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${s??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:s}),!0}if(e.state==="down")return!1}return!1},"ensureUp"),J=i(e=>{const o=e.decl.maxPods??e.decl.minPods??1,r=e.pool.socks.length,s=e.decl.adminUrl??(e.decl.port?`http://localhost:${e.decl.port}`:void 0);if(!s||r===0||r>=o||e.inflight<=r||Date.now()-e.lastScaleAt<5e3)return;e.lastScaleAt=Date.now();const c=(s.startsWith("https")?require("node:https"):http).request(`${s}/webpack/scale`,{method:"POST",rejectUnauthorized:!1},d=>d.resume());c.on("error",()=>{}),c.end(JSON.stringify({pods:r+1})),console.log(`[gateway] autoscale ${e.project}: ${r} \u2192 ${r+1} pods (inflight ${e.inflight})`),h(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"),L=i((e,o,r)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),r?.announce!==!1&&h(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const s=e.proc;try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}S(s,"SIGTERM"),e.proc=void 0,e.state="down",setTimeout(()=>{G(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),G=i(e=>{try{for(const o of fs.readdirSync(e.pool.dir)){if(!o.endsWith(".sock"))continue;const r=path.join(e.pool.dir,o);try{const s=Number(fs.readFileSync(`${r}.owner`,"utf8").trim());if(Number.isFinite(s)&&s>0)try{process.kill(s,0);continue}catch{}}catch{}try{fs.unlinkSync(r)}catch{}try{fs.unlinkSync(`${r}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks"),Y=i((e,o)=>{if(e.rolling)return e.rolling;const r=e.proc;if(!r||t.stopped())return Promise.resolve(!1);const s=e.generation??0,c=s+1,d=Date.now();e.lastStaleRecycleAt=d,console.log(`[gateway] pod handoff start: ${e.project} g${s}\u2192g${c} reason=${o} \u2014 standby spawning, old generation keeps serving.`),h(e,"info",`pod handoff starting: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:o,generation:c,fromGeneration:s});const p=K(e,c,"standby"),_=(async()=>{const $=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],m=d+y;let A;for(;!t.stopped()&&Date.now()<m&&!(p.exitCode!==null||p.signalCode!==null);){k(e.pool);for(const g of e.pool.socks)if((await u(g,void 0,$))?.podGeneration===c){A=g;break}if(A)break;await sleep(E)}if(!A||t.stopped()){const g=t.stopped()?"gateway stopping":p.exitCode!==null||p.signalCode!==null?`standby exited (${p.exitCode??p.signalCode})`:`standby not ready within ${Math.round(y/1e3)}s`;try{p.stdout?.removeAllListeners("data"),p.stderr?.removeAllListeners("data")}catch{}return S(p,"SIGTERM"),console.error(`[gateway] pod handoff failed: ${e.project} g${c}: ${g} \u2014 old generation g${s} kept serving.`),h(e,"warning",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:o,generation:c,error:g,durationMs:Date.now()-d}),setTimeout(()=>G(e),1200).unref?.(),!1}if(e.proc!==r)return S(p,"SIGTERM"),!1;e.proc=p,e.generation=c,e.spawnAt=d,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{r.stdout?.removeAllListeners("data"),r.stderr?.removeAllListeners("data"),r.removeAllListeners("exit")}catch{}return S(r,"SIGTERM"),setTimeout(()=>{G(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${c} answered on ${path.basename(A)} in ${Date.now()-d}ms; retiring g${s}.`),h(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:o,generation:c,fromGeneration:s,durationMs:Date.now()-d}),!0})().finally(()=>{e.rolling=void 0});return e.rolling=_,_},"rollPod"),Q=i(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){h(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}),L(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})`),h(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}),L(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&R(e)},1500).unref?.()},"recycleWedged"),Z={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 q;const X=new Map;let U=!1,B=0,V=0;const ee=i(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(s=>s.project));for(const s of t.routes)s.lens&&o.add(s.lens.project);const r=new Set;for(const s of o){const c=e[s];if(!c)continue;const d=path.relative(t.root,c).split(path.sep).filter(Boolean)[0];d&&!d.startsWith("..")&&r.add(d)}return[...r]},"managedBuckets"),te=i((e,o,r,s)=>{const c=t.projectSourceRoots?.[e];if(c?.length)return c;const d=t.projectRoots?.[e];if(!d)return;const p=sourceScopeDirs(t.root,d,r,o,s);return p.length>0?p:void 0},"scopeFor"),re=i(()=>{if(U||Date.now()-B<types_1.POD_STALE_SCAN_MS)return;U=!0,B=Date.now();const e=B;(async()=>{const o=(0,scope_1.readSourceSetOptions)(t.root),r=o.exclude,s={ignoreTests:o.ignoreTests},c=await sourceSetSnapshot(t.root,void 0,r,s),d=Date.now(),p=sourceChangeWindowMs(d,V,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),_=q?diffSourceSets(q,c,d,p):null;if(_&&(t.sourceSetChangedAt=d,t.sourceSetChange=_,w("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:_.added,removed:_.removed,truncated:_.truncated})),q=c,!t.projectRoots){V=e;return}let $;try{$=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(f=>f.isDirectory()).map(f=>f.name)}catch{return}const m=ee(),A=new Set(t.services.map(f=>f.project));for(const f of t.routes)f.lens&&A.add(f.lens.project);const g=new Map;for(const f of A){const x=te(f,$,m,r);if(!x)continue;const I=x.join("|"),N=g.get(I)??{dirs:x,projects:[]};N.projects.push(f),g.set(I,N)}const j=Date.now(),D=sourceChangeWindowMs(j,V,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);V=e;for(const[f,{dirs:x,projects:I}]of g){const N=await sourceSetSnapshot(t.root,x,r,s),H=X.get(f);if(X.set(f,N),!H)continue;const W=diffSourceSets(H,N,j,D);if(W){t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const z of I)t.sourceSetChangedFor.set(z,j),t.sourceSetChangesFor.set(z,W);w("info",`source file set changed in ${I.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:I,added:W.added,removed:W.removed,truncated:W.truncated})}}})().catch(()=>{}).finally(()=>{U=!1})},"maybeScanSourceSet"),oe=i((e,o)=>{const r=(t.sourceSetChangesFor?.get(e.project)??t.sourceSetChange)?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,s=podWatcherVerdict(e.watcher,o,r),c=i(d=>(e.staleKeptAt!==o&&(e.staleKeptAt=o,console.log(`[gateway] stale source set: pod ${e.project} ${d} \u2014 no recycle.`)),!1),"keep");return s==="heard"?(e.staleVerifiedAt=o,h(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:s}),c(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`)):s==="degraded"?(console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0):s==="silent"?types_1.POD_WATCHER_SILENT==="keep"?c("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):!0:types_1.POD_WATCHER_UNVERIFIED==="keep"?c("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):!0},"podStaleRecycleAllowed"),se=i((e,o)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc){Y(e,o);return}console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),h(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const r=!!e.decl.eager;L(e,`${o} \u2014 recycling stale pod`,{announce:!1}),r&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&R(e)},1500).unref?.()},"recyclePod"),ne=i(()=>{const e=setInterval(()=>{ce().catch(o=>console.error("[gateway] sweep error:",o))},t.sweepMs);return e.unref(),e},"startSweeper"),ce=i(async()=>{{if(t.stopped())return;types_1.POD_RECYCLE_ENABLED&&re();for(const e of t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const o=e.decl.idleKillSec;if(o&&e.inflight===0&&Date.now()-e.lastUsed>o*1e3){L(e,`idle ${o}s`),e.wedgedSince=void 0;continue}const r=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,s=k(e.pool)>0;if(s||r){if(e.wedgedSince=void 0,s&&b(e,"observed"),s&&!r&&types_1.POD_RECYCLE_ENABLED){const c=t.sourceSetChangedFor?.get(e.project)??t.sourceSetChangedAt??0;await O(e);const d=(e.staleVerifiedAt??0)>=c&&c>0,p=podRecycleReason(e,Date.now(),d?0:c,Z);if(p==="stale-source-set"&&!oe(e,c))continue;p&&se(e,p)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,Q(e))}}},"sweepOnce");return{scanNow:k,spawnService:R,ensureUp:P,scaleUp:J,killSpawned:L,markUp:b,startSweeper:ne,rollPod:Y}}v(createServiceLayer,"createServiceLayer"),i(createServiceLayer,"createServiceLayer");
|