@lensmcp/cluster 1.23.6 → 1.23.8

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.
@@ -0,0 +1,31 @@
1
+ /** `LENSMCP_SWAP_RELEASE_ERROR_FRAMES=0` turns it off. */
2
+ export declare function errorFrameReleaseEnabled(env?: NodeJS.ProcessEnv): boolean;
3
+ export interface ErrorFrameRelease {
4
+ /** Objects and functions looked at. */
5
+ visited: number;
6
+ /** Error objects whose stack was read. */
7
+ errors: number;
8
+ }
9
+ export interface ErrorFrameReleaseOptions {
10
+ /** How far from a root to look. A prototype used as a base class sits three steps from the exports. */
11
+ maxDepth?: number;
12
+ /** Objects looked at per root, so one enormous module cannot spend the whole budget. */
13
+ maxPerRoot?: number;
14
+ /** Objects looked at in all. */
15
+ maxNodes?: number;
16
+ }
17
+ /**
18
+ * Read the stack of every native Error reachable from `roots` through plain properties and prototypes.
19
+ * `seen` carries what earlier calls already looked at.
20
+ */
21
+ export declare function readStacksReachableFrom(roots: Iterable<unknown>, opts?: ErrorFrameReleaseOptions, seen?: WeakSet<object>): ErrorFrameRelease;
22
+ /**
23
+ * Tracks what has been loaded so far, so that each call looks only at what is NEW: the modules added to
24
+ * the require cache and the properties added to `globalThis` since the previous call.
25
+ */
26
+ export declare function createErrorFrameReleaser(cache: Record<string, {
27
+ exports?: unknown;
28
+ } | undefined>, opts?: {
29
+ skip?: (moduleId: string) => boolean;
30
+ global?: object;
31
+ } & ErrorFrameReleaseOptions): () => ErrorFrameRelease;
@@ -0,0 +1 @@
1
+ "use strict";var A=Object.defineProperty;var l=(t,r)=>A(t,"name",{value:r,configurable:!0});var k=Object.defineProperty,y=l((t,r)=>k(t,"name",{value:r,configurable:!0}),"y");Object.defineProperty(exports,"__esModule",{value:!0}),exports.errorFrameReleaseEnabled=errorFrameReleaseEnabled,exports.readStacksReachableFrom=readStacksReachableFrom,exports.createErrorFrameReleaser=createErrorFrameReleaser;const node_util_1=require("node:util");function errorFrameReleaseEnabled(t=process.env){const r=(t.LENSMCP_SWAP_RELEASE_ERROR_FRAMES??"").trim().toLowerCase();return!(r==="0"||r==="false"||r==="no"||r==="off")}l(errorFrameReleaseEnabled,"errorFrameReleaseEnabled"),y(errorFrameReleaseEnabled,"errorFrameReleaseEnabled");const BUILTINS=new Set([Object.prototype,Function.prototype,Array.prototype,Error.prototype,TypeError.prototype,RangeError.prototype,SyntaxError.prototype,ReferenceError.prototype,EvalError.prototype,URIError.prototype,AggregateError.prototype,Promise.prototype,Map.prototype,Set.prototype,WeakMap.prototype,WeakSet.prototype,RegExp.prototype,Date.prototype,Symbol.prototype,String.prototype,Number.prototype,Boolean.prototype,BigInt.prototype,Object,Function,Array,Error,Promise,Map,Set,Symbol,globalThis,process]),MAX_KEYS_PER_OBJECT=512,MAX_ARRAY_ITEMS=64,walkable=y(t=>typeof t=="object"&&t!==null||typeof t=="function","walkable");function readStacksReachableFrom(t,r={},p=new WeakSet){const f=r.maxDepth??5,u=r.maxPerRoot??4e3,E=r.maxNodes??4e5;let a=0,o=0;for(const n of t){if(a>=E)break;if(!walkable(n))continue;let S=0;const c=[[n,0]];for(let b=0;b<c.length&&S<u&&a<E;b++){const[e,R]=c[b];if(!(p.has(e)||BUILTINS.has(e))){p.add(e),a+=1,S+=1;try{if(node_util_1.types.isNativeError(e)){e.stack,o+=1;continue}if(node_util_1.types.isProxy(e)||node_util_1.types.isModuleNamespaceObject(e)||node_util_1.types.isPromise(e)||ArrayBuffer.isView(e)||node_util_1.types.isAnyArrayBuffer(e)||node_util_1.types.isMap(e)||node_util_1.types.isSet(e)||node_util_1.types.isWeakMap(e)||node_util_1.types.isWeakSet(e)||R>=f)continue;const m=Object.getPrototypeOf(e);if(walkable(m)&&c.push([m,R+1]),Array.isArray(e)){const i=Math.min(e.length,MAX_ARRAY_ITEMS);for(let s=0;s<i;s++){const d=e[s];walkable(d)&&c.push([d,R+1])}continue}const h=Reflect.ownKeys(e),g=Math.min(h.length,MAX_KEYS_PER_OBJECT);for(let i=0;i<g;i++){const s=Reflect.getOwnPropertyDescriptor(e,h[i]);s&&"value"in s&&walkable(s.value)&&c.push([s.value,R+1])}}catch{}}}}return{visited:a,errors:o}}l(readStacksReachableFrom,"readStacksReachableFrom"),y(readStacksReachableFrom,"readStacksReachableFrom");function createErrorFrameReleaser(t,r={}){const p=r.global??globalThis,f=new Set,u=new Set(Reflect.ownKeys(p)),E=new WeakSet;return()=>{const a=[];for(const o of Object.keys(t))if(!f.has(o)&&(f.add(o),!r.skip?.(o)))try{a.push(t[o]?.exports)}catch{}for(const o of Reflect.ownKeys(p)){if(u.has(o))continue;u.add(o);const n=Reflect.getOwnPropertyDescriptor(p,o);n&&"value"in n&&a.push(n.value)}return readStacksReachableFrom(a,r,E)}}l(createErrorFrameReleaser,"createErrorFrameReleaser"),y(createErrorFrameReleaser,"createErrorFrameReleaser");
@@ -1,4 +1,7 @@
1
1
  import type { Route, ServiceCtl, SockPool } from './types';
2
+ /** A pod that is about to stop accepting connections leaves `<socket>.retiring` beside its socket first
3
+ * (main.devserver.ts `retire`, MUST match its `sockRetiringPath`). */
4
+ export declare const RETIRING_SUFFIX = ".retiring";
2
5
  /** Round-robin over the pool's sockets, rescanning the dir when the cache ages out. */
3
6
  export declare function pickSock(pool: SockPool, scanTtlMs?: number): string | undefined;
4
7
  /** Extract the port from an `upstream` URL like `http://localhost:5173`. */
@@ -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+(?:-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,k,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},k={project:i,decl:t,root:e,wsKey:s,pool:v,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},h.push(k);else if(t.lens){const f=t.port??lensPortFromUpstream(t.upstream)??5173;m=`http://localhost:${f}`,y={project:i,...s?{workspaceKey:s}:{},workspaceRoot:e,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 j={project:i,prependPrefix:t.prependPrefix,target:m,pool:v,svc:k,prefix:t.path,auth:t.auth,lens:y};t.host&&(c.push({...j,host:t.host}),o.push(t.host),v&&t.internalHost!==!1&&c.push({...j,host:typeof t.internalHost=="string"?t.internalHost:`internal.${t.host}`,internal:!0})),t.default&&(n={...j,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");
1
+ "use strict";var y=Object.defineProperty;var d=(e,s)=>y(e,"name",{value:s,configurable:!0});var g=Object.defineProperty,p=d((e,s)=>g(e,"name",{value:s,configurable:!0}),"p");Object.defineProperty(exports,"__esModule",{value:!0}),exports.RETIRING_SUFFIX=void 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");exports.RETIRING_SUFFIX=".retiring";function pickSock(e,s=1500){const u=Date.now();if(u-e.scannedAt>s){try{const n=fs.readdirSync(e.dir),a=n.filter(o=>/^child-\d+(?:-g\d+)?\.sock$/.test(o)),l=a.filter(o=>!n.includes(o+exports.RETIRING_SUFFIX));e.socks=(l.length>0?l:a).map(o=>path.join(e.dir,o))}catch{e.socks=[]}e.scannedAt=u}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}d(pickSock,"pickSock"),p(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const s=/:(\d+)/.exec(e);return s?Number(s[1]):void 0}d(lensPortFromUpstream,"lensPortFromUpstream"),p(lensPortFromUpstream,"lensPortFromUpstream");function discoverRoutes(e,s,u=os.tmpdir(),n="",a={}){const l=p(c=>n?path.join(u,n,`${c}-devserver`):path.join(u,`${c}-devserver`),"sockDirFor"),o=[],h=[],r=[];let i;for(const[c,S]of Object.entries(s)){let t;try{const f=JSON.parse(fs.readFileSync(path.join(e,S.root,"project.json"),"utf8"));t=f.cluster??f.davnx}catch{continue}if(!t)continue;let v,k,m,j;if(t.service)t.lens&&console.warn(`[gateway] ${c}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),v={dir:l(t.service),socks:[],idx:-1,scannedAt:0},k={project:c,decl:t,root:e,wsKey:n,pool:v,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},h.push(k);else if(t.lens){const f=t.port??lensPortFromUpstream(t.upstream)??5173;m=`http://localhost:${f}`,j={project:c,...n?{workspaceKey:n}:{},workspaceRoot:e,projectRoot:path.join(e,S.root),port:f,...a[c]?.length?{sourceRoots:a[c]}:{}}}else if(m=t.upstream??(t.port?`http://localhost:${t.port}`:void 0),!m){console.warn(`[gateway] ${c}: no service/upstream/port \u2014 skipped.`);continue}const x={project:c,prependPrefix:t.prependPrefix,target:m,pool:v,svc:k,prefix:t.path,auth:t.auth,lens:j};t.host&&(o.push({...x,host:t.host}),r.push(t.host),v&&t.internalHost!==!1&&o.push({...x,host:typeof t.internalHost=="string"?t.internalHost:`internal.${t.host}`,internal:!0})),t.default&&(i={...x,prefix:void 0})}return i&&(i.apex=(0,scope_1.baseDomainOf)(r),o.push(i)),(0,manifest_1.sortBySpecificity)(o),{routes:o,services:h}}d(discoverRoutes,"discoverRoutes"),p(discoverRoutes,"discoverRoutes");function projectSourceRoots(e,s,u={}){const n={};for(const a of Object.keys(s)){const l=new Set,o=p(r=>{if(!l.has(r)){l.add(r);for(const i of u[r]??[])o(i)}},"visit");o(a);const h=[...l].map(r=>s[r]?.root).filter(r=>typeof r=="string").map(r=>path.resolve(e,r)).filter(r=>{const i=path.relative(e,r);return i===""||i!==".."&&!i.startsWith(`..${path.sep}`)&&!path.isAbsolute(i)});n[a]=[...new Set(h)]}return n}d(projectSourceRoots,"projectSourceRoots"),p(projectSourceRoots,"projectSourceRoots");
@@ -35,3 +35,28 @@ export declare function probePodAlive(socketPath: string, timeoutMs?: number,
35
35
  /** The target service's east-west key. The pod refuses the route without it, so that a
36
36
  * client coming through the gateway cannot read its diagnostics. */
37
37
  serviceKey?: string): Promise<PodAliveness | undefined>;
38
+ /**
39
+ * What one failed pod connection says about the POD.
40
+ *
41
+ * `gone` — nothing listens behind the socket file any more: the request itself was refused
42
+ * (`ECONNREFUSED`, a ghost file) or the file is missing (`ENOENT`), or a fresh connection
43
+ * to it is. Evicting is right: the file is a lie and rescans must stop seeing it.
44
+ * `alive` — the pod dropped THIS connection (`socket hang up`, `ECONNRESET`, `EPIPE`) but still
45
+ * accepts new ones. One connection failed; the pod did not.
46
+ *
47
+ * The proxy used to evict on every error, i.e. UNLINK THE SOCKET FILE OF A RUNNING POD. The pod
48
+ * cannot know its file is gone and never re-binds, so the service sat with "a live parent and 0 live
49
+ * pods" until the wedge sweeper killed the whole tree and cold-started it. The trigger is ordinary:
50
+ * a hot swap blocks the pod's event loop for seconds while it evaluates the bundle; a request the
51
+ * gateway writes onto an idle pooled connection in that window is still unread when the loop comes
52
+ * back, and the pod's keep-alive timer — due first — destroys the connection under it. foodguard,
53
+ * 2026-10-06: 14 of the 15 full service restarts in one gateway log followed such an eviction.
54
+ *
55
+ * Deliberately a CONNECT and not `probePodAlive`: the listen backlog accepts while the event loop is
56
+ * blocked, which is exactly when this is asked, and a pod of any `@lensmcp/cluster` version has one.
57
+ * A pod that accepts and then never answers is the first-byte guard's case, not this one.
58
+ */
59
+ export type PodConnectionVerdict = 'gone' | 'alive';
60
+ export declare function podConnectionVerdict(socketPath: string, err: Error, timeoutMs?: number): Promise<PodConnectionVerdict>;
61
+ /** Does anything still LISTEN on this pod socket? Never throws; no answer in time reads as "no". */
62
+ export declare function podSocketAccepts(socketPath: string, timeoutMs?: number): Promise<boolean>;
@@ -1 +1 @@
1
- "use strict";var h=Object.defineProperty;var c=(r,t)=>h(r,"name",{value:t,configurable:!0});var f=Object.defineProperty,a=c((r,t)=>f(r,"name",{value:t,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.parsePodWatcherTestimony=parsePodWatcherTestimony,exports.probePodAlive=probePodAlive;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),types_1=require("./types");function parsePodWatcherTestimony(r){if(!r||typeof r!="object")return;const t=r;if(t.schemaVersion!==1)return;const o=a(n=>typeof n=="number"&&Number.isFinite(n)?n:0,"num"),s=t.degraded;return{schemaVersion:1,lastWatchRunAt:o(t.lastWatchRunAt),lastBuildAt:o(t.lastBuildAt),buildsSinceSpawn:o(t.buildsSinceSpawn),degraded:typeof s=="string"&&s?s:s===!0?"degraded":!1,recentPaths:Array.isArray(t.recentPaths)?t.recentPaths.filter(n=>typeof n=="string").slice(-32):[]}}c(parsePodWatcherTestimony,"parsePodWatcherTestimony"),a(parsePodWatcherTestimony,"parsePodWatcherTestimony");async function probePodAlive(r,t=types_1.POD_ALIVE_PROBE_TIMEOUT_MS,o){return new Promise(s=>{let n=!1;const d=a(i=>{n||(n=!0,s(i))},"finish");let p;try{p=http.request({socketPath:r,path:types_1.POD_ALIVE_PATH,method:"GET",timeout:t,agent:!1,...o?{headers:{"x-internal-token":o}}:{}},i=>{if(i.statusCode!==200){i.resume(),d(void 0);return}let u="";i.setEncoding("utf8"),i.on("data",e=>{u+=e}),i.on("end",()=>{try{const e=JSON.parse(u);d(e.ok===!0?{inflight:Number(e.inflight??0),activeWork:Number(e.activeWork??0),swapping:e.swapping===!0,...typeof e.pid=="number"?{pid:e.pid}:{},...typeof e.keyFingerprint=="string"?{keyFingerprint:e.keyFingerprint}:{},...(()=>{const l=parsePodWatcherTestimony(e.watcher);return l?{watcher:l}:{}})(),...typeof e.podGeneration=="number"&&Number.isFinite(e.podGeneration)?{podGeneration:e.podGeneration}:{}}:void 0)}catch{d(void 0)}})})}catch{d(void 0);return}p.on("timeout",()=>{p.destroy(),d(void 0)}),p.on("error",()=>d(void 0)),p.end()})}c(probePodAlive,"probePodAlive"),a(probePodAlive,"probePodAlive");
1
+ "use strict";var h=Object.defineProperty;var p=(r,e)=>h(r,"name",{value:e,configurable:!0});var f=Object.defineProperty,u=p((r,e)=>f(r,"name",{value:e,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.parsePodWatcherTestimony=parsePodWatcherTestimony,exports.probePodAlive=probePodAlive,exports.podConnectionVerdict=podConnectionVerdict,exports.podSocketAccepts=podSocketAccepts;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),net=tslib_1.__importStar(require("node:net")),types_1=require("./types");function parsePodWatcherTestimony(r){if(!r||typeof r!="object")return;const e=r;if(e.schemaVersion!==1)return;const c=u(n=>typeof n=="number"&&Number.isFinite(n)?n:0,"num"),o=e.degraded;return{schemaVersion:1,lastWatchRunAt:c(e.lastWatchRunAt),lastBuildAt:c(e.lastBuildAt),buildsSinceSpawn:c(e.buildsSinceSpawn),degraded:typeof o=="string"&&o?o:o===!0?"degraded":!1,recentPaths:Array.isArray(e.recentPaths)?e.recentPaths.filter(n=>typeof n=="string").slice(-32):[]}}p(parsePodWatcherTestimony,"parsePodWatcherTestimony"),u(parsePodWatcherTestimony,"parsePodWatcherTestimony");async function probePodAlive(r,e=types_1.POD_ALIVE_PROBE_TIMEOUT_MS,c){return new Promise(o=>{let n=!1;const i=u(s=>{n||(n=!0,o(s))},"finish");let a;try{a=http.request({socketPath:r,path:types_1.POD_ALIVE_PATH,method:"GET",timeout:e,agent:!1,...c?{headers:{"x-internal-token":c}}:{}},s=>{if(s.statusCode!==200){s.resume(),i(void 0);return}let d="";s.setEncoding("utf8"),s.on("data",t=>{d+=t}),s.on("end",()=>{try{const t=JSON.parse(d);i(t.ok===!0?{inflight:Number(t.inflight??0),activeWork:Number(t.activeWork??0),swapping:t.swapping===!0,...typeof t.pid=="number"?{pid:t.pid}:{},...typeof t.keyFingerprint=="string"?{keyFingerprint:t.keyFingerprint}:{},...(()=>{const l=parsePodWatcherTestimony(t.watcher);return l?{watcher:l}:{}})(),...typeof t.podGeneration=="number"&&Number.isFinite(t.podGeneration)?{podGeneration:t.podGeneration}:{}}:void 0)}catch{i(void 0)}})})}catch{i(void 0);return}a.on("timeout",()=>{a.destroy(),i(void 0)}),a.on("error",()=>i(void 0)),a.end()})}p(probePodAlive,"probePodAlive"),u(probePodAlive,"probePodAlive");async function podConnectionVerdict(r,e,c=types_1.POD_ALIVE_PROBE_TIMEOUT_MS){const o=e.code??"";return o==="ECONNREFUSED"||o==="ENOENT"?"gone":await podSocketAccepts(r,c)?"alive":"gone"}p(podConnectionVerdict,"podConnectionVerdict"),u(podConnectionVerdict,"podConnectionVerdict");function podSocketAccepts(r,e=types_1.POD_ALIVE_PROBE_TIMEOUT_MS){return new Promise(c=>{let o=!1,n;const i=u(s=>{o||(o=!0,clearTimeout(a),n?.destroy(),c(s))},"finish"),a=setTimeout(()=>i(!1),e);a.unref?.();try{n=net.connect({path:r}),n.once("connect",()=>i(!0)),n.once("error",()=>i(!1))}catch{i(!1)}})}p(podSocketAccepts,"podSocketAccepts"),u(podSocketAccepts,"podSocketAccepts");
@@ -1 +1 @@
1
- "use strict";var L=Object.defineProperty;var T=(n,i)=>L(n,"name",{value:i,configurable:!0});var H=Object.defineProperty,r=T((n,i)=>H(n,"name",{value:i,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),body_stall_1=require("../body-stall"),types_1=require("./types"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys");function isConnError(n){const i=n.code??"";return i==="ECONNREFUSED"||i==="ECONNRESET"||i==="ETIMEDOUT"||i==="EPIPE"||i==="EHOSTUNREACH"||i==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(n.message)}T(isConnError,"isConnError"),r(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(n){if(!IDEMPOTENT_METHODS.has((n.method??"GET").toUpperCase())||n.headers["transfer-encoding"]!==void 0)return!1;const i=n.headers["content-length"];return!(i!==void 0&&String(i).trim()!=="0")}T(isReplayableRequest,"isReplayableRequest"),r(isReplayableRequest,"isReplayableRequest");function edgeGone(n,i){return i.headersSent||i.writableEnded||i.destroyed===!0||n.aborted===!0}T(edgeGone,"edgeGone"),r(edgeGone,"edgeGone");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),UPSTREAM_CONNECTION_HEADERS=["connection","keep-alive","proxy-connection"],httpProxy=require("http-proxy");function declaredFirstByteMs(n){const i=Number(n);return Number.isFinite(i)&&i>0?i:types_1.POD_RESPONSE_TIMEOUT_MS}T(declaredFirstByteMs,"declaredFirstByteMs"),r(declaredFirstByteMs,"declaredFirstByteMs");function armFirstByteGuard(n){let i,m=0,$=!1,O=!1;const P=r(b=>{i=setTimeout(x,b),i.unref?.()},"arm"),x=r(()=>{if(!($||!n.pending())){if(m+=n.firstByteMs,n.ceilingMs<=0||m>=n.ceilingMs){n.onWedged(`no response within ${m}ms (wedged socket) \u2014 evicted + respawning`);return}(0,pod_probe_1.probePodAlive)(n.socketPath,void 0,n.serviceKey).then(b=>{if(!($||!n.pending())){if(!b){n.onWedged(`no response within ${m}ms and no answer on ${types_1.POD_ALIVE_PATH} (wedged socket) \u2014 evicted + respawning`);return}O||(O=!0,n.onSlow(m,b)),P(Math.max(n.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))}})}},"onDeadline");return P(n.firstByteMs),{clear:r(()=>{$=!0,i&&clearTimeout(i)},"clear")}}T(armFirstByteGuard,"armFirstByteGuard"),r(armFirstByteGuard,"armFirstByteGuard");function createProxy(n,i,m){const{recordEdge:$,finishTrace:O,traceStep:P,emit:x}=i,b=r(async e=>{if(!e.lens||!n.lensSupervisor)return"unsupported";let t;const o=new Promise(p=>{t=setTimeout(()=>p("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),t.unref?.()}),u=n.lensSupervisor.recover(e.lens,"proxy-connection").then(p=>p?"ready":"failed").catch(()=>"failed"),y=await Promise.race([u,o]);t&&clearTimeout(t);const _=n.lensSupervisor.status(e.lens);return y==="failed"&&(!_||_.state==="ready")?"unsupported":y},"waitForManagedRecovery"),j=r((e,t,o,u,y,_)=>{if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}const p=e.lens?n.lensSupervisor?.status(e.lens):void 0,h=_==="timeout"||p?.state==="starting"||p?.state==="replacing",f=(0,gateway_errors_1.ensureRequestId)(t),c=e.lens?{project:e.project,reason:p?.reason??"connection-failure",generation:p?.generation??0,...h?{retryAfterMs:2e3}:{}}:void 0,s=h?503:502,d=h?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${f} project=${e.project} target=${String(u)} state=${p?.state??"unmanaged"} generation=${p?.generation??0}: ${y.message}`),x("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:f,project:e.project,target:String(u),state:p?.state??"unmanaged",generation:p?.generation,error:y.message}),(0,edge_1.sendError)(o,t,s,d,h?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,c,h?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),k=(0,body_stall_1.createBodyStallGuard)(n.bodyStallMs??types_1.BODY_STALL_MS),D={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},R=new http.Agent(D),U=new(require("node:https")).Agent(D),A=r(e=>typeof e=="string"&&e.startsWith("https:")?U:R,"agentForTarget"),M=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:R}),C=r((e,t,o)=>{const u=r(()=>{o()||e.destroyed||e.destroy()},"onEdgeClose");t.once("close",u),e.once("close",()=>t.removeListener("close",u))},"propagateEdgeCancel");M.on("proxyReq",(e,t,o)=>{for(const u of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(u);e.setHeader("connection","keep-alive"),C(e,o,()=>o.writableEnded)}),M.on("proxyRes",(e,t,o)=>{const u=t.__lensmcpPod;u&&(e.headers["x-lensmcp-pod"]=u),delete e.headers["x-powered-by"],e.headers.server="LensMCP",o&&C(e,o,()=>o.writableEnded),o&&k.armResponse(o)});const N=r((e,t,o,u)=>{(0,edge_1.stampForwardingHeaders)(n,t),k.arm(t);const y={},_=t.method==="GET"||t.method==="HEAD"||t.method==="OPTIONS";for(const[a,l]of Object.entries(t.headers))l===void 0||a.startsWith(":")||HOP_BY_HOP.has(a)||_&&(a==="content-length"||a==="transfer-encoding")||(y[a]=l);let p=http,h;if(typeof e=="object")h={socketPath:e.socketPath,path:t.url,method:t.method,headers:y,agent:R};else{const a=new URL(e),l=a.protocol==="https:";p=l?require("node:https"):http,y.host=a.host,h={protocol:a.protocol,hostname:a.hostname,port:a.port||(l?443:80),path:t.url,method:t.method,headers:y,agent:A(e)}}const f=o;f.__lensmcpErrContained||(f.__lensmcpErrContained=!0,o.on("error",()=>{}));let c;const s=r(()=>{o.writableEnded||E.destroy()},"onReqGone"),d=r(()=>{c&&!c.destroyed&&c.destroy()},"onEdgeClose"),g=r(()=>{t.removeListener("error",s),t.removeListener("aborted",s),t.removeListener("close",s),o.removeListener("close",d)},"detach"),E=p.request(h,a=>{if(c=a,u.onResponse(),o.headersSent||o.writableEnded||o.destroyed){a.destroy();return}const l={};for(const[v,I]of Object.entries(a.headers))I===void 0||HOP_BY_HOP.has(v)||(l[v]=I);delete l["x-powered-by"],l.server="LensMCP";const w=t.__lensmcpPod;w&&(l["x-lensmcp-pod"]=w);try{o.writeHead(a.statusCode??502,l)}catch(v){console.error("[gateway] h2 relay: bad response header, resetting stream:",v.message),a.destroy(),o.destroyed||o.destroy(v);return}a.pipe(o),k.armResponse(o);const S=r(()=>{!o.writableEnded&&!o.destroyed&&o.destroy()},"abortEdge");a.once("error",S),a.once("close",S),o.on("close",d)});E.once("error",a=>u.onError(a)),E.once("close",g),t.on("error",s),t.on("aborted",s),t.on("close",s),_?E.end():t.pipe(E)},"pipeUpstream"),q=r(async(e,t,o)=>{const u=t.__lensmcpTrace;if(e.pool){const c=e.svc;c&&(c.lastUsed=Date.now(),c.inflight+=1,o.on("close",()=>{c.inflight-=1,c.lastUsed=Date.now()}));let s=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!s&&c&&(P(u,"cold-start",{project:e.project}),await m.ensureUp(c)&&(s=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),c&&s&&m.markUp(c,"observed"),c&&m.scaleUp(c),!s){const l=c?.lastErrors?.length?` Service errors: ${c.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,t,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${l} The watcher recovers and the next request retries automatically.`);return}const d=path.basename(s,".sock");t.__lensmcpPod=d,P(u,"pod-select",{pod:d,pods:e.pool.socks.length,strategy:"round-robin"});let g=!1;const E=r(l=>{e.pool.socks=e.pool.socks.filter(w=>w!==s),e.pool.idx=-1;try{fs.unlinkSync(s)}catch{}console.error(`[gateway] pod ${d} of ${e.project}: ${l}`)},"evictPod"),a=armFirstByteGuard({socketPath:s,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:r(()=>!g&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:r(l=>{g=!0,E(l),(0,edge_1.sendError)(o,t,502,"unavailable",`Pod ${d} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:r((l,w)=>{console.log(`[gateway] pod ${d} of ${e.project}: slow first byte, pod alive (${l}ms, ${w.inflight} in-flight, ${w.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>a.clear()),N({socketPath:s},t,o,{onResponse:r(()=>{a.clear(),g=!0},"onResponse"),onError:r(l=>{a.clear(),!g&&(g=!0,E(l.message),(0,edge_1.sendError)(o,t,502,"unavailable",`Pod ${d} of ${e.project} unavailable \u2014 retry.`))},"onError")});return}const y=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let _=0,p=!1;const h=isReplayableRequest(t),f=r(()=>{if(edgeGone(t,o))return;const c=e.target;N(c,t,o,{onResponse:r(()=>{},"onResponse"),onError:r(s=>{if(isConnError(s)&&(_+=1),e.lens&&isConnError(s)&&h&&_>=2&&!p&&!edgeGone(t,o)){p=!0,b(e).then(d=>{if(!edgeGone(t,o)){if(d==="ready"){f();return}if(d==="unsupported"&&Date.now()<y){setTimeout(f,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}j(e,t,o,e.target,s,d)}});return}if(isConnError(s)&&h&&Date.now()<y&&!edgeGone(t,o)){setTimeout(f,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}j(e,t,o,c,s)},"onError")})},"tryTcp");f()},"forwardNative");return{proxy:M,forward:r(async(e,t,o)=>{const u=Date.now(),y=t.__lensmcpTrace;if(o.on("close",()=>{$(e,Date.now()-u,o.statusCode??0,t.__lensmcpCaller),O(y,e.project,o.statusCode??0,{...t.__lensmcpPod?{pod:t.__lensmcpPod}:{},...t.__lensmcpCaller?{caller:t.__lensmcpCaller}:{}},e)}),e.prependPrefix&&!t.url?.startsWith(e.prependPrefix)&&(t.url=e.prependPrefix+(t.url??"/")),(0,edge_1.stampForwardingHeaders)(n,t),k.arm(t),(t.httpVersionMajor??1)>=2){await q(e,t,o);return}if(e.pool){const s=e.svc;s&&(s.lastUsed=Date.now(),s.inflight+=1,o.on("close",()=>{s.inflight-=1,s.lastUsed=Date.now()}));let d=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!d&&s&&(P(y,"cold-start",{project:e.project}),await m.ensureUp(s)&&(d=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),s&&d&&m.markUp(s,"observed"),s&&m.scaleUp(s),!d){const S=s?.lastErrors?.length?` Service errors: ${s.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,t,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${S} The watcher recovers and the next request retries automatically.`);return}const g=path.basename(d,".sock");t.__lensmcpPod=g,P(y,"pod-select",{pod:g,pods:e.pool.socks.length,strategy:"round-robin"});let E=!1;const a=r(S=>{e.pool.socks=e.pool.socks.filter(v=>v!==d),e.pool.idx=-1;try{fs.unlinkSync(d)}catch{}console.error(`[gateway] pod ${g} of ${e.project}: ${S}`)},"evictPod"),l=armFirstByteGuard({socketPath:d,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:r(()=>!E&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:r(S=>{E=!0,a(S),(0,edge_1.sendError)(o,t,502,"unavailable",`Pod ${g} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:r((S,v)=>{console.log(`[gateway] pod ${g} of ${e.project}: slow first byte, pod alive (${S}ms, ${v.inflight} in-flight, ${v.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>l.clear());const w=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4);M.web(t,o,{target:{socketPath:d},agent:R,proxyTimeout:w},S=>{l.clear(),!E&&(E=!0,a(S.message),(0,edge_1.sendError)(o,t,502,"unavailable",`Pod ${g} of ${e.project} unavailable \u2014 retry.`))});return}const _=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let p=0,h=!1;const f=isReplayableRequest(t),c=r(()=>{if(edgeGone(t,o))return;const s=e.target,d={target:s,autoRewrite:typeof s=="string",changeOrigin:!0,agent:A(s)};M.web(t,o,d,g=>{if(isConnError(g)&&(p+=1),e.lens&&isConnError(g)&&f&&p>=2&&!h&&!edgeGone(t,o)){h=!0,b(e).then(E=>{if(!edgeGone(t,o)){if(E==="ready"){c();return}if(E==="unsupported"&&Date.now()<_){setTimeout(c,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}j(e,t,o,e.target,g,E)}});return}if(isConnError(g)&&f&&Date.now()<_&&!edgeGone(t,o)){setTimeout(c,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}o.destroyed||j(e,t,o,s,g)})},"tryTcp");c()},"forward"),agentForTarget:A,destroyAgents:r(()=>{k.stop(),R.destroy(),U.destroy()},"destroyAgents"),closeProxy:r(()=>{try{M.close?.()}catch{}},"closeProxy")}}T(createProxy,"createProxy"),r(createProxy,"createProxy");
1
+ "use strict";var G=Object.defineProperty;var P=(n,c)=>G(n,"name",{value:c,configurable:!0});var K=Object.defineProperty,s=P((n,c)=>K(n,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),body_stall_1=require("../body-stall"),types_1=require("./types"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys");function isConnError(n){const c=n.code??"";return c==="ECONNREFUSED"||c==="ECONNRESET"||c==="ETIMEDOUT"||c==="EPIPE"||c==="EHOSTUNREACH"||c==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(n.message)}P(isConnError,"isConnError"),s(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(n){if(!IDEMPOTENT_METHODS.has((n.method??"GET").toUpperCase())||n.headers["transfer-encoding"]!==void 0)return!1;const c=n.headers["content-length"];return!(c!==void 0&&String(c).trim()!=="0")}P(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(n,c){return c.headersSent||c.writableEnded||c.destroyed===!0||n.aborted===!0}P(edgeGone,"edgeGone"),s(edgeGone,"edgeGone");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),UPSTREAM_CONNECTION_HEADERS=["connection","keep-alive","proxy-connection"],httpProxy=require("http-proxy");function declaredFirstByteMs(n){const c=Number(n);return Number.isFinite(c)&&c>0?c:types_1.POD_RESPONSE_TIMEOUT_MS}P(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");function armFirstByteGuard(n){let c,v=0,O=!1,A=!1;const M=s(k=>{c=setTimeout(C,k),c.unref?.()},"arm"),C=s(()=>{if(!(O||!n.pending())){if(v+=n.firstByteMs,n.ceilingMs<=0||v>=n.ceilingMs){n.onWedged(`no response within ${v}ms (wedged socket) \u2014 evicted + respawning`);return}(0,pod_probe_1.probePodAlive)(n.socketPath,void 0,n.serviceKey).then(k=>{if(!(O||!n.pending())){if(!k){n.onWedged(`no response within ${v}ms and no answer on ${types_1.POD_ALIVE_PATH} (wedged socket) \u2014 evicted + respawning`);return}A||(A=!0,n.onSlow(v,k)),M(Math.max(n.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))}})}},"onDeadline");return M(n.firstByteMs),{clear:s(()=>{O=!0,c&&clearTimeout(c)},"clear")}}P(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(n,c,v){const{recordEdge:O,finishTrace:A,traceStep:M,emit:C}=c,k=s(async e=>{if(!e.lens||!n.lensSupervisor)return"unsupported";let r;const t=new Promise(p=>{r=setTimeout(()=>p("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),u=n.lensSupervisor.recover(e.lens,"proxy-connection").then(p=>p?"ready":"failed").catch(()=>"failed"),h=await Promise.race([u,t]);r&&clearTimeout(r);const m=n.lensSupervisor.status(e.lens);return h==="failed"&&(!m||m.state==="ready")?"unsupported":h},"waitForManagedRecovery"),U=s((e,r,t,u,h,m)=>{if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}const p=e.lens?n.lensSupervisor?.status(e.lens):void 0,E=m==="timeout"||p?.state==="starting"||p?.state==="replacing",_=(0,gateway_errors_1.ensureRequestId)(r),d=e.lens?{project:e.project,reason:p?.reason??"connection-failure",generation:p?.generation??0,...E?{retryAfterMs:2e3}:{}}:void 0,o=E?503:502,a=E?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${_} project=${e.project} target=${String(u)} state=${p?.state??"unmanaged"} generation=${p?.generation??0}: ${h.message}`),C("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:_,project:e.project,target:String(u),state:p?.state??"unmanaged",generation:p?.generation,error:h.message}),(0,edge_1.sendError)(t,r,o,a,E?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,d,E?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),x=(0,body_stall_1.createBodyStallGuard)(n.bodyStallMs??types_1.BODY_STALL_MS),I={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},D=new http.Agent(I),q=new(require("node:https")).Agent(I),N=s(e=>typeof e=="string"&&e.startsWith("https:")?q:D,"agentForTarget"),R=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:D}),L=s((e,r,t)=>{const u=s(()=>{t()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",u),e.once("close",()=>r.removeListener("close",u))},"propagateEdgeCancel");R.on("proxyReq",(e,r,t)=>{for(const u of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(u);e.setHeader("connection","keep-alive"),L(e,t,()=>t.writableEnded)}),R.on("proxyRes",(e,r,t)=>{const u=r.__lensmcpPod;u&&(e.headers["x-lensmcp-pod"]=u),delete e.headers["x-powered-by"],e.headers.server="LensMCP",t&&L(e,t,()=>t.writableEnded),t&&x.armResponse(t)});const B=s((e,r,t,u)=>{(0,edge_1.stampForwardingHeaders)(n,r),x.arm(r);const h={},m=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[i,y]of Object.entries(r.headers))y===void 0||i.startsWith(":")||HOP_BY_HOP.has(i)||m&&(i==="content-length"||i==="transfer-encoding")||(h[i]=y);let p=http,E;if(typeof e=="object")E={socketPath:e.socketPath,path:r.url,method:r.method,headers:h,agent:D};else{const i=new URL(e),y=i.protocol==="https:";p=y?require("node:https"):http,h.host=i.host,E={protocol:i.protocol,hostname:i.hostname,port:i.port||(y?443:80),path:r.url,method:r.method,headers:h,agent:N(e)}}const _=t;_.__lensmcpErrContained||(_.__lensmcpErrContained=!0,t.on("error",()=>{}));let d;const o=s(()=>{t.writableEnded||g.destroy()},"onReqGone"),a=s(()=>{d&&!d.destroyed&&d.destroy()},"onEdgeClose"),l=s(()=>{r.removeListener("error",o),r.removeListener("aborted",o),r.removeListener("close",o),t.removeListener("close",a)},"detach"),g=p.request(E,i=>{if(d=i,u.onResponse(),t.headersSent||t.writableEnded||t.destroyed){i.destroy();return}const y={};for(const[w,f]of Object.entries(i.headers))f===void 0||HOP_BY_HOP.has(w)||(y[w]=f);delete y["x-powered-by"],y.server="LensMCP";const j=r.__lensmcpPod;j&&(y["x-lensmcp-pod"]=j);try{t.writeHead(i.statusCode??502,y)}catch(w){console.error("[gateway] h2 relay: bad response header, resetting stream:",w.message),i.destroy(),t.destroyed||t.destroy(w);return}i.pipe(t),x.armResponse(t);const $=s(()=>{!t.writableEnded&&!t.destroyed&&t.destroy()},"abortEdge");i.once("error",$),i.once("close",$),t.on("close",a)});g.once("error",i=>u.onError(i)),g.once("close",l),r.on("error",o),r.on("aborted",o),r.on("close",o),m?g.end():r.pipe(g)},"pipeUpstream"),W=s(async(e,r,t)=>{const u=r.__lensmcpTrace;if(e.pool){const d=e.svc;d&&(d.lastUsed=Date.now(),d.inflight+=1,t.on("close",()=>{d.inflight-=1,d.lastUsed=Date.now()}));let o=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!o&&d&&(M(u,"cold-start",{project:e.project}),await v.ensureUp(d)&&(o=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),d&&o&&v.markUp(d,"observed"),d&&v.scaleUp(d),!o){const f=d?.lastErrors?.length?` Service errors: ${d.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${f} The watcher recovers and the next request retries automatically.`);return}const a=path.basename(o,".sock");r.__lensmcpPod=a,M(u,"pod-select",{pod:a,pods:e.pool.socks.length,strategy:"round-robin"});let l=!1;const g=s(f=>{e.pool.socks=e.pool.socks.filter(S=>S!==o),e.pool.idx=-1;try{fs.unlinkSync(o)}catch{}console.error(`[gateway] pod ${a} of ${e.project}: ${f}`)},"evictPod"),i=armFirstByteGuard({socketPath:o,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!l&&!t.headersSent&&!t.writableEnded,"pending"),onWedged:s(f=>{l=!0,g(f),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:s((f,S)=>{console.log(`[gateway] pod ${a} of ${e.project}: slow first byte, pod alive (${f}ms, ${S.inflight} in-flight, ${S.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>i.clear());const y=o,j=isReplayableRequest(r);let $=0;const w=s(()=>{$+=1,B({socketPath:y},r,t,{onResponse:s(()=>{i.clear(),l=!0},"onResponse"),onError:s(f=>{if(!l){if(edgeGone(r,t)){i.clear(),l=!0;return}(0,pod_probe_1.podConnectionVerdict)(y,f).then(S=>{if(!l){if(S==="alive"&&$<2&&j&&!edgeGone(r,t)){console.log(`[gateway] pod ${a} of ${e.project}: ${f.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),w();return}if(i.clear(),l=!0,S==="gone"){g(f.message),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} unavailable \u2014 retry.`);return}console.error(`[gateway] pod ${a} of ${e.project}: ${f.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");w();return}const h=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let m=0,p=!1;const E=isReplayableRequest(r),_=s(()=>{if(edgeGone(r,t))return;const d=e.target;B(d,r,t,{onResponse:s(()=>{},"onResponse"),onError:s(o=>{if(isConnError(o)&&(m+=1),e.lens&&isConnError(o)&&E&&m>=2&&!p&&!edgeGone(r,t)){p=!0,k(e).then(a=>{if(!edgeGone(r,t)){if(a==="ready"){_();return}if(a==="unsupported"&&Date.now()<h){setTimeout(_,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}U(e,r,t,e.target,o,a)}});return}if(isConnError(o)&&E&&Date.now()<h&&!edgeGone(r,t)){setTimeout(_,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}U(e,r,t,d,o)},"onError")})},"tryTcp");_()},"forwardNative");return{proxy:R,forward:s(async(e,r,t)=>{const u=Date.now(),h=r.__lensmcpTrace;if(t.on("close",()=>{O(e,Date.now()-u,t.statusCode??0,r.__lensmcpCaller),A(h,e.project,t.statusCode??0,{...r.__lensmcpPod?{pod:r.__lensmcpPod}:{},...r.__lensmcpCaller?{caller:r.__lensmcpCaller}:{}},e)}),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),(0,edge_1.stampForwardingHeaders)(n,r),x.arm(r),(r.httpVersionMajor??1)>=2){await W(e,r,t);return}if(e.pool){const o=e.svc;o&&(o.lastUsed=Date.now(),o.inflight+=1,t.on("close",()=>{o.inflight-=1,o.lastUsed=Date.now()}));let a=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!a&&o&&(M(h,"cold-start",{project:e.project}),await v.ensureUp(o)&&(a=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),o&&a&&v.markUp(o,"observed"),o&&v.scaleUp(o),!a){const T=o?.lastErrors?.length?` Service errors: ${o.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${T} The watcher recovers and the next request retries automatically.`);return}const l=path.basename(a,".sock");r.__lensmcpPod=l,M(h,"pod-select",{pod:l,pods:e.pool.socks.length,strategy:"round-robin"});let g=!1;const i=s(T=>{e.pool.socks=e.pool.socks.filter(b=>b!==a),e.pool.idx=-1;try{fs.unlinkSync(a)}catch{}console.error(`[gateway] pod ${l} of ${e.project}: ${T}`)},"evictPod"),y=armFirstByteGuard({socketPath:a,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!g&&!t.headersSent&&!t.writableEnded,"pending"),onWedged:s(T=>{g=!0,i(T),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${l} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:s((T,b)=>{console.log(`[gateway] pod ${l} of ${e.project}: slow first byte, pod alive (${T}ms, ${b.inflight} in-flight, ${b.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>y.clear());const j=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),$=a,w=isReplayableRequest(r);let f=0;const S=s(()=>{f+=1;const T=Date.now();R.web(r,t,{target:{socketPath:$},agent:D,proxyTimeout:j},b=>{if(!g){if(edgeGone(r,t)){y.clear(),g=!0;return}(0,pod_probe_1.podConnectionVerdict)($,b).then(H=>{if(g)return;const F=Date.now()-T>=j-250;if(H==="alive"&&f<2&&w&&!F&&!edgeGone(r,t)){console.log(`[gateway] pod ${l} of ${e.project}: ${b.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),S();return}if(y.clear(),g=!0,H==="gone"){i(b.message),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${l} of ${e.project} unavailable \u2014 retry.`);return}console.error(`[gateway] pod ${l} of ${e.project}: ${b.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${l} of ${e.project} dropped the connection \u2014 retry.`)})}})},"sendToPod");S();return}const m=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let p=0,E=!1;const _=isReplayableRequest(r),d=s(()=>{if(edgeGone(r,t))return;const o=e.target,a={target:o,autoRewrite:typeof o=="string",changeOrigin:!0,agent:N(o)};R.web(r,t,a,l=>{if(isConnError(l)&&(p+=1),e.lens&&isConnError(l)&&_&&p>=2&&!E&&!edgeGone(r,t)){E=!0,k(e).then(g=>{if(!edgeGone(r,t)){if(g==="ready"){d();return}if(g==="unsupported"&&Date.now()<m){setTimeout(d,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}U(e,r,t,e.target,l,g)}});return}if(isConnError(l)&&_&&Date.now()<m&&!edgeGone(r,t)){setTimeout(d,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}t.destroyed||U(e,r,t,o,l)})},"tryTcp");d()},"forward"),agentForTarget:N,destroyAgents:s(()=>{x.stop(),D.destroy(),q.destroy()},"destroyAgents"),closeProxy:s(()=>{try{R.close?.()}catch{}},"closeProxy")}}P(createProxy,"createProxy"),s(createProxy,"createProxy");
@@ -59,6 +59,27 @@ export declare function nestTypeormEntityRegistries(cache: NodeJS.Dict<NodeJS.Mo
59
59
  * Read generically, so a TypeORM version that adds a kind is covered without a change here.
60
60
  */
61
61
  export declare function typeormArgsRegistries(scope: object): ClassRegistry[];
62
+ /**
63
+ * class-validator's `MetadataStorage` on `globalThis.classValidatorMetadataStorage` (0.14: two Maps).
64
+ *
65
+ * - `validationMetadatas`: DTO class → one record per decorated property. Never removed, so every swap
66
+ * stacked a full generation of DTO classes on the last (foodguard engine, 2026-10-06: 539 targets =
67
+ * 7 generations × 77, in exact repeating blocks; dropping the old blocks freed 800 MB at once).
68
+ * - `constraintMetadatas`: constraint class → record. A rule written as a class (`@ValidatorConstraint`)
69
+ * is an app class with a name of its own and is matched like any other. A built-in rule is wrapped in
70
+ * a class that is ALWAYS called `CustomConstraint`, so it cannot be matched by name — a library
71
+ * evaluated once has the same name and no successor. Those go with the DTO record that points at
72
+ * them (`constraintCls`), and only when no surviving record still does.
73
+ *
74
+ * Read by `validate()` per request, so it is a `serve` registry.
75
+ */
76
+ export declare function classValidatorRegistries(scope: object): ClassRegistry[];
77
+ /**
78
+ * class-transformer's `defaultMetadataStorage`: four Maps keyed by the decorated class (`@Type`,
79
+ * `@Transform`, `@Expose`, `@Exclude`) plus `_ancestorsMap`, a per-class cache of its parents. One
80
+ * registry per LOADED copy of the package. Read by `plainToInstance` per request — a `serve` registry.
81
+ */
82
+ export declare function classTransformerRegistries(cache: NodeJS.Dict<NodeJS.Module>): ClassRegistry[];
62
83
  /** The registries a pod process holds right now — re-read on every call, since a library loads late. */
63
84
  export declare function defaultRegistries(cache?: NodeJS.Dict<NodeJS.Module>, scope?: object): ClassRegistry[];
64
85
  /** What every registry holds before a bundle import. */
@@ -1 +1 @@
1
- "use strict";var l=Object.defineProperty;var c=(t,e)=>l(t,"name",{value:e,configurable:!0});var d=Object.defineProperty,o=c((t,e)=>d(t,"name",{value:e,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.nestTypeormEntityRegistries=nestTypeormEntityRegistries,exports.typeormArgsRegistries=typeormArgsRegistries,exports.defaultRegistries=defaultRegistries,exports.captureRegistries=captureRegistries,exports.supersededSince=supersededSince,exports.pruneRegistries=pruneRegistries,exports.registryPruningEnabled=registryPruningEnabled,exports.describePrune=describePrune;const isClass=o(t=>typeof t=="function","isClass");function filterInPlace(t,e){const r=t.filter(e),n=t.length-r.length;return n>0&&t.splice(0,t.length,...r),n}c(filterInPlace,"filterInPlace"),o(filterInPlace,"filterInPlace");const NEST_TYPEORM_STORAGE=/[\\/]@nestjs[\\/]typeorm[\\/]dist[\\/]entities-metadata\.storage\.js$/;function nestTypeormEntityRegistries(t){const e=[];for(const[r,n]of Object.entries(t)){if(!n||!NEST_TYPEORM_STORAGE.test(r))continue;const s=n.exports?.EntitiesMetadataStorage?.storage;if(!(s instanceof Map))continue;const i=o(()=>[...s.values()].filter(u=>Array.isArray(u)),"lists");e.push({id:"@nestjs/typeorm entities",phase:"build",classes:o(()=>i().flat().filter(isClass),"classes"),prune:o(u=>i().reduce((a,f)=>a+filterInPlace(f,p=>!(isClass(p)&&u.has(p))),0),"prune")})}return e}c(nestTypeormEntityRegistries,"nestTypeormEntityRegistries"),o(nestTypeormEntityRegistries,"nestTypeormEntityRegistries");function typeormArgsRegistries(t){const e=t.typeormMetadataArgsStorage;if(!e||typeof e!="object")return[];const r=o(()=>Object.values(e).filter(s=>Array.isArray(s)),"lists"),n=o(s=>s&&typeof s=="object"?s.target:void 0,"targetOf");return[{id:"typeorm metadata",phase:"serve",classes:o(()=>r().flat().map(n).filter(isClass),"classes"),prune:o(s=>r().reduce((i,u)=>i+filterInPlace(u,a=>{const f=n(a);return!(isClass(f)&&s.has(f))}),0),"prune")}]}c(typeormArgsRegistries,"typeormArgsRegistries"),o(typeormArgsRegistries,"typeormArgsRegistries");function defaultRegistries(t=require.cache,e=globalThis){return[...nestTypeormEntityRegistries(t),...typeormArgsRegistries(e)]}c(defaultRegistries,"defaultRegistries"),o(defaultRegistries,"defaultRegistries");function captureRegistries(t=defaultRegistries()){const e=new Map;for(const r of t){const n=e.get(r.id)??new Set;for(const s of r.classes())n.add(s);e.set(r.id,n)}return e}c(captureRegistries,"captureRegistries"),o(captureRegistries,"captureRegistries");function supersededSince(t,e=defaultRegistries()){const r=new Set;for(const[n,s]of captureRegistries(e)){const i=t.get(n)??new Set,u=new Set;for(const a of s)!i.has(a)&&a.name&&u.add(a.name);for(const a of i)a.name&&u.has(a.name)&&r.add(a)}return r}c(supersededSince,"supersededSince"),o(supersededSince,"supersededSince");function pruneRegistries(t,e,r=defaultRegistries()){if(t.size===0)return[];const n=[];for(const s of r){if(s.phase!==e)continue;const i=s.prune(t);i>0&&n.push({registry:s.id,removed:i})}return n}c(pruneRegistries,"pruneRegistries"),o(pruneRegistries,"pruneRegistries");function registryPruningEnabled(t=process.env){const e=(t.LENSMCP_SWAP_PRUNE_REGISTRIES??"").trim().toLowerCase();return!(e==="0"||e==="false"||e==="no"||e==="off")}c(registryPruningEnabled,"registryPruningEnabled"),o(registryPruningEnabled,"registryPruningEnabled");function describePrune(t,e){const r=t.reduce((s,i)=>s+i.removed,0);if(r===0)return null;const n=t.map(s=>`${s.registry} ${s.removed}`).join(", ");return`[child] released ${r} superseded class registration(s) ${e} (${n})`}c(describePrune,"describePrune"),o(describePrune,"describePrune");
1
+ "use strict";var m=Object.defineProperty;var l=(t,e)=>m(t,"name",{value:e,configurable:!0});var g=Object.defineProperty,o=l((t,e)=>g(t,"name",{value:e,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.nestTypeormEntityRegistries=nestTypeormEntityRegistries,exports.typeormArgsRegistries=typeormArgsRegistries,exports.classValidatorRegistries=classValidatorRegistries,exports.classTransformerRegistries=classTransformerRegistries,exports.defaultRegistries=defaultRegistries,exports.captureRegistries=captureRegistries,exports.supersededSince=supersededSince,exports.pruneRegistries=pruneRegistries,exports.registryPruningEnabled=registryPruningEnabled,exports.describePrune=describePrune;const isClass=o(t=>typeof t=="function","isClass");function filterInPlace(t,e){const n=t.filter(e),a=t.length-n.length;return a>0&&t.splice(0,t.length,...n),a}l(filterInPlace,"filterInPlace"),o(filterInPlace,"filterInPlace");const NEST_TYPEORM_STORAGE=/[\\/]@nestjs[\\/]typeorm[\\/]dist[\\/]entities-metadata\.storage\.js$/;function nestTypeormEntityRegistries(t){const e=[];for(const[n,a]of Object.entries(t)){if(!a||!NEST_TYPEORM_STORAGE.test(n))continue;const s=a.exports?.EntitiesMetadataStorage?.storage;if(!(s instanceof Map))continue;const c=o(()=>[...s.values()].filter(f=>Array.isArray(f)),"lists");e.push({id:"@nestjs/typeorm entities",phase:"build",classes:o(()=>c().flat().filter(isClass),"classes"),prune:o(f=>c().reduce((r,d)=>r+filterInPlace(d,u=>!(isClass(u)&&f.has(u))),0),"prune")})}return e}l(nestTypeormEntityRegistries,"nestTypeormEntityRegistries"),o(nestTypeormEntityRegistries,"nestTypeormEntityRegistries");function typeormArgsRegistries(t){const e=t.typeormMetadataArgsStorage;if(!e||typeof e!="object")return[];const n=o(()=>Object.values(e).filter(s=>Array.isArray(s)),"lists"),a=o(s=>s&&typeof s=="object"?s.target:void 0,"targetOf");return[{id:"typeorm metadata",phase:"serve",classes:o(()=>n().flat().map(a).filter(isClass),"classes"),prune:o(s=>n().reduce((c,f)=>c+filterInPlace(f,r=>{const d=a(r);return!(isClass(d)&&s.has(d))}),0),"prune")}]}l(typeormArgsRegistries,"typeormArgsRegistries"),o(typeormArgsRegistries,"typeormArgsRegistries");const GENERIC_CONSTRAINT="CustomConstraint";function classValidatorRegistries(t){const e=t.classValidatorMetadataStorage;if(!e||typeof e!="object")return[];const{validationMetadatas:n,constraintMetadatas:a}=e;if(!(n instanceof Map))return[];const s=a instanceof Map?a:new Map,c=o(()=>[...s.keys()].filter(isClass).filter(r=>r.name!==GENERIC_CONSTRAINT),"namedConstraints"),f=o(r=>r&&typeof r=="object"?r.constraintCls:void 0,"constraintOf");return[{id:"class-validator metadata",phase:"serve",classes:o(()=>[...[...n.keys()].filter(isClass),...c()],"classes"),prune:o(r=>{let d=0;const u=new Set;for(const i of[...n.keys()]){if(!isClass(i)||!r.has(i))continue;const p=n.get(i);if(Array.isArray(p))for(const y of p)u.add(f(y));n.delete(i),d++}for(const i of c())r.has(i)&&s.delete(i)&&d++;if(u.size>0){for(const i of n.values())if(Array.isArray(i))for(const p of i)u.delete(f(p));for(const i of u)isClass(i)&&i.name===GENERIC_CONSTRAINT&&s.delete(i)&&d++}return d},"prune")}]}l(classValidatorRegistries,"classValidatorRegistries"),o(classValidatorRegistries,"classValidatorRegistries");const CLASS_TRANSFORMER_STORAGE=/[\\/]class-transformer[\\/]cjs[\\/]storage\.js$/,CLASS_TRANSFORMER_MAPS=["_typeMetadatas","_transformMetadatas","_exposeMetadatas","_excludeMetadatas"];function classTransformerRegistries(t){const e=[];for(const[n,a]of Object.entries(t)){if(!a||!CLASS_TRANSFORMER_STORAGE.test(n))continue;const s=a.exports?.defaultMetadataStorage;if(!s||typeof s!="object")continue;const c=CLASS_TRANSFORMER_MAPS.map(r=>s[r]).filter(r=>r instanceof Map);if(c.length===0)continue;const f=s._ancestorsMap;e.push({id:"class-transformer metadata",phase:"serve",classes:o(()=>c.flatMap(r=>[...r.keys()]).filter(isClass),"classes"),prune:o(r=>{let d=0;for(const u of c)for(const i of[...u.keys()])isClass(i)&&r.has(i)&&u.delete(i)&&d++;if(f instanceof Map)for(const[u,i]of[...f.entries()])(isClass(u)&&r.has(u)||Array.isArray(i)&&i.some(p=>isClass(p)&&r.has(p)))&&f.delete(u);return d},"prune")})}return e}l(classTransformerRegistries,"classTransformerRegistries"),o(classTransformerRegistries,"classTransformerRegistries");function defaultRegistries(t=require.cache,e=globalThis){return[...nestTypeormEntityRegistries(t),...typeormArgsRegistries(e),...classValidatorRegistries(e),...classTransformerRegistries(t)]}l(defaultRegistries,"defaultRegistries"),o(defaultRegistries,"defaultRegistries");function captureRegistries(t=defaultRegistries()){const e=new Map;for(const n of t){const a=e.get(n.id)??new Set;for(const s of n.classes())a.add(s);e.set(n.id,a)}return e}l(captureRegistries,"captureRegistries"),o(captureRegistries,"captureRegistries");function supersededSince(t,e=defaultRegistries()){const n=new Set;for(const[a,s]of captureRegistries(e)){const c=t.get(a)??new Set,f=new Set;for(const r of s)!c.has(r)&&r.name&&f.add(r.name);for(const r of c)r.name&&f.has(r.name)&&n.add(r)}return n}l(supersededSince,"supersededSince"),o(supersededSince,"supersededSince");function pruneRegistries(t,e,n=defaultRegistries()){if(t.size===0)return[];const a=[];for(const s of n){if(s.phase!==e)continue;const c=s.prune(t);c>0&&a.push({registry:s.id,removed:c})}return a}l(pruneRegistries,"pruneRegistries"),o(pruneRegistries,"pruneRegistries");function registryPruningEnabled(t=process.env){const e=(t.LENSMCP_SWAP_PRUNE_REGISTRIES??"").trim().toLowerCase();return!(e==="0"||e==="false"||e==="no"||e==="off")}l(registryPruningEnabled,"registryPruningEnabled"),o(registryPruningEnabled,"registryPruningEnabled");function describePrune(t,e){const n=t.reduce((s,c)=>s+c.removed,0);if(n===0)return null;const a=t.map(s=>`${s.registry} ${s.removed}`).join(", ");return`[child] released ${n} superseded class registration(s) ${e} (${a})`}l(describePrune,"describePrune"),o(describePrune,"describePrune");
package/main.devserver.js CHANGED
@@ -1,10 +1,21 @@
1
- "use strict";var Ee=Object.defineProperty;var g=(o,u)=>Ee(o,"name",{value:u,configurable:!0});var ge=Object.defineProperty,i=g((o,u)=>ge(o,"name",{value:u,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DEBUG_FIXED_PORTS_ENV=void 0,exports.isOwnerOnlyTransport=isOwnerOnlyTransport,exports.usesFixedInspectorPorts=usesFixedInspectorPorts,exports.inspectorPortFor=inspectorPortFor,exports.redactInspectorUrls=redactInspectorUrls,exports.inspectorPortOf=inspectorPortOf;const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path")),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),node_child_process_1=require("node:child_process"),http=tslib_1.__importStar(require("node:http")),readline=tslib_1.__importStar(require("node:readline")),inspector=tslib_1.__importStar(require("node:inspector")),util=tslib_1.__importStar(require("node:util")),exit_reason_1=require("./exit-reason"),hot_swap_registries_1=require("./hot-swap-registries"),process_name_1=require("./process-name"),httpProxy=require("http-proxy"),{prettyFactory}=require("pino-pretty"),BUNDLE_PATH=process.env.BUNDLE_PATH?.trim()||"./main.js",SERVICE_NAME=process.env.SERVICE_NAME||"",SERVICE_PREFIX=process.env.SERVE_PREFIX?`/${process.env.SERVE_PREFIX}`:"",GATEWAY_MIDDLEWARE_PATH=process.env.GATEWAY_MIDDLEWARE||"",GATEWAY_CONFIG=(()=>{const o=process.env.GATEWAY_CONFIG;if(!o)return{};try{return JSON.parse(o)}catch{return{}}})();let gatewayMiddleware=null;if(GATEWAY_MIDDLEWARE_PATH)try{const o=require(GATEWAY_MIDDLEWARE_PATH);gatewayMiddleware=typeof o=="function"?o:typeof o.default=="function"?o.default:null,gatewayMiddleware||console.warn(`[gateway] ${GATEWAY_MIDDLEWARE_PATH} does not export a function, gateway disabled`)}catch(o){console.error(`[gateway] Failed to load middleware from ${GATEWAY_MIDDLEWARE_PATH}:`,o)}function applyGatewayMiddleware(o){if(gatewayMiddleware)try{gatewayMiddleware(o,GATEWAY_CONFIG)}catch(u){console.error("[gateway] Middleware error:",u)}}g(applyGatewayMiddleware,"applyGatewayMiddleware"),i(applyGatewayMiddleware,"applyGatewayMiddleware");const GATEWAY_ROUTES=(()=>{try{return JSON.parse(process.env.GATEWAY_ROUTES||"[]")}catch{return[]}})(),GATEWAY_HTTPS=process.env.GATEWAY_HTTPS==="1";function hostMatches(o,u){if(o.startsWith("*.")){const f=o.slice(1);return u.endsWith(f)||u===o.slice(2)}return u===o}g(hostMatches,"hostMatches"),i(hostMatches,"hostMatches");function matchGatewayRoute(o){if(GATEWAY_ROUTES.length===0)return;const u=(o.headers.host||"").split(":")[0],f=o.url||"/";for(const E of GATEWAY_ROUTES)if(!(E.host&&!hostMatches(E.host,u))&&!(E.prefix&&!f.startsWith(E.prefix)))return E}g(matchGatewayRoute,"matchGatewayRoute"),i(matchGatewayRoute,"matchGatewayRoute");const WORKER_NAMES=(()=>{try{return JSON.parse(process.env.WORKERS||"[]")}catch{return[]}})(),LENSMCP_WS_KEY=process.env.LENSMCP_WS_KEY||"",SOCK_DIR=LENSMCP_WS_KEY?path.join(os.tmpdir(),LENSMCP_WS_KEY,`${SERVICE_NAME||"lensmcp-cluster"}-devserver`):path.join(os.tmpdir(),`${SERVICE_NAME||"lensmcp-cluster"}-devserver`),POD_GENERATION=(()=>{const o=Number(process.env.LENSMCP_POD_GENERATION);return Number.isFinite(o)&&o>1?Math.floor(o):1})();function childSockPath(o){return path.join(SOCK_DIR,POD_GENERATION>1?`child-${o}-g${POD_GENERATION}.sock`:`child-${o}.sock`)}g(childSockPath,"childSockPath"),i(childSockPath,"childSockPath");function sockOwnerPath(o){return`${o}.owner`}g(sockOwnerPath,"sockOwnerPath"),i(sockOwnerPath,"sockOwnerPath");function isOwnerOnlyTransport(o){try{return typeof o.socket?.server?.address?.()=="string"}catch{return!1}}g(isOwnerOnlyTransport,"isOwnerOnlyTransport"),i(isOwnerOnlyTransport,"isOwnerOnlyTransport"),exports.DEBUG_FIXED_PORTS_ENV="LENSMCP_DEBUG_FIXED_PORTS";function usesFixedInspectorPorts(o=process.env){const u=(o[exports.DEBUG_FIXED_PORTS_ENV]??"").trim().toLowerCase();return u==="1"||u==="true"||u==="yes"||u==="on"}g(usesFixedInspectorPorts,"usesFixedInspectorPorts"),i(usesFixedInspectorPorts,"usesFixedInspectorPorts");function inspectorPortFor(o,u,f=process.env){if(!usesFixedInspectorPorts(f))return 0;const E=Math.trunc(o)+Math.trunc(u);return Number.isInteger(E)&&E>1024&&E<=65535?E:0}g(inspectorPortFor,"inspectorPortFor"),i(inspectorPortFor,"inspectorPortFor");const INSPECTOR_TOKEN_RE=/((?:wss?:\/\/[^\s/]+\/|ws=[^\s/]+\/))([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;function redactInspectorUrls(o){return o.replace(INSPECTOR_TOKEN_RE,"$1<inspector-token-redacted>")}g(redactInspectorUrls,"redactInspectorUrls"),i(redactInspectorUrls,"redactInspectorUrls");function inspectorPortOf(o){const u=/^wss?:\/\/[^/]*?:(\d+)\//.exec(o??""),f=u?Number(u[1]):NaN;return Number.isInteger(f)&&f>0?f:null}g(inspectorPortOf,"inspectorPortOf"),i(inspectorPortOf,"inspectorPortOf");function ensureOwnerOnlyDir(o){try{fs.mkdirSync(o,{recursive:!0,mode:448})}catch{}try{const u=fs.lstatSync(o);u.isDirectory()&&(u.mode&63)!==0&&fs.chmodSync(o,448)}catch{}}g(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),i(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function claimSock(o,u=process.pid){ensureOwnerOnlyDir(path.dirname(o));try{const f=sockOwnerPath(o);try{fs.unlinkSync(f)}catch{}fs.writeFileSync(f,String(u),{flag:"wx",mode:384})}catch{}try{fs.unlinkSync(o)}catch{}}g(claimSock,"claimSock"),i(claimSock,"claimSock");function cleanupSock(o,u=process.pid){if(o){try{const f=fs.readFileSync(sockOwnerPath(o),"utf8").trim();if(f&&f!==String(u))return}catch{}try{fs.unlinkSync(o)}catch{}try{fs.unlinkSync(sockOwnerPath(o))}catch{}}}g(cleanupSock,"cleanupSock"),i(cleanupSock,"cleanupSock");function serviceKeyFingerprint(){const o=process.env.LENSMCP_SERVICE_KEY;if(!o)return;const{createHash:u}=require("node:crypto");return u("sha256").update(o).digest("hex").slice(0,8)}g(serviceKeyFingerprint,"serviceKeyFingerprint"),i(serviceKeyFingerprint,"serviceKeyFingerprint");async function importFresh(spec){process.env.DEVSERVER_MODE="1";const requested=path.isAbsolute(spec)?spec:path.join(__dirname,spec);let resolved=requested;try{resolved=fs.realpathSync(requested)}catch{}return eval(`delete require.cache[${JSON.stringify(resolved)}]`),eval(`require(${JSON.stringify(resolved)})`)}g(importFresh,"importFresh"),i(importFresh,"importFresh");function debounce(o,u){let f=null;return(...E)=>{f&&clearTimeout(f),f=setTimeout(()=>o(...E),u)}}if(g(debounce,"debounce"),i(debounce,"debounce"),process.env.APP_RUNNER==="1"){const o=i(f=>{if(!inspector.url())try{inspector.open(f,"127.0.0.1",!1)}catch($){console.warn(`[child] inspector.open(${f}) failed (port taken by another devserver?) \u2014 falling back to a random port:`,$.message);try{inspector.open(0,"127.0.0.1",!1)}catch{}}const E=inspector.url();E&&process.send&&process.send({type:"inspector-url",url:E})},"openInspector"),u=process.env.CHILD_DEBUG_PORT;if(u!==void 0&&u!==""){const f=Number(u);Number.isInteger(f)&&(f===0||f>1024)&&o(f)}try{require("@lensmcp/node-instrumentation/register")}catch{}(async()=>{let f=null,E=!1,$,G=!1;const k=(0,hot_swap_registries_1.registryPruningEnabled)(),M=i((r,s)=>{const h=(0,hot_swap_registries_1.describePrune)(r,s);h&&console.log(h)},"logPrune");let T={app:null,handler:i((r,s)=>{s.statusCode=503,s.end("starting")},"handler"),inflight:0};const L=(()=>{const r=Number(process.env.LENSMCP_SWAP_DRAIN_MS);return Number.isFinite(r)&&r>=0?r:15*6e4})(),W=i(r=>{const s=globalThis.__lensmcpActiveWork;if(typeof s!="function")return{count:0};try{const h=s(r);return{count:Number(h?.count??0),...h?.oldestStartedAt?{oldestStartedAt:h.oldestStartedAt}:{}}}catch{return{count:0}}},"activeWork"),U=(()=>{const r=Number(process.env.LENSMCP_SWAP_MAX_DRAINING);return Number.isFinite(r)&&r>=1?Math.floor(r):1})(),b=new Set;async function R(r,s){if(!r.closed&&(r.closed=!0,b.delete(r),s&&console.error(`[child] ${s}`),!!r.app)){try{await r.app.close()}catch(h){console.error("[child] error closing a previous app:",h)}if(r.superseded){try{M((0,hot_swap_registries_1.pruneRegistries)(r.superseded,"serve"),"after closing the previous app")}catch(h){console.error("[child] could not release the previous app's class registrations:",h)}r.superseded=void 0}}}g(R,"W"),i(R,"closeGeneration");async function Y(r){for(;b.size>=U;){const S=b.values().next().value;if(!S)break;await R(S,`closing a still-draining generation early \u2014 ${b.size} were pending and the cap is ${U} (each holds a whole app: DI container, DB pool, queue workers). Raise LENSMCP_SWAP_MAX_DRAINING if this pod legitimately needs more.`)}b.add(r);const s=Date.now(),h=i(()=>({inflight:r.inflight,work:W(r.workGen).count}),"busy");for(;L>0&&Date.now()-s<L;){if(r.closed)return;const S=h();if(S.inflight===0&&S.work===0)break;await new Promise(w=>{setTimeout(w,250).unref?.()})}if(r.closed)return;const _=h();_.inflight>0||_.work>0?console.error(`[child] swap drain expired after ${Date.now()-s}ms \u2014 closing the previous app with ${_.inflight} in-flight request(s) and ${_.work} background job(s) still running. A queue job killed here keeps its lock until the TTL lapses. Raise LENSMCP_SWAP_DRAIN_MS (currently ${L}ms) if this is legitimate work.`):Date.now()-s>250&&console.log(`[child] previous app drained in ${Date.now()-s}ms \u2014 closing it`),await R(r)}g(Y,"J"),i(Y,"drainThenClose");async function x(){if(E)return G=!0,console.log("[child] Reload deferred \u2014 will re-swap after current swap completes"),f;E=!0,process.removeAllListeners("uncaughtException"),process.removeAllListeners("unhandledRejection"),process.on("uncaughtException",(y,P)=>{console.error(`[child] uncaughtException (${P}):`,y)}),process.on("unhandledRejection",y=>{const P=y instanceof Error?y.stack||y.message:String(y);P.includes("FronteggContextScope")||P.includes("populateLoggerMetadata")||console.error("[child] unhandledRejection:",y)});let r;if(k)try{r=(0,hot_swap_registries_1.captureRegistries)()}catch{r=void 0}try{try{await importFresh(BUNDLE_PATH)}catch(y){if(!(y instanceof SyntaxError))throw y;console.warn(`[child] bundle was unreadable mid-write (${y.message}) \u2014 retrying the swap once`),await new Promise(P=>setTimeout(P,400)),await importFresh(BUNDLE_PATH)}}catch(y){throw E=!1,y}if(typeof global.createChildApp!="function")throw E=!1,new Error(`Bundle '${BUNDLE_PATH}' does not export createChildApp() and the zero-touch bridge is not active. Either install @lensmcp/node-instrumentation (a plain NestFactory.create + app.listen main.ts then just works) or set global.createChildApp in main.ts when DEVSERVER_MODE === '1'.`);let s;if(r)try{s=(0,hot_swap_registries_1.supersededSince)(r),M((0,hot_swap_registries_1.pruneRegistries)(s,"build"),"at the swap")}catch(y){console.error("[child] could not release superseded class registrations:",y)}const h=global.__lensmcpBeginWorkGeneration,_=typeof h=="function"?h():void 0;let S;try{S=await global.createChildApp()}catch(y){throw E=!1,y}const w=T;return s?.size&&(w.app?w.superseded=s:M((0,hot_swap_registries_1.pruneRegistries)(s,"serve"),"at the swap")),f=S,T={app:S,handler:S.handler,inflight:0,..._===void 0?{}:{workGen:_}},console.log("[child] Swapped to fresh app (previous draining in background)"),w.app&&Y(w).catch(y=>console.error("[child] background close error:",y)),E=!1,G?(G=!1,x()):S}g(x,"M"),i(x,"swapNow");const Q=debounce(async()=>{try{console.log("[child] /webpack/reload"),await x()}catch(r){console.error("[child] reload failed:",r)}},250),F=http.createServer((r,s)=>{if(r.method==="POST"&&r.url==="/webpack/reload"){s.writeHead(200,{"content-type":"application/json"}),s.end(JSON.stringify({ok:!0})),Q();return}if(r.method==="GET"&&r.url==="/__lensmcp/alive"){const _=r.headers["x-internal-token"],S=process.env.LENSMCP_SERVICE_KEY;if(S&&_!==S){s.writeHead(404,{"content-type":"text/plain"}),s.end("not found");return}const w=W();s.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),s.end(JSON.stringify({ok:!0,pid:process.pid,swapping:E,inflight:T.inflight,oldestInflightAt:T.oldestAt,activeWork:w.count,oldestWorkAt:w.oldestStartedAt,keyFingerprint:serviceKeyFingerprint(),...$?{watcher:$}:{},...Number.isFinite(Number(process.env.LENSMCP_POD_GENERATION))&&process.env.LENSMCP_POD_GENERATION?{podGeneration:Number(process.env.LENSMCP_POD_GENERATION)}:{}}));return}const h=T;h.inflight+=1,h.oldestAt===void 0&&(h.oldestAt=Date.now()),s.on("close",()=>{h.inflight-=1,h.inflight===0&&(h.oldestAt=void 0)}),h.handler(r,s)}),v=process.env.CHILD_SOCK_PATH;process.on("disconnect",()=>{cleanupSock(v),process.exit(0)});try{await x();const r=path.isAbsolute(BUNDLE_PATH)?BUNDLE_PATH:path.join(__dirname,BUNDLE_PATH);console.log(`[child] Using bundle: ${r}`),claimSock(v),F.listen(v,()=>{console.log(`[child] Listening on ${v}`),process.send&&process.send({type:"ready",socketPath:v})})}catch(r){console.error("[child] Startup error:",r),process.send&&process.send({type:"boot-error",error:String(r?.stack||r)}),process.exitCode=1;return}process.on("message",async r=>{if(!(!r||typeof r!="object")){if(r.type==="debug-open"){const s=Number(r.port);o(Number.isInteger(s)&&s>1024?s:0);return}if(r.type==="watcher"){const s=r.testimony;s&&typeof s=="object"&&($=s);return}if(r.type==="reload")try{await x(),process.send&&process.send({type:"reloaded",socketPath:v})}catch(s){console.error("[child] reload error:",s),process.send&&process.send({type:"reload-error",error:String(s?.stack||s)})}}}),process.on("beforeExit",()=>{try{inspector?.close?.()}catch{}});const p=i(async r=>{console.log(`[child ${r}] shutting down\u2026`);const s=f?.close().catch(_=>console.error("[child] close error:",_)),h=new Promise(_=>F.close(()=>_()));cleanupSock(v),setTimeout(()=>process.exit(0),700).unref(),await Promise.all([s,h]),process.exit(0)},"shutdown");["SIGINT","SIGTERM","SIGHUP","SIGQUIT","SIGUSR2"].forEach(r=>{process.on(r,()=>{inspector?.close?.(),p(r)})})})()}else{let o=g(function(p){b.push(p),b.length>U&&b.splice(0,b.length-U),L?.write(p.replace(W,""));for(const r of R)try{const s=r.plain?p.replace(W,""):p;r.res.write(r.redact?redactInspectorUrls(s):s)}catch{R.delete(r)}},"W");i(o,"emitLogLine");const u=["\x1B[36m","\x1B[33m","\x1B[35m","\x1B[32m","\x1B[34m","\x1B[31m"],f="\x1B[0m",E=Math.max(1,Number(process.env.CHILD_COUNT||1)),$=LENSMCP_WS_KEY||path.basename(process.cwd()),G=i(p=>(0,process_name_1.namedExecPath)((0,process_name_1.processName)($,SERVICE_NAME||"service",p)),"execPathFor");let k=(()=>{const p=Number(process.env.LENSMCP_DEBUG_PORT||"");return Number.isInteger(p)&&p>1024?p:0})();const M=20;k&&(console.warn("[parent] DEBUGGER ENABLED \u2014 pods and workers open a Node inspector. Node's inspector has NO authentication: the ws:// URL's UUID is the whole credential, and any account on this host that obtains it can run code as you. Do not enable this on a shared machine. (issues/codex-security/06)"),console.warn(usesFixedInspectorPorts()?`[parent] ${exports.DEBUG_FIXED_PORTS_ENV} is set \u2014 inspectors bind PREDICTABLE ports (${k}+slot, workers ${k}+${M}+i). Any account on this host can connect to 127.0.0.1:${k} and read the session UUID from /json/list. Unset it to use ephemeral ports.`:`[parent] Inspectors bind an EPHEMERAL loopback port; attach via the printed "Debugger listening" URL, or \`lensmcp debug <service>\` (control socket). Set ${exports.DEBUG_FIXED_PORTS_ENV}=1 for fixed ports if your IDE needs a pre-configured attach port \u2014 any account on this host can then reach them.`));const T=process.env.LENSMCP_LOG_FILE||"",L=T?(()=>{try{fs.mkdirSync(path.dirname(T),{recursive:!0});const p=fs.createWriteStream(T,{flags:"a",mode:384});try{fs.chmodSync(T,384)}catch{}return p}catch(p){return console.warn(`[parent] cannot open log file ${T}:`,p.message),null}})():null,W=/\x1b\[[0-9;]*m/g,U=2e3,b=[],R=new Set;for(const p of["log","info","warn","error"]){const r=console[p].bind(console);console[p]=(...s)=>{r(...s),o(s.map(h=>typeof h=="string"?h:util.inspect(h)).join(" ")+`
2
- `)}}const Y=i(p=>u[(p-1)%u.length],"colorFor"),x=i(p=>{if(E===1)return"";const r=`[child#${p}] `;return`${Y(p)}${r}${f}`},"tagFor"),Q=i(p=>p.startsWith("{")&&p.endsWith("}"),"isJsonLog"),F=i((p,r,s)=>{const h=prettyFactory({sync:!0,colorize:!0,crlf:!0,messageKey:"message",errorLikeObjectKeys:["err","error"],errorProps:"type,message,stack",ignore:["logContext","context","hostname","req","res","err.driverError","module","cloudEnvironment","frontegg-application-id","frontegg-tenant-id","frontegg-trace-id","frontegg-vendor-id","host","service","version","err","error"].join(","),messageFormat:"{if module}[{module}] {end}{if context}[{context}] {end}{if logContext}[{logContext}] {end}{message}",customPrettifiers:{stack:i(w=>`
3
- `+String(w),"stack")}}),_=i((w,y)=>{const P=w==="stdout"?process.stdout:process.stderr;try{s?.(y)}catch{}const A=Q(y)?h(y):`${y}
4
- `;P.write(`${r}${A}`),o(`${r}${A}`)},"write"),S=i((w,y)=>{if(!w)return;const P=readline.createInterface({input:w});P.on("line",A=>_(y,A)),P.on("close",()=>{})},"attach");S(p.stdout,"stdout"),S(p.stderr,"stderr")},"wireProcLogging"),v=i(p=>F(p.proc,x(p.id),r=>p.tail.push(r)),"wireChildLogging");(async()=>{ensureOwnerOnlyDir(SOCK_DIR);const p=httpProxy.createProxyServer({});let r=Number(process.env.PORT)||0;r||(r=9090);const s=[],h=new Set;function _(){let e=0;for(;h.has(e);)e++;return h.add(e),e}g(_,"w"),i(_,"allocDebugSlot");let S=1,w=0,y=!1,P=E,A=[];function Z(){return s.filter(e=>e.healthy&&typeof e.socketPath=="string")}g(Z,"te"),i(Z,"healthyChildren");let K;function ee(e){if(!e||typeof e!="object")return;const t=e;if(t.schemaVersion===1){K=t;for(const d of s)try{d.proc.send?.({type:"watcher",testimony:t})}catch{}}}g(ee,"re"),i(ee,"recordWatcherTestimony");function te(e,t=256*1024){return new Promise(d=>{let c="",l=!1;e.on("data",n=>{l||(c+=n,c.length>t&&(l=!0))}),e.on("end",()=>{if(l||!c)return d(void 0);try{d(JSON.parse(c))}catch{d(void 0)}}),e.on("error",()=>d(void 0))})}g(te,"oe"),i(te,"readJsonBody");function H(){const e=Z();if(e.length===0)return null;const t=w%e.length;return w=(w+1)%e.length,e[t]}g(H,"Y"),i(H,"pickChild");async function V(){const e=S++,t=childSockPath(e);cleanupSock(t);const d=["--enable-source-maps"];__filename.endsWith(".ts")&&d.unshift("--require","@swc-node/register");const c=k?_():-1,l=(0,node_child_process_1.fork)(__filename,{env:{...process.env,APP_RUNNER:"1",CHILD_SOCK_PATH:t,...c>=0&&{CHILD_DEBUG_PORT:String(inspectorPortFor(k,c))}},stdio:["inherit","pipe","pipe","ipc"],execArgv:d,execPath:G("pod")}),n={id:e,proc:l,healthy:!1,lastError:null,debugSlot:c,tail:new exit_reason_1.TailBuffer};return l.on("message",m=>{if(!m||typeof m!="object")return;const a=m;if(a.type==="ready"){if(n.socketPath=String(a.socketPath),n.healthy=!0,console.log(`[parent] Child#${n.id} ready on socket ${n.socketPath}`),K)try{l.send?.({type:"watcher",testimony:K})}catch{}}else a.type==="inspector-url"?(n.inspectorUrl=String(a.url),console.log(`[parent] Child#${n.id} Debugger listening on ${a.url}`)):a.type==="reloaded"?(n.socketPath=String(a.socketPath),n.healthy=!0,console.log(`[parent] Child#${n.id} hot-swapped on socket ${n.socketPath}`)):a.type==="reload-error"?(n.lastError=String(a.error||"unknown reload error"),console.error(`[parent] Child#${n.id} reload error: ${n.lastError}`),n.healthy=!1):a.type==="boot-error"&&(n.lastError=String(a.error||"unknown boot error"),console.error(`[parent] Child#${n.id} boot error: ${n.lastError}`),n.healthy=!1)}),l.on("exit",(m,a)=>{n.debugSlot>=0&&(h.delete(n.debugSlot),n.debugSlot=-1);const N=a==="SIGINT"||a==="SIGTERM"||m===0;if(N?console.log(`[parent] Child#${n.id} exited cleanly (code=${m}, signal=${a??"none"})`):(console.error((0,exit_reason_1.crashReport)(`[parent] Child#${n.id} crashed (code=${m}, signal=${a??"none"}, pid=${n.proc.pid}, ${n.healthy?"was serving":"was not ready"})`,m,a,n.tail.snapshot())),n.lastError||(n.lastError=`Child exited abnormally (code=${m}, signal=${a??"none"})`)),n.healthy=!1,cleanupSock(n.socketPath,n.proc.pid),n.socketPath=void 0,!N&&!y){const O=s.indexOf(n);O>=0&&s.splice(O,1);const q=Date.now();if(A=A.filter(C=>q-C<3e4),A.length>=5){console.error(`[parent] crash-loop detected (${A.length} respawns/30s) \u2014 pausing auto-respawn; POST /webpack/reload to retry`);return}A.push(q),setTimeout(()=>{const C=s.filter(ye=>ye.proc.killed===!1).length;!y&&C<P&&(console.log(`[parent] respawning a crashed pod (${C}/${P} live)`),V().catch(()=>{}))},1500).unref?.()}}),s.push(n),v(n),n}g(V,"Q"),i(V,"spawnChild");async function re(e){P=Math.max(P,e);const t=s.filter(c=>c.proc.killed===!1),d=e-t.length;for(let c=0;c<d;c++)await V()}g(re,"ne"),i(re,"ensurePoolSize");async function ce(){const e=s.filter(t=>!t.healthy||typeof t.socketPath!="string");await Promise.all(e.map(async t=>{try{t.proc.kill("SIGTERM")}catch{}cleanupSock(t.socketPath,t.proc.pid),t.socketPath=void 0;const d=s.indexOf(t);d>=0&&s.splice(d,1),await V()}))}g(ce,"pe"),i(ce,"respawnUnhealthy");const I=[];function oe(e){const t=path.dirname(path.isAbsolute(BUNDLE_PATH)?BUNDLE_PATH:path.resolve(BUNDLE_PATH)),d=path.join(t,`${e}.js`);if(!fs.existsSync(d))return console.error(`[parent] Worker "${e}" bundle not found: ${d}`),null;const c=["--enable-source-maps"];__filename.endsWith(".ts")&&c.unshift("--require","@swc-node/register");try{c.push("--require",require.resolve("@lensmcp/node-instrumentation/register"))}catch{}const l=inspectorPortFor(k||9229,M+Math.max(0,WORKER_NAMES.indexOf(e)));c.push(k?`--inspect=127.0.0.1:${l}`:`--inspect-port=127.0.0.1:${l}`);const n=(0,node_child_process_1.fork)(d,{env:process.env,stdio:["inherit","pipe","pipe","ipc"],execArgv:c,execPath:G(`w-${e}`)}),m={name:e,proc:n,debugPort:l};return I.push(m),F(n,`\x1B[35m[worker:${e}] \x1B[0m`,a=>{const N=/Debugger listening on (wss?:\/\/\S+)/.exec(a);N&&(m.inspectorUrl=N[1])}),n.on("exit",(a,N)=>{N==="SIGINT"||N==="SIGTERM"||a===0||console.error(`[parent] Worker "${e}" crashed (code=${a}, signal=${N??"none"})`);const O=I.indexOf(m);O>=0&&I.splice(O,1)}),console.log(`[parent] Worker "${e}" started (pid=${n.pid})`),m}g(oe,"se"),i(oe,"spawnWorkerProcess");function ae(){for(const e of[...I])try{e.proc.kill("SIGTERM")}catch{}I.length=0;for(const e of WORKER_NAMES)oe(e)}g(ae,"de"),i(ae,"restartWorkers");const he=debounce(async()=>{const e=Z();if(e.length>0){console.log(`[parent] Forwarding reload to ${e.length} child(ren)`);for(const t of e)t.proc.send?.({type:"reload"})}await ce(),WORKER_NAMES.length>0&&ae()},200),B=i(async(e,t)=>{if(e.method==="POST"&&e.url==="/webpack/reload"){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0})),te(e).then(l=>ee(l?.watcher)),he();return}if(e.method==="POST"&&e.url==="/webpack/watching"){const l=await te(e);ee(l),t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0,relayed:K!==void 0}));return}if(e.method==="POST"&&e.url==="/webpack/scale"){let l="";e.on("data",n=>{l+=n}),e.on("end",async()=>{let n=0;try{n=Number(JSON.parse(l||"{}").pods)||0}catch{}n=Math.max(1,Math.min(n,16)),await re(n),t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0,pods:n}))});return}if(e.method==="GET"&&e.url?.startsWith("/webpack/logs")){const l=new URL(e.url,"http://localhost").searchParams,n=Math.min(Math.max(Number(l.get("tail")??"100")||0,0),U),m=l.get("follow")!=="0",a=l.get("plain")==="1",N=!isOwnerOnlyTransport(e);t.writeHead(200,{"content-type":"text/plain; charset=utf-8","cache-control":"no-cache"});const O=n===0?"":b.slice(-n).join(""),q=a?O.replace(W,""):O;if(t.write(N?redactInspectorUrls(q):q),!m){t.end();return}const C={res:t,plain:a,redact:N};R.add(C),e.on("close",()=>R.delete(C));return}if(e.method==="POST"&&e.url?.startsWith("/webpack/debug")){if(!isOwnerOnlyTransport(e)){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not found"}));return}const l=Number(new URL(e.url,"http://localhost").searchParams.get("port")||"");Number.isInteger(l)&&l>1024&&(k=l),k||(k=9229);const n=s.filter(a=>a.proc.exitCode===null&&a.proc.signalCode===null);for(const a of n){a.debugSlot<0&&(a.debugSlot=_());try{a.proc.send?.({type:"debug-open",port:inspectorPortFor(k,a.debugSlot)})}catch{}}for(const a of I)try{a.proc.kill("SIGUSR1")}catch{}const m=Date.now()+2e3;for(;Date.now()<m&&n.some(a=>!a.inspectorUrl);)await new Promise(a=>setTimeout(a,50));t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify({ok:!0,basePort:k,fixedPorts:usesFixedInspectorPorts(),pods:n.map(a=>({id:a.id,inspectorUrl:a.inspectorUrl??null})),workers:I.map(a=>({name:a.name,port:a.debugPort||inspectorPortOf(a.inspectorUrl),inspectorUrl:a.inspectorUrl??null}))},null,2)+`
5
- `);return}const d=matchGatewayRoute(e);if(d?.prependPrefix&&!e.url?.startsWith(d.prependPrefix)&&(e.url=d.prependPrefix+(e.url??"/")),d?.target){applyGatewayMiddleware(e),p.web(e,t,{target:d.target,autoRewrite:typeof d.target=="string",changeOrigin:!0},l=>{console.error(`[parent] proxy error to ${d.target}:`,l),t.statusCode=502,t.setHeader("content-type","text/plain; charset=utf-8"),t.end(`Upstream error from ${d.target}.`)});return}if(SERVICE_PREFIX){if(!e.url?.startsWith(SERVICE_PREFIX)){t.writeHead(404,{"content-type":"text/plain; charset=utf-8"}),t.end(`Not found. This service is mounted at ${SERVICE_PREFIX}/`);return}e.url=e.url.slice(SERVICE_PREFIX.length)||"/"}applyGatewayMiddleware(e);const c=H();if(!c){t.statusCode=503,t.setHeader("content-type","text/plain; charset=utf-8");const l=s.map(n=>`#${n.id}: ${n.lastError??"no error recorded"}`).join(`
6
- `);t.end(`No healthy children available. POST /webpack/reload to recover.
1
+ "use strict";var Oe=Object.defineProperty;var u=(n,h)=>Oe(n,"name",{value:h,configurable:!0});var Ce=Object.defineProperty,i=u((n,h)=>Ce(n,"name",{value:h,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DEBUG_FIXED_PORTS_ENV=void 0,exports.isOwnerOnlyTransport=isOwnerOnlyTransport,exports.usesFixedInspectorPorts=usesFixedInspectorPorts,exports.inspectorPortFor=inspectorPortFor,exports.redactInspectorUrls=redactInspectorUrls,exports.inspectorPortOf=inspectorPortOf;const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path")),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),node_child_process_1=require("node:child_process"),http=tslib_1.__importStar(require("node:http")),readline=tslib_1.__importStar(require("node:readline")),inspector=tslib_1.__importStar(require("node:inspector")),util=tslib_1.__importStar(require("node:util")),v8=tslib_1.__importStar(require("node:v8")),vm=tslib_1.__importStar(require("node:vm")),exit_reason_1=require("./exit-reason"),hot_swap_registries_1=require("./hot-swap-registries"),process_name_1=require("./process-name"),schema_first_instance_1=require("./schema-first-instance"),redis_disconnect_fix_1=require("./redis-disconnect-fix"),error_stack_release_1=require("./error-stack-release"),swap_recycle_1=require("./swap-recycle"),stdio_guard_1=require("./stdio-guard"),httpProxy=require("http-proxy"),{prettyFactory}=require("pino-pretty"),BUNDLE_PATH=process.env.BUNDLE_PATH?.trim()||"./main.js",SERVICE_NAME=process.env.SERVICE_NAME||"",SERVICE_PREFIX=process.env.SERVE_PREFIX?`/${process.env.SERVE_PREFIX}`:"",GATEWAY_MIDDLEWARE_PATH=process.env.GATEWAY_MIDDLEWARE||"",GATEWAY_CONFIG=(()=>{const n=process.env.GATEWAY_CONFIG;if(!n)return{};try{return JSON.parse(n)}catch{return{}}})();let gatewayMiddleware=null;if(GATEWAY_MIDDLEWARE_PATH)try{const n=require(GATEWAY_MIDDLEWARE_PATH);gatewayMiddleware=typeof n=="function"?n:typeof n.default=="function"?n.default:null,gatewayMiddleware||console.warn(`[gateway] ${GATEWAY_MIDDLEWARE_PATH} does not export a function, gateway disabled`)}catch(n){console.error(`[gateway] Failed to load middleware from ${GATEWAY_MIDDLEWARE_PATH}:`,n)}function applyGatewayMiddleware(n){if(gatewayMiddleware)try{gatewayMiddleware(n,GATEWAY_CONFIG)}catch(h){console.error("[gateway] Middleware error:",h)}}u(applyGatewayMiddleware,"applyGatewayMiddleware"),i(applyGatewayMiddleware,"applyGatewayMiddleware");const GATEWAY_ROUTES=(()=>{try{return JSON.parse(process.env.GATEWAY_ROUTES||"[]")}catch{return[]}})(),GATEWAY_HTTPS=process.env.GATEWAY_HTTPS==="1";function hostMatches(n,h){if(n.startsWith("*.")){const g=n.slice(1);return h.endsWith(g)||h===n.slice(2)}return h===n}u(hostMatches,"hostMatches"),i(hostMatches,"hostMatches");function matchGatewayRoute(n){if(GATEWAY_ROUTES.length===0)return;const h=(n.headers.host||"").split(":")[0],g=n.url||"/";for(const _ of GATEWAY_ROUTES)if(!(_.host&&!hostMatches(_.host,h))&&!(_.prefix&&!g.startsWith(_.prefix)))return _}u(matchGatewayRoute,"matchGatewayRoute"),i(matchGatewayRoute,"matchGatewayRoute");const WORKER_NAMES=(()=>{try{return JSON.parse(process.env.WORKERS||"[]")}catch{return[]}})(),LENSMCP_WS_KEY=process.env.LENSMCP_WS_KEY||"",SOCK_DIR=LENSMCP_WS_KEY?path.join(os.tmpdir(),LENSMCP_WS_KEY,`${SERVICE_NAME||"lensmcp-cluster"}-devserver`):path.join(os.tmpdir(),`${SERVICE_NAME||"lensmcp-cluster"}-devserver`),POD_GENERATION=(()=>{const n=Number(process.env.LENSMCP_POD_GENERATION);return Number.isFinite(n)&&n>1?Math.floor(n):1})();function childSockPath(n){return path.join(SOCK_DIR,POD_GENERATION>1?`child-${n}-g${POD_GENERATION}.sock`:`child-${n}.sock`)}u(childSockPath,"childSockPath"),i(childSockPath,"childSockPath");function sockOwnerPath(n){return`${n}.owner`}u(sockOwnerPath,"sockOwnerPath"),i(sockOwnerPath,"sockOwnerPath");function sockRetiringPath(n){return`${n}.retiring`}u(sockRetiringPath,"sockRetiringPath"),i(sockRetiringPath,"sockRetiringPath");function isOwnerOnlyTransport(n){try{return typeof n.socket?.server?.address?.()=="string"}catch{return!1}}u(isOwnerOnlyTransport,"isOwnerOnlyTransport"),i(isOwnerOnlyTransport,"isOwnerOnlyTransport"),exports.DEBUG_FIXED_PORTS_ENV="LENSMCP_DEBUG_FIXED_PORTS";function usesFixedInspectorPorts(n=process.env){const h=(n[exports.DEBUG_FIXED_PORTS_ENV]??"").trim().toLowerCase();return h==="1"||h==="true"||h==="yes"||h==="on"}u(usesFixedInspectorPorts,"usesFixedInspectorPorts"),i(usesFixedInspectorPorts,"usesFixedInspectorPorts");function inspectorPortFor(n,h,g=process.env){if(!usesFixedInspectorPorts(g))return 0;const _=Math.trunc(n)+Math.trunc(h);return Number.isInteger(_)&&_>1024&&_<=65535?_:0}u(inspectorPortFor,"inspectorPortFor"),i(inspectorPortFor,"inspectorPortFor");const INSPECTOR_TOKEN_RE=/((?:wss?:\/\/[^\s/]+\/|ws=[^\s/]+\/))([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;function redactInspectorUrls(n){return n.replace(INSPECTOR_TOKEN_RE,"$1<inspector-token-redacted>")}u(redactInspectorUrls,"redactInspectorUrls"),i(redactInspectorUrls,"redactInspectorUrls");function inspectorPortOf(n){const h=/^wss?:\/\/[^/]*?:(\d+)\//.exec(n??""),g=h?Number(h[1]):NaN;return Number.isInteger(g)&&g>0?g:null}u(inspectorPortOf,"inspectorPortOf"),i(inspectorPortOf,"inspectorPortOf");function ensureOwnerOnlyDir(n){try{fs.mkdirSync(n,{recursive:!0,mode:448})}catch{}try{const h=fs.lstatSync(n);h.isDirectory()&&(h.mode&63)!==0&&fs.chmodSync(n,448)}catch{}}u(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),i(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function claimSock(n,h=process.pid){ensureOwnerOnlyDir(path.dirname(n));try{const g=sockOwnerPath(n);try{fs.unlinkSync(g)}catch{}fs.writeFileSync(g,String(h),{flag:"wx",mode:384})}catch{}try{fs.unlinkSync(n)}catch{}try{fs.unlinkSync(sockRetiringPath(n))}catch{}}u(claimSock,"claimSock"),i(claimSock,"claimSock");function cleanupSock(n,h=process.pid){if(n){try{const g=fs.readFileSync(sockOwnerPath(n),"utf8").trim();if(g&&g!==String(h))return}catch{}try{fs.unlinkSync(n)}catch{}try{fs.unlinkSync(sockOwnerPath(n))}catch{}try{fs.unlinkSync(sockRetiringPath(n))}catch{}}}u(cleanupSock,"cleanupSock"),i(cleanupSock,"cleanupSock");function serviceKeyFingerprint(){const n=process.env.LENSMCP_SERVICE_KEY;if(!n)return;const{createHash:h}=require("node:crypto");return h("sha256").update(n).digest("hex").slice(0,8)}u(serviceKeyFingerprint,"serviceKeyFingerprint"),i(serviceKeyFingerprint,"serviceKeyFingerprint");async function importFresh(spec){process.env.DEVSERVER_MODE="1";const requested=path.isAbsolute(spec)?spec:path.join(__dirname,spec);let resolved=requested;try{resolved=fs.realpathSync(requested)}catch{}return eval(`(() => {
2
+ const key = ${JSON.stringify(resolved)};
3
+ const previous = require.cache[key];
4
+ delete require.cache[key]; // delete from CJS cache
5
+ if (!previous) return;
6
+ const owners = new Set(Object.values(require.cache));
7
+ if (require.main) owners.add(require.main);
8
+ for (const owner of owners) {
9
+ const at = owner && Array.isArray(owner.children) ? owner.children.indexOf(previous) : -1;
10
+ if (at >= 0) owner.children.splice(at, 1);
11
+ }
12
+ })()`),eval(`require(${JSON.stringify(resolved)})`)}u(importFresh,"importFresh"),i(importFresh,"importFresh");function debounce(n,h){let g=null;return(..._)=>{g&&clearTimeout(g),g=setTimeout(()=>n(..._),h)}}if(u(debounce,"debounce"),i(debounce,"debounce"),process.env.APP_RUNNER==="1"){(0,stdio_guard_1.guardBrokenStdio)();const n=i(g=>{if(!inspector.url())try{inspector.open(g,"127.0.0.1",!1)}catch(B){console.warn(`[child] inspector.open(${g}) failed (port taken by another devserver?) \u2014 falling back to a random port:`,B.message);try{inspector.open(0,"127.0.0.1",!1)}catch{}}const _=inspector.url();_&&process.send&&process.send({type:"inspector-url",url:_})},"openInspector"),h=process.env.CHILD_DEBUG_PORT;if(h!==void 0&&h!==""){const g=Number(h);Number.isInteger(g)&&(g===0||g>1024)&&n(g)}try{require("@lensmcp/node-instrumentation/register")}catch{}(async()=>{let g=null,_=!1,B,j=!1;const v=(0,hot_swap_registries_1.registryPruningEnabled)(),Z=i((t,s)=>{const f=(0,hot_swap_registries_1.describePrune)(t,s);f&&console.log(f)},"logPrune");let $={app:null,handler:i((t,s)=>{s.statusCode=503,s.end("starting")},"handler"),inflight:0};const ee=(()=>{const t=Number(process.env.LENSMCP_SWAP_DRAIN_MS);return Number.isFinite(t)&&t>=0?t:15*6e4})(),Y=i(t=>{const s=globalThis.__lensmcpActiveWork;if(typeof s!="function")return{count:0};try{const f=s(t);return{count:Number(f?.count??0),...f?.oldestStartedAt?{oldestStartedAt:f.oldestStartedAt}:{}}}catch{return{count:0}}},"activeWork"),te=(()=>{const t=Number(process.env.LENSMCP_SWAP_MAX_DRAINING);return Number.isFinite(t)&&t>=1?Math.floor(t):1})(),x=new Set,P=(0,swap_recycle_1.swapRecyclePolicy)();let oe,le=0;const ge=(()=>{const t=Number(process.env.LENSMCP_POD_RETIRE_NOTICE_MS);return Number.isFinite(t)&&t>=0?t:3500})();let K=!1,V=!1,p;const E=i(()=>{if(p===void 0)try{v8.setFlagsFromString("--expose-gc");const t=vm.runInNewContext("gc");p=typeof t=="function"?t:null}catch{p=null}try{p?.()}catch{}return process.memoryUsage().heapUsed},"liveHeap"),w=(()=>{if(!(0,error_stack_release_1.errorFrameReleaseEnabled)())return null;try{const t=path.dirname(path.isAbsolute(BUNDLE_PATH)?BUNDLE_PATH:path.join(__dirname,BUNDLE_PATH))+path.sep,s=`${path.sep}node_modules${path.sep}`,f=new Set(Object.keys(require.cache));return(0,error_stack_release_1.createErrorFrameReleaser)(require.cache,{skip:i(y=>f.has(y)||y.startsWith(t)&&!y.slice(t.length).includes(s)&&!y.slice(t.length).startsWith(`node_modules${path.sep}`),"skip")})}catch{return null}})();let O=!1;function W(){if(!(!w||O))try{const t=w();t.errors>0&&console.log(`[child] read the stack of ${t.errors} Error object(s) that libraries created while loading and keep for good \u2014 unread, each held the functions that were on its stack, this bundle's included`)}catch(t){console.error("[child] could not release library error frames:",t),O=!0}}u(W,"z"),i(W,"releaseLibraryErrorFrames");const D=new Map;let b=Date.now();const T=i(()=>P.settleMs<=0||!_&&!j&&D.size===0&&Date.now()-b>=P.settleMs,"settled");let R;function A(t){if(!(!P.enabled||K||V)){if(P.settleMs<=0){F(t);return}R&&clearTimeout(R),R=setTimeout(()=>{R=void 0,T()?F(t):A(t)},P.settleMs),R.unref?.()}}u(A,"N"),i(A,"recycleCheckWhenSettled");const ne=i(()=>{if(P.stuckCloseMs<=0)return 0;const t=Date.now();let s=0;for(const f of D.values())t-f>=P.stuckCloseMs&&(s+=1);return s},"stuckCloses");function F(t){if(!P.enabled||K||V||!process.send)return;const s=v8.getHeapStatistics().heap_size_limit,f=i(m=>({swaps:le,usedBytes:m,limitBytes:s,stuckCloses:ne(),settled:T(),...oe===void 0?{}:{baselineBytes:oe}}),"state");if(!(0,swap_recycle_1.recycleReason)(P,f(process.memoryUsage().heapUsed)))return;const y=(0,swap_recycle_1.recycleReason)(P,f(E()));y&&(K=!0,console.log(`[child] asking for a fresh pod ${t} \u2014 ${y}. This pod keeps serving until its successor is ready. Size the bound with LENSMCP_SWAP_RECYCLE_HEAP_FACTOR / _HEAP_RATIO / _MIN_MB, or turn it off with LENSMCP_SWAP_RECYCLE=0.`),process.send({type:"recycle-request",reason:y}))}u(F,"K"),i(F,"considerRecycle");async function J(t,s){if(t.closed||(t.closed=!0,x.delete(t),s&&console.error(`[child] ${s}`),!t.app))return;D.set(t,Date.now());const f=P.enabled&&P.stuckCloseMs>0?setTimeout(()=>{D.has(t)&&(console.error(`[child] a previous app has been closing for ${Math.round(P.stuckCloseMs/1e3)}s and its close() has not returned \u2014 one of its shutdown hooks is waiting on something that does not end (a queue worker, a connection, a stream). It stays in memory until this pod is replaced.`),F("with a previous app stuck in close()"))},P.stuckCloseMs):void 0;f?.unref?.();try{await t.app.close()}catch(y){console.error("[child] error closing a previous app:",y)}finally{D.delete(t),b=Date.now(),f&&clearTimeout(f),t.app=null,t.handler=(y,m)=>{m.statusCode=503,m.end("closed")}}if(t.superseded){try{Z((0,hot_swap_registries_1.pruneRegistries)(t.superseded,"serve"),"after closing the previous app")}catch(y){console.error("[child] could not release the previous app's class registrations:",y)}t.superseded=void 0}A("after closing the previous app")}u(J,"Q"),i(J,"closeGeneration");async function de(t){for(;x.size>=te;){const m=x.values().next().value;if(!m)break;await J(m,`closing a still-draining generation early \u2014 ${x.size} were pending and the cap is ${te} (each holds a whole app: DI container, DB pool, queue workers). Raise LENSMCP_SWAP_MAX_DRAINING if this pod legitimately needs more.`)}x.add(t);const s=Date.now(),f=i(()=>({inflight:t.inflight,work:Y(t.workGen).count}),"busy");for(;ee>0&&Date.now()-s<ee;){if(t.closed)return;const m=f();if(m.inflight===0&&m.work===0)break;await new Promise(C=>{setTimeout(C,250).unref?.()})}if(t.closed)return;const y=f();y.inflight>0||y.work>0?console.error(`[child] swap drain expired after ${Date.now()-s}ms \u2014 closing the previous app with ${y.inflight} in-flight request(s) and ${y.work} background job(s) still running. A queue job killed here keeps its lock until the TTL lapses. Raise LENSMCP_SWAP_DRAIN_MS (currently ${ee}ms) if this is legitimate work.`):Date.now()-s>250&&console.log(`[child] previous app drained in ${Date.now()-s}ms \u2014 closing it`),await J(t)}u(de,"he"),i(de,"drainThenClose");async function M(){if(_)return j=!0,console.log("[child] Reload deferred \u2014 will re-swap after current swap completes"),g;_=!0,b=Date.now(),process.removeAllListeners("uncaughtException"),process.removeAllListeners("unhandledRejection"),process.on("uncaughtException",(S,L)=>{console.error(`[child] uncaughtException (${L}):`,S)}),process.on("unhandledRejection",S=>{const L=S instanceof Error?S.stack||S.message:String(S);L.includes("FronteggContextScope")||L.includes("populateLoggerMetadata")||console.error("[child] unhandledRejection:",S)});let t;if(v)try{t=(0,hot_swap_registries_1.captureRegistries)()}catch{t=void 0}try{try{await importFresh(BUNDLE_PATH)}catch(S){if(!(S instanceof SyntaxError))throw S;console.warn(`[child] bundle was unreadable mid-write (${S.message}) \u2014 retrying the swap once`),await new Promise(L=>setTimeout(L,400)),await importFresh(BUNDLE_PATH)}}catch(S){throw _=!1,b=Date.now(),S}if(typeof global.createChildApp!="function")throw _=!1,b=Date.now(),new Error(`Bundle '${BUNDLE_PATH}' does not export createChildApp() and the zero-touch bridge is not active. Either install @lensmcp/node-instrumentation (a plain NestFactory.create + app.listen main.ts then just works) or set global.createChildApp in main.ts when DEVSERVER_MODE === '1'.`);let s;if(t)try{s=(0,hot_swap_registries_1.supersededSince)(t),Z((0,hot_swap_registries_1.pruneRegistries)(s,"build"),"at the swap")}catch(S){console.error("[child] could not release superseded class registrations:",S)}const f=global.__lensmcpBeginWorkGeneration,y=typeof f=="function"?f():void 0;let m;try{m=await global.createChildApp()}catch(S){throw _=!1,b=Date.now(),S}const C=$;return s?.size&&(C.app?C.superseded=s:Z((0,hot_swap_registries_1.pruneRegistries)(s,"serve"),"at the swap")),g=m,$={app:m,handler:m.handler,inflight:0,...y===void 0?{}:{workGen:y}},console.log("[child] Swapped to fresh app (previous draining in background)"),W(),C.app&&(le+=1,de(C).catch(S=>console.error("[child] background close error:",S)),setTimeout(()=>F("30s after a hot swap"),3e4).unref?.()),_=!1,b=Date.now(),j?(j=!1,M()):m}u(M,"V"),i(M,"swapNow");const re=debounce(async()=>{try{console.log("[child] /webpack/reload"),await M()}catch(t){console.error("[child] reload failed:",t)}},250),G=http.createServer((t,s)=>{if(t.method==="POST"&&t.url==="/webpack/reload"){s.writeHead(200,{"content-type":"application/json"}),s.end(JSON.stringify({ok:!0})),re();return}if(t.method==="GET"&&t.url==="/__lensmcp/alive"){const y=t.headers["x-internal-token"],m=process.env.LENSMCP_SERVICE_KEY;if(m&&y!==m){s.writeHead(404,{"content-type":"text/plain"}),s.end("not found");return}const C=Y();s.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),s.end(JSON.stringify({ok:!0,pid:process.pid,swapping:_,inflight:$.inflight,oldestInflightAt:$.oldestAt,activeWork:C.count,oldestWorkAt:C.oldestStartedAt,keyFingerprint:serviceKeyFingerprint(),...B?{watcher:B}:{},...Number.isFinite(Number(process.env.LENSMCP_POD_GENERATION))&&process.env.LENSMCP_POD_GENERATION?{podGeneration:Number(process.env.LENSMCP_POD_GENERATION)}:{}}));return}const f=$;f.inflight+=1,f.oldestAt===void 0&&(f.oldestAt=Date.now()),s.on("close",()=>{f.inflight-=1,f.inflight===0&&(f.oldestAt=void 0)}),f.handler(t,s)});G.keepAliveTimeout=0;const I=process.env.CHILD_SOCK_PATH;process.on("disconnect",()=>{cleanupSock(I),process.exit(0)});try{const t=path.isAbsolute(BUNDLE_PATH)?BUNDLE_PATH:path.join(__dirname,BUNDLE_PATH);if((0,schema_first_instance_1.schemaPrewarmEnabled)())try{const s=(0,schema_first_instance_1.prewarmZodFor)(t);s?.created&&console.log(`[child] zod${s.version?` ${s.version}`:""}: built ${s.created} throwaway schemas first, so none of the bundle's own is the first of its class`)}catch{}if((0,redis_disconnect_fix_1.redisDisconnectFixEnabled)())try{const s=(0,redis_disconnect_fix_1.finishStrandedDisconnectsFor)(t);s?.patched&&console.log(`[child] ioredis${s.versions.length?` ${s.versions.join(", ")}`:""}: a disconnect() between reconnect attempts now ends the client \u2014 it used to leave a queue worker's close() waiting for good`)}catch{}await M(),P.enabled&&P.heapFactor>0&&(oe=E()),console.log(`[child] Using bundle: ${t}`),claimSock(I),G.listen(I,()=>{console.log(`[child] Listening on ${I}`),process.send&&process.send({type:"ready",socketPath:I})})}catch(t){console.error("[child] Startup error:",t),process.send&&process.send({type:"boot-error",error:String(t?.stack||t)}),process.exitCode=1;return}process.on("message",async t=>{if(!(!t||typeof t!="object")){if(t.type==="debug-open"){const s=Number(t.port);n(Number.isInteger(s)&&s>1024?s:0);return}if(t.type==="watcher"){const s=t.testimony;s&&typeof s=="object"&&(B=s);return}if(t.type==="recycle-declined"){K=!1;return}if(t.type==="retire"){U();return}if(t.type==="reload"){if(V)return;try{await M(),process.send&&process.send({type:"reloaded",socketPath:I})}catch(s){console.error("[child] reload error:",s),process.send&&process.send({type:"reload-error",error:String(s?.stack||s)})}}}});async function U(){if(V)return;V=!0,console.log("[child] retiring \u2014 a fresh pod has taken over; finishing in-flight work, then exiting");try{fs.writeFileSync(sockRetiringPath(I),String(process.pid),{flag:"w",mode:384})}catch{}await new Promise(s=>{setTimeout(s,ge).unref?.()}),cleanupSock(I),G.close();const t=Date.now();for(;Date.now()-t<ee&&(G.closeIdleConnections?.(),!($.inflight===0&&Y().count===0));)await new Promise(s=>{setTimeout(s,250).unref?.()});setTimeout(()=>process.exit(0),3e4).unref?.();try{await g?.close()}catch(s){console.error("[child] close error while retiring:",s)}process.exit(0)}u(U,"ee"),i(U,"retire"),process.on("beforeExit",()=>{try{inspector?.close?.()}catch{}});const X=i(async t=>{console.log(`[child ${t}] shutting down\u2026`);const s=g?.close().catch(y=>console.error("[child] close error:",y)),f=new Promise(y=>G.close(()=>y()));cleanupSock(I),setTimeout(()=>process.exit(0),700).unref(),await Promise.all([s,f]),process.exit(0)},"shutdown");["SIGINT","SIGTERM","SIGHUP","SIGQUIT","SIGUSR2"].forEach(t=>{process.on(t,()=>{inspector?.close?.(),X(t)})})})()}else{let n=u(function(p){x.push(p),x.length>te&&x.splice(0,x.length-te),ee?.write(p.replace(Y,""));for(const E of P)try{const w=E.plain?p.replace(Y,""):p;E.res.write(E.redact?redactInspectorUrls(w):w)}catch{P.delete(E)}},"P");i(n,"emitLogLine");const h=["\x1B[36m","\x1B[33m","\x1B[35m","\x1B[32m","\x1B[34m","\x1B[31m"],g="\x1B[0m",_=Math.max(1,Number(process.env.CHILD_COUNT||1)),B=LENSMCP_WS_KEY||path.basename(process.cwd()),j=i(p=>(0,process_name_1.namedExecPath)((0,process_name_1.processName)(B,SERVICE_NAME||"service",p)),"execPathFor");let v=(()=>{const p=Number(process.env.LENSMCP_DEBUG_PORT||"");return Number.isInteger(p)&&p>1024?p:0})();const Z=20;v&&(console.warn("[parent] DEBUGGER ENABLED \u2014 pods and workers open a Node inspector. Node's inspector has NO authentication: the ws:// URL's UUID is the whole credential, and any account on this host that obtains it can run code as you. Do not enable this on a shared machine. (issues/codex-security/06)"),console.warn(usesFixedInspectorPorts()?`[parent] ${exports.DEBUG_FIXED_PORTS_ENV} is set \u2014 inspectors bind PREDICTABLE ports (${v}+slot, workers ${v}+${Z}+i). Any account on this host can connect to 127.0.0.1:${v} and read the session UUID from /json/list. Unset it to use ephemeral ports.`:`[parent] Inspectors bind an EPHEMERAL loopback port; attach via the printed "Debugger listening" URL, or \`lensmcp debug <service>\` (control socket). Set ${exports.DEBUG_FIXED_PORTS_ENV}=1 for fixed ports if your IDE needs a pre-configured attach port \u2014 any account on this host can then reach them.`));const $=process.env.LENSMCP_LOG_FILE||"",ee=$?(()=>{try{fs.mkdirSync(path.dirname($),{recursive:!0});const p=fs.createWriteStream($,{flags:"a",mode:384});try{fs.chmodSync($,384)}catch{}return p}catch(p){return console.warn(`[parent] cannot open log file ${$}:`,p.message),null}})():null,Y=/\x1b\[[0-9;]*m/g,te=2e3,x=[],P=new Set;for(const p of["log","info","warn","error"]){const E=console[p].bind(console);console[p]=(...w)=>{E(...w),n(w.map(O=>typeof O=="string"?O:util.inspect(O)).join(" ")+`
13
+ `)}}const oe=i(p=>h[(p-1)%h.length],"colorFor"),le=i(p=>{if(_===1)return"";const E=`[child#${p}] `;return`${oe(p)}${E}${g}`},"tagFor"),ge=i(p=>p.startsWith("{")&&p.endsWith("}"),"isJsonLog"),K=i((p,E,w)=>{const O=prettyFactory({sync:!0,colorize:!0,crlf:!0,messageKey:"message",errorLikeObjectKeys:["err","error"],errorProps:"type,message,stack",ignore:["logContext","context","hostname","req","res","err.driverError","module","cloudEnvironment","frontegg-application-id","frontegg-tenant-id","frontegg-trace-id","frontegg-vendor-id","host","service","version","err","error"].join(","),messageFormat:"{if module}[{module}] {end}{if context}[{context}] {end}{if logContext}[{logContext}] {end}{message}",customPrettifiers:{stack:i(b=>`
14
+ `+String(b),"stack")}}),W=i((b,T)=>{const R=b==="stdout"?process.stdout:process.stderr;try{w?.(T)}catch{}const A=ge(T)?O(T):`${T}
15
+ `;R.write(`${E}${A}`),n(`${E}${A}`)},"write"),D=i((b,T)=>{if(!b)return;const R=readline.createInterface({input:b});R.on("line",A=>W(T,A)),R.on("close",()=>{})},"attach");D(p.stdout,"stdout"),D(p.stderr,"stderr")},"wireProcLogging"),V=i(p=>K(p.proc,le(p.id),E=>p.tail.push(E)),"wireChildLogging");(async()=>{ensureOwnerOnlyDir(SOCK_DIR);const p=httpProxy.createProxyServer({});let E=Number(process.env.PORT)||0;E||(E=9090);const w=[],O=new Set;function W(){let e=0;for(;O.has(e);)e++;return O.add(e),e}u(W,"z"),i(W,"allocDebugSlot");let D=1,b=0,T=!1,R=_,A=[],ne=0;const F=(()=>{const e=Number(process.env.LENSMCP_POD_BOOT_GRACE_MS);return Number.isFinite(e)&&e>=0?e:12e4})(),J=i(e=>e.proc.exitCode!==null||e.proc.signalCode!==null,"childExited"),de=i(e=>e.booting&&!J(e)&&Date.now()-e.spawnedAt<F,"stillBooting");function M(){return w.filter(e=>e.healthy&&typeof e.socketPath=="string")}u(M,"V"),i(M,"healthyChildren");let re;function G(e){if(!e||typeof e!="object")return;const r=e;if(r.schemaVersion===1){re=r;for(const d of w)try{d.proc.send?.({type:"watcher",testimony:r})}catch{}}}u(G,"U"),i(G,"recordWatcherTestimony");function I(e,r=256*1024){return new Promise(d=>{let a="",c=!1;e.on("data",o=>{c||(a+=o,a.length>r&&(c=!0))}),e.on("end",()=>{if(c||!a)return d(void 0);try{d(JSON.parse(a))}catch{d(void 0)}}),e.on("error",()=>d(void 0))})}u(I,"v"),i(I,"readJsonBody");function U(){const e=M();if(e.length===0)return null;const r=b%e.length;return b=(b+1)%e.length,e[r]}u(U,"ee"),i(U,"pickChild");async function X(){const e=D++,r=childSockPath(e);cleanupSock(r);const d=["--enable-source-maps"];__filename.endsWith(".ts")&&d.unshift("--require","@swc-node/register");const a=v?W():-1,c=(0,node_child_process_1.fork)(__filename,{env:{...process.env,APP_RUNNER:"1",CHILD_SOCK_PATH:r,...a>=0&&{CHILD_DEBUG_PORT:String(inspectorPortFor(v,a))}},stdio:["inherit","pipe","pipe","ipc"],execArgv:d,execPath:j("pod")}),o={id:e,proc:c,healthy:!1,lastError:null,debugSlot:a,tail:new exit_reason_1.TailBuffer,booting:!0,spawnedAt:Date.now(),bootReloadSeq:ne};return c.on("message",k=>{if(!k||typeof k!="object")return;const l=k;if(l.type==="ready"){if(o.socketPath=String(l.socketPath),o.healthy=!0,o.booting=!1,console.log(`[parent] Child#${o.id} ready on socket ${o.socketPath}`),re)try{c.send?.({type:"watcher",testimony:re})}catch{}if(ne!==o.bootReloadSeq)try{c.send?.({type:"reload"})}catch{}}else l.type==="recycle-request"?L(o,String(l.reason||"it has outgrown its bound")):l.type==="inspector-url"?(o.inspectorUrl=String(l.url),console.log(`[parent] Child#${o.id} Debugger listening on ${l.url}`)):l.type==="reloaded"?(o.socketPath=String(l.socketPath),o.healthy=!0,console.log(`[parent] Child#${o.id} hot-swapped on socket ${o.socketPath}`)):l.type==="reload-error"?(o.lastError=String(l.error||"unknown reload error"),console.error(`[parent] Child#${o.id} reload error: ${o.lastError}`),o.healthy=!1):l.type==="boot-error"&&(o.lastError=String(l.error||"unknown boot error"),console.error(`[parent] Child#${o.id} boot error: ${o.lastError}`),o.healthy=!1,o.booting=!1)}),c.on("exit",(k,l)=>{o.booting=!1,o.debugSlot>=0&&(O.delete(o.debugSlot),o.debugSlot=-1);const N=l==="SIGINT"||l==="SIGTERM"||k===0;if(N?console.log(`[parent] Child#${o.id} exited cleanly (code=${k}, signal=${l??"none"})`):(console.error((0,exit_reason_1.crashReport)(`[parent] Child#${o.id} crashed (code=${k}, signal=${l??"none"}, pid=${o.proc.pid}, ${o.healthy?"was serving":"was not ready"})`,k,l,o.tail.snapshot())),o.lastError||(o.lastError=`Child exited abnormally (code=${k}, signal=${l??"none"})`)),o.healthy=!1,cleanupSock(o.socketPath,o.proc.pid),o.socketPath=void 0,!N&&!T){const q=w.indexOf(o);q>=0&&w.splice(q,1);const ae=Date.now();if(A=A.filter(Q=>ae-Q<3e4),A.length>=5){console.error(`[parent] crash-loop detected (${A.length} respawns/30s) \u2014 pausing auto-respawn; POST /webpack/reload to retry`);return}A.push(ae),setTimeout(()=>{const Q=w.filter($e=>$e.proc.killed===!1).length;!T&&Q<R&&(console.log(`[parent] respawning a crashed pod (${Q}/${R} live)`),X().catch(()=>{}))},1500).unref?.()}}),w.push(o),V(o),o}u(X,"te"),i(X,"spawnChild");async function t(e){R=Math.max(R,e);const r=w.filter(a=>a.proc.killed===!1),d=e-r.length;for(let a=0;a<d;a++)await X()}u(t,"r"),i(t,"ensurePoolSize");async function s(){const e=w.filter(r=>(!r.healthy||typeof r.socketPath!="string")&&!de(r));await Promise.all(e.map(async r=>{try{r.proc.kill("SIGTERM")}catch{}cleanupSock(r.socketPath,r.proc.pid),r.socketPath=void 0;const d=w.indexOf(r);d>=0&&w.splice(d,1),await X()}))}u(s,"c"),i(s,"respawnUnhealthy");const f=new Set;let y=!1,m=0;const C=3e4,S=(()=>{const e=Number(process.env.LENSMCP_SWAP_DRAIN_MS);return(Number.isFinite(e)&&e>=0?e:15*6e4)+6e4})();async function L(e,r){const d=i(c=>{console.log(`[parent] Child#${e.id} asked for a fresh pod (${r}) \u2014 not now: ${c}`);try{e.proc.send?.({type:"recycle-declined"})}catch{}},"decline");if(T||!w.includes(e))return;if(y)return d("another pod is already being replaced");const a=Date.now()-m;if(a<C)return d(`a pod was replaced ${Math.round(a/1e3)}s ago`);y=!0,console.log(`[parent] Child#${e.id} asked for a fresh pod \u2014 ${r}. Starting one; Child#${e.id} keeps serving until it is ready.`);try{const c=await X(),o=Date.now()+F;for(;!c.healthy&&!c.lastError&&!J(c)&&Date.now()<o;)await new Promise(N=>{setTimeout(N,100).unref?.()});if(T)return;if(!c.healthy){const N=w.indexOf(c);N>=0&&w.splice(N,1);try{c.proc.kill("SIGTERM")}catch{}return d(`its successor Child#${c.id} did not come up (${c.lastError??"no answer in time"})`)}const k=w.indexOf(e);k>=0&&w.splice(k,1),e.healthy=!1,f.add(e);const l=setTimeout(()=>{if(!J(e)){console.error(`[parent] Child#${e.id} was still running ${S}ms after it was retired \u2014 killing it`);try{e.proc.kill("SIGKILL")}catch{}}},S);l.unref?.(),e.proc.once("exit",()=>{clearTimeout(l),f.delete(e)});try{e.proc.send?.({type:"retire"})}catch{try{e.proc.kill("SIGTERM")}catch{}}m=Date.now(),console.log(`[parent] Child#${c.id} took over from Child#${e.id} (pid ${e.proc.pid} \u2192 ${c.proc.pid})`)}catch(c){d(`could not start a successor: ${c?.message??String(c)}`)}finally{y=!1}}u(L,"x"),i(L,"recyclePod");const H=[];function ye(e){const r=path.dirname(path.isAbsolute(BUNDLE_PATH)?BUNDLE_PATH:path.resolve(BUNDLE_PATH)),d=path.join(r,`${e}.js`);if(!fs.existsSync(d))return console.error(`[parent] Worker "${e}" bundle not found: ${d}`),null;const a=["--enable-source-maps"];__filename.endsWith(".ts")&&a.unshift("--require","@swc-node/register");try{a.push("--require",require.resolve("@lensmcp/node-instrumentation/register"))}catch{}const c=inspectorPortFor(v||9229,Z+Math.max(0,WORKER_NAMES.indexOf(e)));a.push(v?`--inspect=127.0.0.1:${c}`:`--inspect-port=127.0.0.1:${c}`);const o=(0,node_child_process_1.fork)(d,{env:process.env,stdio:["inherit","pipe","pipe","ipc"],execArgv:a,execPath:j(`w-${e}`)}),k={name:e,proc:o,debugPort:c};return H.push(k),K(o,`\x1B[35m[worker:${e}] \x1B[0m`,l=>{const N=/Debugger listening on (wss?:\/\/\S+)/.exec(l);N&&(k.inspectorUrl=N[1])}),o.on("exit",(l,N)=>{N==="SIGINT"||N==="SIGTERM"||l===0||console.error(`[parent] Worker "${e}" crashed (code=${l}, signal=${N??"none"})`);const q=H.indexOf(k);q>=0&&H.splice(q,1)}),console.log(`[parent] Worker "${e}" started (pid=${o.pid})`),k}u(ye,"we"),i(ye,"spawnWorkerProcess");const me=(()=>{const e=Number(process.env.LENSMCP_SWAP_DRAIN_MS);return Number.isFinite(e)&&e>=0?e:15*6e4})(),Se=(()=>{const e=Number(process.env.LENSMCP_SWAP_MAX_DRAINING);return Number.isFinite(e)&&e>=1?Math.floor(e):1})(),se=new Set,Pe=i(e=>e.proc.exitCode!==null||e.proc.signalCode!==null,"workerExited");function pe(e,r){if(se.delete(e),!Pe(e)){console.error(`[parent] Worker "${e.name}" (pid=${e.proc.pid}) ${r} \u2014 killing it. A worker that returns from its SIGTERM handler but leaves a handle open (a Redis client handed to BullMQ is not closed by worker.close()) never exits on its own: close what it opened, or call process.exit().`);try{e.proc.kill("SIGKILL")}catch{}}}u(pe,"fe"),i(pe,"killDrainingWorker");function ke(e){try{e.proc.kill("SIGTERM")}catch{}if(Pe(e))return;const r=[...se].filter(a=>a.name===e.name);for(;r.length>=Se;)pe(r.shift(),`was still running when a later reload replaced its successor (cap ${Se})`);se.add(e);const d=setTimeout(()=>pe(e,`was still running ${me}ms after SIGTERM`),me);d.unref?.(),e.proc.once("exit",()=>{clearTimeout(d),se.delete(e)})}u(ke,"Ae"),i(ke,"retireWorker");function be(){for(const e of[...H])ke(e);H.length=0;for(const e of WORKER_NAMES)ye(e)}u(be,"Te"),i(be,"restartWorkers");const Ae=debounce(async()=>{ne+=1;const e=M();if(e.length>0){console.log(`[parent] Forwarding reload to ${e.length} child(ren)`);for(const r of e)r.proc.send?.({type:"reload"})}await s(),WORKER_NAMES.length>0&&be()},200),ie=i(async(e,r)=>{if(e.method==="POST"&&e.url==="/webpack/reload"){r.writeHead(200,{"content-type":"application/json"}),r.end(JSON.stringify({ok:!0})),I(e).then(c=>G(c?.watcher)),Ae();return}if(e.method==="POST"&&e.url==="/webpack/watching"){const c=await I(e);G(c),r.writeHead(200,{"content-type":"application/json"}),r.end(JSON.stringify({ok:!0,relayed:re!==void 0}));return}if(e.method==="POST"&&e.url==="/webpack/scale"){let c="";e.on("data",o=>{c+=o}),e.on("end",async()=>{let o=0;try{o=Number(JSON.parse(c||"{}").pods)||0}catch{}o=Math.max(1,Math.min(o,16)),await t(o),r.writeHead(200,{"content-type":"application/json"}),r.end(JSON.stringify({ok:!0,pods:o}))});return}if(e.method==="GET"&&e.url?.startsWith("/webpack/logs")){const c=new URL(e.url,"http://localhost").searchParams,o=Math.min(Math.max(Number(c.get("tail")??"100")||0,0),te),k=c.get("follow")!=="0",l=c.get("plain")==="1",N=!isOwnerOnlyTransport(e);r.writeHead(200,{"content-type":"text/plain; charset=utf-8","cache-control":"no-cache"});const q=o===0?"":x.slice(-o).join(""),ae=l?q.replace(Y,""):q;if(r.write(N?redactInspectorUrls(ae):ae),!k){r.end();return}const Q={res:r,plain:l,redact:N};P.add(Q),e.on("close",()=>P.delete(Q));return}if(e.method==="POST"&&e.url?.startsWith("/webpack/debug")){if(!isOwnerOnlyTransport(e)){r.writeHead(404,{"content-type":"application/json"}),r.end(JSON.stringify({error:"not found"}));return}const c=Number(new URL(e.url,"http://localhost").searchParams.get("port")||"");Number.isInteger(c)&&c>1024&&(v=c),v||(v=9229);const o=w.filter(l=>l.proc.exitCode===null&&l.proc.signalCode===null);for(const l of o){l.debugSlot<0&&(l.debugSlot=W());try{l.proc.send?.({type:"debug-open",port:inspectorPortFor(v,l.debugSlot)})}catch{}}for(const l of H)try{l.proc.kill("SIGUSR1")}catch{}const k=Date.now()+2e3;for(;Date.now()<k&&o.some(l=>!l.inspectorUrl);)await new Promise(l=>setTimeout(l,50));r.writeHead(200,{"content-type":"application/json"}),r.end(JSON.stringify({ok:!0,basePort:v,fixedPorts:usesFixedInspectorPorts(),pods:o.map(l=>({id:l.id,inspectorUrl:l.inspectorUrl??null})),workers:H.map(l=>({name:l.name,port:l.debugPort||inspectorPortOf(l.inspectorUrl),inspectorUrl:l.inspectorUrl??null}))},null,2)+`
16
+ `);return}const d=matchGatewayRoute(e);if(d?.prependPrefix&&!e.url?.startsWith(d.prependPrefix)&&(e.url=d.prependPrefix+(e.url??"/")),d?.target){applyGatewayMiddleware(e),p.web(e,r,{target:d.target,autoRewrite:typeof d.target=="string",changeOrigin:!0},c=>{console.error(`[parent] proxy error to ${d.target}:`,c),r.statusCode=502,r.setHeader("content-type","text/plain; charset=utf-8"),r.end(`Upstream error from ${d.target}.`)});return}if(SERVICE_PREFIX){if(!e.url?.startsWith(SERVICE_PREFIX)){r.writeHead(404,{"content-type":"text/plain; charset=utf-8"}),r.end(`Not found. This service is mounted at ${SERVICE_PREFIX}/`);return}e.url=e.url.slice(SERVICE_PREFIX.length)||"/"}applyGatewayMiddleware(e);const a=U();if(!a){r.statusCode=503,r.setHeader("content-type","text/plain; charset=utf-8");const c=w.map(o=>`#${o.id}: ${o.lastError??"no error recorded"}`).join(`
17
+ `);r.end(`No healthy children available. POST /webpack/reload to recover.
7
18
 
8
19
  Last errors:
9
- ${l}`);return}p.web(e,t,{target:{socketPath:c.socketPath},headers:{"x-proxy-child-id":String(c.id)}},l=>{console.error(`[parent] proxy error to Child#${c.id}:`,l),c.healthy=!1,c.lastError=String(l?.stack||l),t.statusCode=502,t.setHeader("content-type","text/plain; charset=utf-8"),t.end(`Upstream error from Child#${c.id}. Try /webpack/reload.
10
- ${c.lastError}`)})},"requestHandler");let D,j,ne;const J=[];if(GATEWAY_HTTPS){const e=require("./basic-ssl"),t=GATEWAY_ROUTES.map(c=>c.host).filter(c=>!!c),d=path.join(process.cwd(),"node_modules",".cache","davnx-webpack");j=e.getCertificateSync(d,SERVICE_NAME||"lensmcp.dev",t),ne=e.caCertPath(),D=require("node:https").createServer({key:j,cert:j},B)}else D=http.createServer(B);const le=i((e,t,d)=>{const c=matchGatewayRoute(e);if(c?.prependPrefix&&!e.url?.startsWith(c.prependPrefix)&&(e.url=c.prependPrefix+(e.url??"/")),c?.target){p.ws(e,t,d,{target:c.target,changeOrigin:!0},()=>t.destroy());return}const l=H();if(!l){t.destroy();return}p.ws(e,t,d,{target:{socketPath:l.socketPath}},()=>t.destroy())},"upgradeHandler");D.on("upgrade",le);const pe=2e3;let de=!1;D.on("error",e=>{if(e.code==="EADDRINUSE"){de||(de=!0,console.warn(`[parent] public port ${r} already in use \u2014 continuing on the unix-socket pool only (gateway routing is unaffected) and retrying the bind every ${pe/1e3}s. Give this service a distinct cluster.port if this is not a rolling handoff.`)),setTimeout(()=>{!y&&!D.listening&&D.listen(r)},pe).unref?.();return}console.error("[parent] public server error:",e)}),await re(E);for(const e of WORKER_NAMES)oe(e);D.listen(r,()=>{const e=GATEWAY_HTTPS?"https":"http";console.log(`[parent] Listening on ${e}://localhost:${r}`),SERVICE_PREFIX&&console.log(`[parent] Service prefix: "${SERVICE_PREFIX}" (enforced \u2014 requests without it will get 404)`),gatewayMiddleware&&console.log(`[parent] Gateway middleware: ACTIVE (${GATEWAY_MIDDLEWARE_PATH})`),GATEWAY_ROUTES.length>0&&console.log(`[parent] Gateway routes: ${GATEWAY_ROUTES.map(t=>`${t.host??"*"}${t.prefix??"/"}\u2192${t.target??"children"}`).join(" ")}`),ne&&(console.log("[parent] HTTPS dev CA (trust ONCE, then rotations/new hostnames stay green):"),console.log(`[parent] sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${ne}`)),console.log(`POST ${e}://localhost:${r}/webpack/reload to trigger rolling swap/respawn`)});const X=Number(process.env.INTERNAL_PORT||0);if(X>0){const e=i((d,c)=>{const l=H();if(!l){c.statusCode=503,c.setHeader("content-type","text/plain; charset=utf-8"),c.end("No healthy children available.");return}p.web(d,c,{target:{socketPath:l.socketPath}},n=>{l.healthy=!1,l.lastError=String(n?.stack||n),c.statusCode=502,c.end(`Upstream error from Child#${l.id}.`)})},"internalHandler"),t=http.createServer(e);t.on("upgrade",(d,c,l)=>{const n=H();if(!n){c.destroy();return}p.ws(d,c,l,{target:{socketPath:n.socketPath}},()=>c.destroy())}),t.on("error",d=>{console.warn(`[parent] internal port ${X} unavailable (${d.code}).`)}),t.listen(X,()=>{console.log(`[parent] Internal (service-to-service, no gateway middleware) on http://localhost:${X}`)}),J.push(t)}const fe=(()=>{try{return JSON.parse(process.env.GATEWAY_EXTRA_PORTS||"[]")}catch{return[]}})();for(const e of fe){if(!Number.isInteger(e)||e===Number(r))continue;const t=GATEWAY_HTTPS?require("node:https").createServer({key:j,cert:j},B):http.createServer(B);t.on("upgrade",le),t.on("error",d=>{console.warn(`[parent] extra port ${e} unavailable (${d.code}) \u2014 main port still up.`)}),t.listen(e,()=>{console.log(`[parent] Also listening on ${GATEWAY_HTTPS?"https":"http"}://localhost:${e}`)}),J.push(t)}const se=path.join(SOCK_DIR,"parent.sock");claimSock(se);const ie=http.createServer(B);ie.on("error",e=>{console.warn(`[parent] control socket unavailable (${e.code}) \u2014 lensmcp logs/debug attach disabled.`)}),ie.listen(se),J.push(ie);const ue=i(async e=>{y=!0,console.log(`[${e}] [parent] shutting down\u2026`),cleanupSock(se);for(const t of J)try{t.close()}catch{}for(const t of I)try{t.proc.kill("SIGTERM")}catch{}for(const t of s){try{t.proc.kill("SIGTERM")}catch{}t.socketPath&&cleanupSock(t.socketPath,t.proc.pid)}try{fs.rmdirSync(SOCK_DIR)}catch{}setTimeout(()=>process.exit(0),700).unref()},"shutdown");["SIGINT","SIGTERM","SIGHUP","SIGQUIT","SIGUSR2"].forEach(e=>process.on(e,()=>{ue(e)}));const z=Number(process.env.LENSMCP_GATEWAY_PID??0);if(Number.isInteger(z)&&z>0){const e=setInterval(()=>{try{process.kill(z,0)}catch(t){if(t.code==="EPERM")return;clearInterval(e),console.error(`[parent] gateway (pid ${z}) is gone \u2014 shutting the pod down to avoid an orphan tree.`),ue("gateway-gone")}},5e3);e.unref()}})()}
20
+ ${c}`);return}p.web(e,r,{target:{socketPath:a.socketPath},headers:{"x-proxy-child-id":String(a.id)}},c=>{console.error(`[parent] proxy error to Child#${a.id}:`,c),a.healthy=!1,a.lastError=String(c?.stack||c),r.statusCode=502,r.setHeader("content-type","text/plain; charset=utf-8"),r.end(`Upstream error from Child#${a.id}. Try /webpack/reload.
21
+ ${a.lastError}`)})},"requestHandler");let z,ce,we;const ue=[];if(GATEWAY_HTTPS){const e=require("./basic-ssl"),r=GATEWAY_ROUTES.map(a=>a.host).filter(a=>!!a),d=path.join(process.cwd(),"node_modules",".cache","davnx-webpack");ce=e.getCertificateSync(d,SERVICE_NAME||"lensmcp.dev",r),we=e.caCertPath(),z=require("node:https").createServer({key:ce,cert:ce},ie)}else z=http.createServer(ie);const Ne=i((e,r,d)=>{const a=matchGatewayRoute(e);if(a?.prependPrefix&&!e.url?.startsWith(a.prependPrefix)&&(e.url=a.prependPrefix+(e.url??"/")),a?.target){p.ws(e,r,d,{target:a.target,changeOrigin:!0},()=>r.destroy());return}const c=U();if(!c){r.destroy();return}p.ws(e,r,d,{target:{socketPath:c.socketPath}},()=>r.destroy())},"upgradeHandler");z.on("upgrade",Ne);const Te=2e3;let ve=!1;z.on("error",e=>{if(e.code==="EADDRINUSE"){ve||(ve=!0,console.warn(`[parent] public port ${E} already in use \u2014 continuing on the unix-socket pool only (gateway routing is unaffected) and retrying the bind every ${Te/1e3}s. Give this service a distinct cluster.port if this is not a rolling handoff.`)),setTimeout(()=>{!T&&!z.listening&&z.listen(E)},Te).unref?.();return}console.error("[parent] public server error:",e)}),await t(_);for(const e of WORKER_NAMES)ye(e);z.listen(E,()=>{const e=GATEWAY_HTTPS?"https":"http";console.log(`[parent] Listening on ${e}://localhost:${E}`),SERVICE_PREFIX&&console.log(`[parent] Service prefix: "${SERVICE_PREFIX}" (enforced \u2014 requests without it will get 404)`),gatewayMiddleware&&console.log(`[parent] Gateway middleware: ACTIVE (${GATEWAY_MIDDLEWARE_PATH})`),GATEWAY_ROUTES.length>0&&console.log(`[parent] Gateway routes: ${GATEWAY_ROUTES.map(r=>`${r.host??"*"}${r.prefix??"/"}\u2192${r.target??"children"}`).join(" ")}`),we&&(console.log("[parent] HTTPS dev CA (trust ONCE, then rotations/new hostnames stay green):"),console.log(`[parent] sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${we}`)),console.log(`POST ${e}://localhost:${E}/webpack/reload to trigger rolling swap/respawn`)});const he=Number(process.env.INTERNAL_PORT||0);if(he>0){const e=i((d,a)=>{const c=U();if(!c){a.statusCode=503,a.setHeader("content-type","text/plain; charset=utf-8"),a.end("No healthy children available.");return}p.web(d,a,{target:{socketPath:c.socketPath}},o=>{c.healthy=!1,c.lastError=String(o?.stack||o),a.statusCode=502,a.end(`Upstream error from Child#${c.id}.`)})},"internalHandler"),r=http.createServer(e);r.on("upgrade",(d,a,c)=>{const o=U();if(!o){a.destroy();return}p.ws(d,a,c,{target:{socketPath:o.socketPath}},()=>a.destroy())}),r.on("error",d=>{console.warn(`[parent] internal port ${he} unavailable (${d.code}).`)}),r.listen(he,()=>{console.log(`[parent] Internal (service-to-service, no gateway middleware) on http://localhost:${he}`)}),ue.push(r)}const Ie=(()=>{try{return JSON.parse(process.env.GATEWAY_EXTRA_PORTS||"[]")}catch{return[]}})();for(const e of Ie){if(!Number.isInteger(e)||e===Number(E))continue;const r=GATEWAY_HTTPS?require("node:https").createServer({key:ce,cert:ce},ie):http.createServer(ie);r.on("upgrade",Ne),r.on("error",d=>{console.warn(`[parent] extra port ${e} unavailable (${d.code}) \u2014 main port still up.`)}),r.listen(e,()=>{console.log(`[parent] Also listening on ${GATEWAY_HTTPS?"https":"http"}://localhost:${e}`)}),ue.push(r)}const _e=path.join(SOCK_DIR,"parent.sock");claimSock(_e);const Ee=http.createServer(ie);Ee.on("error",e=>{console.warn(`[parent] control socket unavailable (${e.code}) \u2014 lensmcp logs/debug attach disabled.`)}),Ee.listen(_e),ue.push(Ee);const Re=i(async e=>{T=!0,console.log(`[${e}] [parent] shutting down\u2026`),cleanupSock(_e);for(const r of ue)try{r.close()}catch{}for(const r of H)try{r.proc.kill("SIGTERM")}catch{}for(const r of[...se])pe(r,"was still draining when its devserver shut down");for(const r of f)try{r.proc.kill("SIGTERM")}catch{}for(const r of w){try{r.proc.kill("SIGTERM")}catch{}r.socketPath&&cleanupSock(r.socketPath,r.proc.pid)}try{fs.rmdirSync(SOCK_DIR)}catch{}setTimeout(()=>process.exit(0),700).unref()},"shutdown");["SIGINT","SIGTERM","SIGHUP","SIGQUIT","SIGUSR2"].forEach(e=>process.on(e,()=>{Re(e)}));const fe=Number(process.env.LENSMCP_GATEWAY_PID??0);if(Number.isInteger(fe)&&fe>0){const e=setInterval(()=>{try{process.kill(fe,0)}catch(r){if(r.code==="EPERM")return;clearInterval(e),console.error(`[parent] gateway (pid ${fe}) is gone \u2014 shutting the pod down to avoid an orphan tree.`),Re("gateway-gone")}},5e3);e.unref()}})()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.23.6",
3
+ "version": "1.23.8",
4
4
  "description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -57,9 +57,9 @@
57
57
  }
58
58
  },
59
59
  "dependencies": {
60
- "@lensmcp/core": "1.23.6",
61
- "@lensmcp/node-instrumentation": "1.23.6",
62
- "@lensmcp/nx-plugin": "1.23.6",
60
+ "@lensmcp/core": "1.23.8",
61
+ "@lensmcp/node-instrumentation": "1.23.8",
62
+ "@lensmcp/nx-plugin": "1.23.8",
63
63
  "fastify": "^5.8.5",
64
64
  "@fastify/reply-from": "^12.6.2",
65
65
  "@typescript/native-preview": "^7.0.0-dev.20260707.2",
@@ -0,0 +1,17 @@
1
+ export declare function redisDisconnectFixEnabled(env?: NodeJS.ProcessEnv): boolean;
2
+ /**
3
+ * Patch one copy of ioredis. `true` when its `disconnect` was wrapped by this call, `false` when it was
4
+ * already, or when this is not ioredis.
5
+ */
6
+ export declare function finishStrandedDisconnect(ioredis: unknown): boolean;
7
+ export interface RedisDisconnectFix {
8
+ /** Distinct copies of ioredis patched by this call. */
9
+ patched: number;
10
+ versions: string[];
11
+ }
12
+ /**
13
+ * Patch every copy of ioredis the BUNDLE can reach: its own, and the one `bullmq` resolves (a nested
14
+ * copy when their ranges differ — and the one that matters, since BullMQ's connections are made from it).
15
+ * `undefined` when the app has neither.
16
+ */
17
+ export declare function finishStrandedDisconnectsFor(bundlePath: string, req?: NodeJS.Require): RedisDisconnectFix | undefined;
@@ -0,0 +1 @@
1
+ "use strict";var h=Object.defineProperty;var r=(t,e)=>h(t,"name",{value:e,configurable:!0});var d=Object.defineProperty,f=r((t,e)=>d(t,"name",{value:e,configurable:!0}),"f");Object.defineProperty(exports,"__esModule",{value:!0}),exports.redisDisconnectFixEnabled=redisDisconnectFixEnabled,exports.finishStrandedDisconnect=finishStrandedDisconnect,exports.finishStrandedDisconnectsFor=finishStrandedDisconnectsFor;const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path"));function redisDisconnectFixEnabled(t=process.env){const e=(t.LENSMCP_SWAP_FIX_IOREDIS_DISCONNECT??"").trim().toLowerCase();return!(e==="0"||e==="false"||e==="no"||e==="off")}r(redisDisconnectFixEnabled,"redisDisconnectFixEnabled"),f(redisDisconnectFixEnabled,"redisDisconnectFixEnabled");const failAll=f((t,e)=>{if(!(!t||typeof t.shift!="function"))for(let n=t.shift();n;n=t.shift())try{n.command?.reject?.(e)}catch{}},"failAll");function finishStrandedDisconnect(t){const e=t,n=(typeof e?.Redis=="function"?e.Redis:typeof e?.default=="function"?e.default:e)?.prototype,i=n?.disconnect;if(typeof i!="function"||i.__lensmcpEnds||typeof n.setStatus!="function")return!1;const o=f(function(c=!1){const a=!c&&this.status==="reconnecting",u=i.call(this,c);if(a&&this.status==="reconnecting")try{const s=new Error("Connection is closed.");this.setStatus("end"),typeof this.flushQueue=="function"&&this.flushQueue(s),failAll(this.prevCommandQueue,s),this.prevCommandQueue=null}catch{}return u},"wrapped");return o.__lensmcpEnds=!0,n.disconnect=o,!0}r(finishStrandedDisconnect,"finishStrandedDisconnect"),f(finishStrandedDisconnect,"finishStrandedDisconnect");function finishStrandedDisconnectsFor(t,e=require){const n=[path.dirname(t)],i=[n];try{i.push([path.dirname(e.resolve("bullmq/package.json",{paths:n}))])}catch{}const o=new Set,c=[];let a=0;for(const u of i){let s;try{s=e.resolve("ioredis",{paths:u})}catch{continue}if(!o.has(s)&&(o.add(s),!!finishStrandedDisconnect(e(s)))){a+=1;try{const l=e(e.resolve("ioredis/package.json",{paths:u})).version;l&&!c.includes(l)&&c.push(l)}catch{}}}return o.size===0?void 0:{patched:a,versions:c}}r(finishStrandedDisconnectsFor,"finishStrandedDisconnectsFor"),f(finishStrandedDisconnectsFor,"finishStrandedDisconnectsFor");
@@ -0,0 +1,14 @@
1
+ /** `LENSMCP_SWAP_PREWARM_ZOD=0` turns it off. */
2
+ export declare function schemaPrewarmEnabled(env?: NodeJS.ProcessEnv): boolean;
3
+ /** Build the throwaway schemas on `zod`'s namespace; returns how many of the builders it had. */
4
+ export declare function prewarmZod(zod: unknown): number;
5
+ export interface SchemaPrewarm {
6
+ /** How many throwaway schemas were built. */
7
+ created: number;
8
+ version?: string;
9
+ }
10
+ /**
11
+ * Pre-warm the zod the BUNDLE will get: resolved from the bundle's own directory, so it is the same module
12
+ * instance its `require('zod')` returns. `undefined` when the app has no zod to resolve.
13
+ */
14
+ export declare function prewarmZodFor(bundlePath: string, req?: NodeJS.Require): SchemaPrewarm | undefined;
@@ -0,0 +1 @@
1
+ "use strict";var c=Object.defineProperty;var r=(n,e)=>c(n,"name",{value:e,configurable:!0});var u=Object.defineProperty,t=r((n,e)=>u(n,"name",{value:e,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.schemaPrewarmEnabled=schemaPrewarmEnabled,exports.prewarmZod=prewarmZod,exports.prewarmZodFor=prewarmZodFor;const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path"));function schemaPrewarmEnabled(n=process.env){const e=(n.LENSMCP_SWAP_PREWARM_ZOD??"").trim().toLowerCase();return!(e==="0"||e==="false"||e==="no"||e==="off")}r(schemaPrewarmEnabled,"schemaPrewarmEnabled"),t(schemaPrewarmEnabled,"schemaPrewarmEnabled");const identity=t(n=>n,"identity"),BUILDERS=[n=>n.string(),n=>n.number(),n=>n.bigint(),n=>n.boolean(),n=>n.date(),n=>n.symbol(),n=>n.undefined(),n=>n.null(),n=>n.any(),n=>n.unknown(),n=>n.never(),n=>n.void(),n=>n.nan(),n=>n.int(),n=>n.float32(),n=>n.float64(),n=>n.int32(),n=>n.uint32(),n=>n.int64(),n=>n.uint64(),n=>n.email(),n=>n.guid(),n=>n.uuid(),n=>n.url(),n=>n.emoji(),n=>n.nanoid(),n=>n.cuid(),n=>n.cuid2(),n=>n.ulid(),n=>n.xid(),n=>n.ksuid(),n=>n.ipv4(),n=>n.ipv6(),n=>n.mac(),n=>n.cidrv4(),n=>n.cidrv6(),n=>n.base64(),n=>n.base64url(),n=>n.e164(),n=>n.jwt(),n=>n.hostname(),n=>n.hex(),n=>n.httpUrl(),n=>n.stringFormat("lensmcp",identity),n=>n.iso.datetime(),n=>n.iso.date(),n=>n.iso.time(),n=>n.iso.duration(),n=>n.array(n.unknown()),n=>n.object({}),n=>n.strictObject({}),n=>n.looseObject({}),n=>n.union([n.unknown(),n.unknown()]),n=>n.xor([n.string(),n.number()]),n=>n.discriminatedUnion("kind",[n.object({kind:n.literal("a")}),n.object({kind:n.literal("b")})]),n=>n.intersection(n.unknown(),n.unknown()),n=>n.tuple([n.unknown()]),n=>n.record(n.string(),n.unknown()),n=>n.partialRecord(n.string(),n.unknown()),n=>n.map(n.unknown(),n.unknown()),n=>n.set(n.unknown()),n=>n.enum(["a"]),n=>n.nativeEnum({A:"a"}),n=>n.literal("a"),n=>n.file(),n=>n.templateLiteral(["a"]),n=>n.transform(identity),n=>n.unknown().transform(identity),n=>n.unknown().pipe(n.unknown()),n=>n.unknown().optional(),n=>n.exactOptional(n.unknown()),n=>n.unknown().nullable(),n=>n.unknown().nullish(),n=>n.unknown().default(0),n=>n.unknown().prefault(0),n=>n.unknown().nonoptional(),n=>n.success(n.unknown()),n=>n.unknown().catch(0),n=>n.unknown().readonly(),n=>n.lazy(()=>n.unknown()),n=>n.promise(n.unknown()),n=>n.custom(()=>!0),n=>n.unknown().refine(()=>!0),n=>n.unknown().superRefine(()=>{}),n=>n.instanceof(Object),n=>n.function(),n=>n.json(),n=>n.stringbool(),n=>n.preprocess(identity,n.unknown()),n=>n.codec(n.unknown(),n.unknown(),{decode:identity,encode:identity}),n=>n.coerce.string(),n=>n.coerce.number(),n=>n.coerce.boolean(),n=>n.coerce.bigint(),n=>n.coerce.date()];function prewarmZod(n){const e=n?.z??n;if(!e||typeof e.object!="function")return 0;let o=0;for(const i of BUILDERS)try{i(e)!==void 0&&(o+=1)}catch{}return o}r(prewarmZod,"prewarmZod"),t(prewarmZod,"prewarmZod");function prewarmZodFor(n,e=require){let o;try{o=e.resolve("zod",{paths:[path.dirname(n)]})}catch{return}const i=prewarmZod(e(o));let a;try{a=e(e.resolve("zod/package.json",{paths:[path.dirname(n)]})).version}catch{}return{created:i,...a?{version:a}:{}}}r(prewarmZodFor,"prewarmZodFor"),t(prewarmZodFor,"prewarmZodFor");
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A pod child's stdout and stderr are PIPES to its devserver parent, and the parent can go first
3
+ * (issues/foodguard/25). A write to a pipe nobody reads fails with EPIPE, which Node delivers as an
4
+ * `'error'` EVENT on the stream; with no listener that is an uncaught exception. The devserver's
5
+ * catch-all then printed `[child] uncaughtException (uncaughtException): Error: write EPIPE`, and the
6
+ * stdout tap filed it — stack and all — as a server-side error. So a `lensmcp setup` or a gateway
7
+ * restart that caught a pod mid-boot left the lens reporting a failing workspace over a pod that had
8
+ * only been told to stop.
9
+ *
10
+ * `console.*` already ignores stream errors. What does not is a DIRECT `process.stdout.write`, which
11
+ * is how Nest's `ConsoleLogger` prints — so the guard sits on the streams themselves.
12
+ *
13
+ * EPIPE only. It means exactly "the reader is gone", which for a pod is its own teardown: the IPC
14
+ * `disconnect` guard or the parent's SIGTERM ends the process either way, and there is nobody left to
15
+ * tell. Any other stream error is re-thrown, so it reaches the handler it reached before.
16
+ */
17
+ import type { EventEmitter } from 'node:events';
18
+ /** Swallow EPIPE on this process's stdout/stderr. Idempotent — a stream is guarded once. */
19
+ export declare function guardBrokenStdio(streams?: readonly EventEmitter[]): void;
package/stdio-guard.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var c=Object.defineProperty;var r=(o,e)=>c(o,"name",{value:e,configurable:!0});var d=Object.defineProperty,s=r((o,e)=>d(o,"name",{value:e,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.guardBrokenStdio=guardBrokenStdio;function guardBrokenStdio(o=[process.stdout,process.stderr]){for(const e of o){const t=e;t.__lensmcpStdioGuard||(t.__lensmcpStdioGuard=!0,e.on("error",n=>{if(n?.code!=="EPIPE")throw n}))}}r(guardBrokenStdio,"guardBrokenStdio"),s(guardBrokenStdio,"guardBrokenStdio");
@@ -0,0 +1,79 @@
1
+ /**
2
+ * When a hot-swapped pod should stop being hot-swapped and be REPLACED by a fresh process.
3
+ *
4
+ * A swap re-evaluates the bundle in the same process, and the previous copy only goes away if nothing
5
+ * anywhere still points into it. The registries in `hot-swap-registries.ts` are the retainers this
6
+ * project has found and can prune. They are not the only ones, and some cannot be pruned from outside:
7
+ *
8
+ * - zod 4.4 installs its builder methods on each schema class's prototype from closures created in the
9
+ * initializer of the FIRST instance of that class, which keeps that instance — and, when it is one of
10
+ * the app's own schemas, the bundle it came from — for the life of the process;
11
+ * - an app whose shutdown never finishes (a queue worker still blocked, a listener never closed) keeps
12
+ * its whole generation, DI container and connections included;
13
+ * - a library this file has never heard of keeps a registry of its own.
14
+ *
15
+ * Measured on foodguard (2026-10-06), WITH the registry pruning in place: the engine pod went from 1.1 GB
16
+ * to 3.9 GB over five swaps and died on V8's heap limit; ten pods filled 24 GB of swap on a 32 GB machine.
17
+ *
18
+ * So memory needs a bound that does not depend on knowing the retainer. A new process has none of the old
19
+ * generations by construction: when a pod has grown past the bound it asks its parent for a successor,
20
+ * keeps serving until the successor is ready, then drains and exits. The cost is one cold start, paid off
21
+ * the request path, instead of a crash on it.
22
+ *
23
+ * Pure: the devserver measures, this decides. Everything is an env knob so a workspace can size it.
24
+ */
25
+ export interface SwapRecyclePolicy {
26
+ /** `LENSMCP_SWAP_RECYCLE=0` turns the whole guard off. */
27
+ enabled: boolean;
28
+ /** Replace the pod once its live heap is this many times what it was right after boot. `0` = off. */
29
+ heapFactor: number;
30
+ /** …but never for a heap smaller than this: three copies of a 20 MB service are not worth a restart. */
31
+ minHeapBytes: number;
32
+ /** Replace the pod once its live heap passes this share of V8's heap limit, whatever it started at. `0` = off. */
33
+ heapRatio: number;
34
+ /** Replace the pod after this many hot swaps, regardless of memory. `0` = off (the default). */
35
+ maxSwaps: number;
36
+ /** Replace the pod when a previous app has been closing for this long without returning. `0` = off. */
37
+ stuckCloseMs: number;
38
+ /** How long the pod must have been quiet — no swap running, no previous app closing — before its heap
39
+ * is compared with what it booted with. `0` = compare at once, whatever is going on. */
40
+ settleMs: number;
41
+ }
42
+ export interface SwapRecycleState {
43
+ /** Live heap right after the first app was built, in bytes. Unknown until measured. */
44
+ baselineBytes?: number;
45
+ /** Hot swaps this process has done since it booted. */
46
+ swaps: number;
47
+ usedBytes: number;
48
+ /** V8's `heap_size_limit`. */
49
+ limitBytes: number;
50
+ /** Previous apps whose `close()` has been pending for longer than `stuckCloseMs`. */
51
+ stuckCloses?: number;
52
+ /** `false` while a swap is running or a previous app is still closing, and for `settleMs` after. */
53
+ settled?: boolean;
54
+ }
55
+ /**
56
+ * - `LENSMCP_SWAP_RECYCLE` — `0`/`false`/`off`/`no` disables the guard.
57
+ * - `LENSMCP_SWAP_RECYCLE_HEAP_FACTOR` — default `3`. Two is not enough: a pod whose first generation is
58
+ * pinned (zod, above) sits at about twice its boot heap forever and must not be replaced for it.
59
+ * - `LENSMCP_SWAP_RECYCLE_MIN_MB` — default `512`.
60
+ * - `LENSMCP_SWAP_RECYCLE_HEAP_RATIO` — default `0.7`.
61
+ * - `LENSMCP_SWAP_RECYCLE_AFTER` — default `0` (off).
62
+ * - `LENSMCP_SWAP_RECYCLE_STUCK_CLOSE_MS` — default `60000`. An app whose shutdown hook waits on something
63
+ * that never ends is never released, whatever the heap says yet; a new process is the only way out.
64
+ * - `LENSMCP_SWAP_RECYCLE_SETTLE_MS` — default `5000`. The "times what it booted with" rule reads the heap
65
+ * only once the pod has been quiet that long. In the middle of a burst of rebuilds a pod legitimately
66
+ * holds the app that serves, the one being built to replace it and the ones still closing behind it, and
67
+ * none of that is retention. Measured on foodguard (2026-10-06, 03:01): five saves in forty seconds put
68
+ * the engine at 3.2× its boot heap, knowledge at 4.1× and audit at 3.5× — three pods replaced in the same
69
+ * half minute, each replacement one more cold boot on top of the burst that caused it. The share-of-limit
70
+ * rule is NOT held back: running into V8's limit is a crash whenever it happens.
71
+ */
72
+ export declare function swapRecyclePolicy(env?: NodeJS.ProcessEnv): SwapRecyclePolicy;
73
+ /**
74
+ * Why this pod should be replaced, in the words the log will print — or `undefined` when it should not.
75
+ *
76
+ * A pod that has never swapped is never replaced: whatever it holds, it is a single generation, and a
77
+ * fresh process would hold the same.
78
+ */
79
+ export declare function recycleReason(policy: SwapRecyclePolicy, state: SwapRecycleState): string | undefined;
@@ -0,0 +1 @@
1
+ "use strict";var r=Object.defineProperty;var i=(e,s)=>r(e,"name",{value:s,configurable:!0});var o=Object.defineProperty,t=i((e,s)=>o(e,"name",{value:s,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.swapRecyclePolicy=swapRecyclePolicy,exports.recycleReason=recycleReason;const MB=1024*1024,numberFrom=t((e,s,a)=>{if(e===void 0||e.trim()==="")return s;const n=Number(e);return Number.isFinite(n)&&n>=a?n:s},"numberFrom");function swapRecyclePolicy(e=process.env){return{enabled:!["0","false","no","off"].includes((e.LENSMCP_SWAP_RECYCLE??"").trim().toLowerCase()),heapFactor:numberFrom(e.LENSMCP_SWAP_RECYCLE_HEAP_FACTOR,3,0),minHeapBytes:numberFrom(e.LENSMCP_SWAP_RECYCLE_MIN_MB,512,0)*MB,heapRatio:Math.min(1,numberFrom(e.LENSMCP_SWAP_RECYCLE_HEAP_RATIO,.7,0)),maxSwaps:Math.floor(numberFrom(e.LENSMCP_SWAP_RECYCLE_AFTER,0,0)),stuckCloseMs:Math.floor(numberFrom(e.LENSMCP_SWAP_RECYCLE_STUCK_CLOSE_MS,6e4,0)),settleMs:Math.floor(numberFrom(e.LENSMCP_SWAP_RECYCLE_SETTLE_MS,5e3,0))}}i(swapRecyclePolicy,"swapRecyclePolicy"),t(swapRecyclePolicy,"swapRecyclePolicy");const mb=t(e=>(e/MB).toFixed(0),"mb");function recycleReason(e,s){if(!(!e.enabled||s.swaps<1)){if(e.maxSwaps>0&&s.swaps>=e.maxSwaps)return`${s.swaps} hot swaps in this process (LENSMCP_SWAP_RECYCLE_AFTER=${e.maxSwaps})`;if(e.stuckCloseMs>0&&(s.stuckCloses??0)>0){const a=s.stuckCloses;return`${a} previous app${a===1?" has":"s have"} not finished closing after ${Math.round(e.stuckCloseMs/1e3)}s \u2014 a shutdown hook that never returns keeps the whole app, its connections included, in memory`}if(e.heapRatio>0&&s.limitBytes>0&&s.usedBytes>=e.heapRatio*s.limitBytes){const a=Math.round(s.usedBytes/s.limitBytes*100);return`its heap is ${mb(s.usedBytes)} MB after a full GC, ${a}% of V8's ${mb(s.limitBytes)} MB limit`}if(e.heapFactor>0&&s.settled!==!1&&s.baselineBytes!==void 0&&s.baselineBytes>0&&s.usedBytes>=e.minHeapBytes&&s.usedBytes>=e.heapFactor*s.baselineBytes){const a=(s.usedBytes/s.baselineBytes).toFixed(1);return`its heap is ${mb(s.usedBytes)} MB after a full GC, ${a}\xD7 the ${mb(s.baselineBytes)} MB it booted with \u2014 earlier generations of the bundle are still in memory`}}}i(recycleReason,"recycleReason"),t(recycleReason,"recycleReason");