@lensmcp/cluster 1.25.2 → 1.25.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/executors/gateway/runtime/lifecycle.d.ts +12 -0
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/own-log.d.ts +41 -0
- package/executors/gateway/runtime/own-log.js +3 -0
- package/executors/gateway/runtime/proxy.d.ts +1 -0
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/types.d.ts +4 -0
- package/executors/serve/schema.d.ts +4 -0
- package/executors/serve/schema.json +14 -1
- package/executors/serve/serve.impl.js +1 -1
- package/main.devserver.js +12 -11
- package/package.json +5 -4
- package/swap-mode.d.ts +8 -0
- package/swap-mode.js +1 -1
- package/worker-guard.d.ts +1 -0
- package/worker-guard.js +1 -0
- package/worker-supervisor.d.ts +142 -0
- package/worker-supervisor.js +2 -0
|
@@ -328,6 +328,18 @@ export interface ServiceLayerDeps {
|
|
|
328
328
|
/** The shared compiler's relay. Default one for this runtime ({@link createSharedBuildRelay}). */
|
|
329
329
|
sharedBuild?: SharedBuildRelay;
|
|
330
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* The source-set change a pod is judged by (issues/closed/foodguard-76): its own scope's — and, once its scope has been
|
|
333
|
+
* diffed, NONE when nothing changed in it. The workspace-wide clock is the fallback only for a project the scanner
|
|
334
|
+
* could not scope (no project root) or has not diffed yet. It used to be the fallback for any project without a scoped
|
|
335
|
+
* stamp — and a project gets one only when ITS scope changes — so one file added in engine's scope recycled six
|
|
336
|
+
* unrelated FoodGuard services (knowledge, processor, audit, auth, plan, iam) whose scopes had never changed.
|
|
337
|
+
*/
|
|
338
|
+
export declare function podChangeClock(rt: Pick<GatewayRuntime, 'sourceSetChangedFor' | 'sourceSetChangesFor' | 'sourceSetScoped' | 'sourceSetChangedAt' | 'sourceSetChange'>, project: string): {
|
|
339
|
+
at: number;
|
|
340
|
+
change?: SourceSetChange;
|
|
341
|
+
scope: 'project' | 'workspace' | 'none';
|
|
342
|
+
};
|
|
331
343
|
export declare function createServiceLayer(rt: GatewayRuntime, obs: Observability, deps?: ServiceLayerDeps): ServiceLayer;
|
|
332
344
|
/**
|
|
333
345
|
* Which pid — if any — may be signalled to evict a stale adopted pod.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var Ve=Object.defineProperty;var v=(t,a)=>Ve(t,"name",{value:a,configurable:!0});var Ge=Object.defineProperty,s=v((t,a)=>Ge(t,"name",{value:a,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.scaleTarget=scaleTarget,exports.isLoopbackHostname=isLoopbackHostname,exports.isOwnRunnerCommand=isOwnRunnerCommand,exports.isRunnerOf=isRunnerOf,exports.isTestSourcePath=isTestSourcePath,exports.sourceScanPass=sourceScanPass,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer,exports.chooseStalePodKillTarget=chooseStalePodKillTarget;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),pod_sock_1=require("../../../pod-sock"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),inspector_secret_1=require("../../../inspector-secret"),pod_output_1=require("./pod-output"),serve_launch_1=require("./serve-launch"),shared_build_1=require("./shared-build"),protocol_1=require("../../../shared-build/protocol"),types_1=require("./types"),exit_reason_1=require("../../../exit-reason"),handoff_retire_1=require("../../../handoff-retire"),process_name_1=require("../../../process-name");function eastWestEnv(t,a){const i={},d=t.serviceKeys[(0,service_keys_1.serviceKeyId)(a.wsKey,a.project)];if(d&&(i.LENSMCP_SERVICE_KEY=d),t.https){const f=require("../../../basic-ssl").caCertPath();fs.existsSync(f)&&(i.NODE_EXTRA_CA_CERTS=f)}return i}v(eastWestEnv,"eastWestEnv"),s(eastWestEnv,"eastWestEnv");const sleep=s(t=>new Promise(a=>setTimeout(a,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let a=2166136261;for(let i=0;i<t.length;i+=1)a^=t.charCodeAt(i),a=Math.imul(a,16777619);return a>>>0}v(fnv1a,"fnv1a"),s(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:a}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(a.split("\0").filter(Boolean))}catch{return new Set}}v(gitIgnoredPaths,"gitIgnoredPaths"),s(gitIgnoredPaths,"gitIgnoredPaths");function scaleTarget(t,a=fs.existsSync){if(t.decl.adminUrl)return{url:t.decl.adminUrl};const i=path.join(t.pool.dir,"parent.sock");return a(i)?{socketPath:i}:t.decl.port?{url:`http://localhost:${t.decl.port}`}:void 0}v(scaleTarget,"scaleTarget"),s(scaleTarget,"scaleTarget");function isLoopbackHostname(t){const a=t.toLowerCase().replace(/^\[(.*)\]$/,"$1").replace(/\.$/,"");return a==="localhost"||a.endsWith(".localhost")?!0:net.isIPv4(a)?a.split(".")[0]==="127":net.isIPv6(a)&&new URL(`http://[${a}]/`).hostname==="[::1]"}v(isLoopbackHostname,"isLoopbackHostname"),s(isLoopbackHostname,"isLoopbackHostname");function postScale(t,a,i){let d;if("socketPath"in t)d=http.request({socketPath:t.socketPath,path:"/webpack/scale",method:"POST",headers:{"content-type":"application/json"}},f=>f.resume());else{const f=t.url.startsWith("https"),T=f?require("node:https"):http,y={method:"POST"};f&&isLoopbackHostname(new URL(t.url).hostname)&&(y.rejectUnauthorized=!1),i&&(y.lookup=i),d=T.request(`${t.url}/webpack/scale`,y,_=>_.resume())}return d.on("error",()=>{}),d.end(JSON.stringify({pods:a})),d}v(postScale,"postScale"),s(postScale,"postScale");function isOwnRunnerCommand(t,a){return(0,serve_launch_1.isServeRunnerCommand)(t)?(0,serve_launch_1.isServeRunnerCommandOf)(t,a):t.includes("/node_modules/nx/")?t.includes(`${a.replace(/\/+$/,"")}/node_modules/nx/`):!0}v(isOwnRunnerCommand,"isOwnRunnerCommand"),s(isOwnRunnerCommand,"isOwnRunnerCommand");function isRunnerOf(t,a,i){const d=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return!new RegExp(`(^|\\s)run ${d}(:|\\s|$)`).test(t)||!(0,serve_launch_1.isServeRunnerCommand)(t)&&!/\bnx(\.js)?\b/.test(t)?!1:isOwnRunnerCommand(t,i)}v(isRunnerOf,"isRunnerOf"),s(isRunnerOf,"isRunnerOf");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const a=t.split("/"),i=a[a.length-1]??"";if(TEST_FILE_RE.test(i))return!0;for(let d=0;d<a.length-1;d+=1)if(TEST_DIR_SEGMENTS.has(a[d]))return!0;return!1}v(isTestSourcePath,"isTestSourcePath"),s(isTestSourcePath,"isTestSourcePath");function sourceScanPass(t,a,i){const d=i?.ignoreTests??!0;let f;const T=new Map,y=`tree:${path.resolve(t)}`,_=s(g=>path.relative(t,g).split(path.sep).join("/"),"rel"),C=s(async(g,S)=>{const $=await(f??=gitIgnoredPaths(t)),L=[],I=new Set;let P=!1;const G=s(async(Q,w)=>{if(L.length>=SCAN_FILE_CAP){P=!0;return}let O;try{O=await fs.promises.readdir(Q,{withFileTypes:!0})}catch{return}w&&I.add(_(Q));for(const B of O){if(L.length>=SCAN_FILE_CAP){P=!0;return}const N=B.name,Z=path.join(Q,N);if(B.isDirectory()){if(!w||N.startsWith(".")||SRC_EXCLUDE.has(N)||a?.has(N)||N.endsWith("-devserver")||$.has(`${_(Z)}/`))continue;await G(Z,!0)}else if(B.isFile()){const ee=N.lastIndexOf(".");if(ee<=0||!SRC_EXT.has(N.slice(ee)))continue;const X=_(Z);if($.has(X)||d&&isTestSourcePath(X))continue;L.push(X)}}},"walk");return await G(g,S),{paths:L,entered:I,truncated:P}},"walkDir"),b=s((g,S)=>{let $=T.get(g);return $||($=S(),T.set(g,$)),$},"memo"),se=s(async()=>{const g=T.get(y);if(!g)return;const S=await g;return S.truncated?void 0:S},"wideWalk"),J=new Set,ne=s(()=>b(`files:${path.resolve(t)}`,async()=>{const g=await se();return g?{paths:g.paths.filter(S=>!S.includes("/")),entered:J,truncated:!1}:C(t,!1)}),"rootFiles"),ae=s(g=>b(`tree:${path.resolve(g)}`,async()=>{const S=await se(),$=_(g);if(S?.entered.has($)){const L=`${$}/`;return{paths:$===""?S.paths:S.paths.filter(I=>I.startsWith(L)),entered:J,truncated:!1}}return C(g,!0)}),"subtree"),U=s(g=>{const S=new Set;let $=0,L=g.some(P=>P.truncated);const I=s(P=>S.has(P)?!0:S.size>=SCAN_FILE_CAP?(L=!0,!1):(S.add(P),$^=fnv1a(P),!0),"add");for(const P of g)if(!P.paths.every(I))break;return{signature:`${S.size}:${($>>>0).toString(36)}`,paths:S,truncated:L}},"compose");return{async snapshot(g){if(!g||g.length===0)return U([await b(y,()=>C(t,!0))]);const S=[await ne()];for(const $ of g)S.push(await ae($));return U(S)}}}v(sourceScanPass,"sourceScanPass"),s(sourceScanPass,"sourceScanPass");async function sourceSetSnapshot(t,a,i,d){return sourceScanPass(t,i,d).snapshot(a)}v(sourceSetSnapshot,"sourceSetSnapshot"),s(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,a,i,d){return(await sourceSetSnapshot(t,a,i,d)).signature}v(sourceSetSignature,"sourceSetSignature"),s(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,a,i,d){if(t.paths.size===a.paths.size){let _=!0;for(const C of t.paths)if(!a.paths.has(C)){_=!1;break}if(_)return null}const f=[],T=[];let y=t.truncated||a.truncated;for(const _ of a.paths)t.paths.has(_)||(f.length+T.length<SOURCE_DIFF_PATH_CAP?f.push(_):y=!0);for(const _ of t.paths)a.paths.has(_)||(f.length+T.length<SOURCE_DIFF_PATH_CAP?T.push(_):y=!0);return{changedAt:i,added:f,removed:T,truncated:y,...d!==void 0?{windowMs:d}:{}}}v(diffSourceSets,"diffSourceSets"),s(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,a,i,d){const f=a&&a>0?t-a:i;return Math.max(f,i)+d}v(sourceChangeWindowMs,"sourceChangeWindowMs"),s(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,a,i,d,f){const T=s(C=>{const b=path.relative(t,C).split(path.sep).filter(Boolean);return b.length===0||b[0].startsWith("..")?"":b[0]},"bucketOf")(a);if(!T)return[];const y=new Set(i.filter(Boolean)),_=new Set([T]);for(const C of d)C.startsWith(".")||SRC_EXCLUDE.has(C)||f?.has(C)||C.endsWith("-devserver")||y.has(C)||_.add(C);return[..._].sort().map(C=>path.join(t,C))}v(sourceScopeDirs,"sourceScopeDirs"),s(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,a,i,d){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||a-t.lastUsed<d.settleMs||t.lastStaleRecycleAt!=null&&a-t.lastStaleRecycleAt<d.cooldownMs?null:i>0&&i-t.spawnAt>d.graceMs?"stale-source-set":d.maxAgeMs>0&&a-t.spawnAt>d.maxAgeMs?"max-age":null}v(podRecycleReason,"podRecycleReason"),s(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,a,i,d,f){return!(d-a<f.cooldownMs||i<=0||i-t<=f.graceMs||d-i<f.settleMs)}v(lensFrontendStale,"lensFrontendStale"),s(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,a,i,d=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&d.length>0){const f=a-i,T=new Set(t.recentEvents.filter(y=>y.at>=f).map(y=>path.resolve(y.path)));return d.every(y=>T.has(path.resolve(y)))?"heard":"missed"}return t.lastFsSetEventAt>=a-i?"heard":"missed"}v(lensWatcherVerdict,"lensWatcherVerdict"),s(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,a,i){if(!t)return"unknown";if(t.degraded)return"degraded";const d=a-i;return t.lastWatchRunAt>=d||t.lastBuildAt>=d?"heard":"silent"}v(podWatcherVerdict,"podWatcherVerdict"),s(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,a,i,d){const f=t.hasListened?d.graceMs:Math.max(d.graceMs,d.neverListenedGraceMs);return i-t.spawnedAt<f||i-a<d.cooldownMs||t.probeFails<Math.max(2,d.failureThreshold)?null:"wedged-no-listener"}v(lensChildWedged,"lensChildWedged"),s(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,a){return t.signal||t.code===null||t.code===0||t.ranMs>a.fastFailMs?!1:t.portHeld}v(isPortConflictExit,"isPortConflictExit"),s(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,a,i={}){const d=i.spawnProcess??node_child_process_1.spawn,f=i.probeAlive??pod_probe_1.probePodAlive,T=i.killTree??((e,o="SIGTERM")=>{try{process.kill(-e.pid,o)}catch{try{e.kill(o)}catch{}}}),y=i.sharedBuild??(0,shared_build_1.createSharedBuildRelay)(t),_=s((e,o="SIGTERM")=>{y.detach(e),T(e,o)},"killTree"),C=i.handoffDrainMs??(0,handoff_retire_1.handoffDrainBoundMs)(),b=i.treeAlive??(e=>{if(!e.pid)return!1;try{return process.kill(-e.pid,0),!0}catch(o){return o.code==="EPERM"}}),se=i.retirePollMs??1e3,J=i.rollReadyMs??types_1.POD_ROLL_READY_MS,ne=i.rollPollMs??750,ae=i.maxConcurrentRolls??types_1.POD_MAX_CONCURRENT_ROLLS,U=ae>0?ae:Number.POSITIVE_INFINITY,g=i.maxConcurrentColdStarts??types_1.MAX_COLD_STARTS,S=g>0?g:Number.POSITIVE_INFINITY;let $=0;const L=[],I=s(()=>$<S?($+=1,Promise.resolve()):new Promise(e=>{L.push(()=>{$+=1,e()})}),"acquireColdSlot"),P=s(()=>{$-=1,L.shift()?.()},"releaseColdSlot"),{emit:G}=a,Q=s(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),w=s((e,o,n,r,c)=>G(o,n,r,{wsKey:e.wsKey,...c},void 0,Q(e)),"emitSvc"),O=s(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),B=s((e,o)=>{let n;try{n=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const r=e.proc?.pid;let c=0;for(const l of n.split(`
|
|
2
|
-
`)){const p=l.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!p)continue;const u=Number(p[1])
|
|
3
|
-
`)){const k=(0,pod_output_1.classifyPodLine)(E),{clean:A,broken:F,healthy:j}=k,W=A;if(F){e.lastErrors.push(A),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const D=Date.now();(A!==e.lastErrorEmitSig||D-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=A,e.lastErrorEmitAt=D,w(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:A,fault:k.fault}))}j&&(e.buildFailed=!1);const x=(e.faults??=new pod_output_1.PodFaults).observe(k);if(x){e.lastErrorEmitSig=void 0;const D=x.open.length===0;w(e,"info",`${D?"service recovered":`${x.closed.join(" + ")} recovered`}: ${e.project}`,`cluster-service:${e.project}`,{kind:D?"service-recovered":"service-fault-closed",project:e.project,line:A,closed:x.closed,open:x.open})}!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(W)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${r}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),w(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(K(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),B(e,r),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&V(e)},1500).unref?.())},250).unref?.())}},"sniff"),R=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0,M=s(m=>h=>{if(t.stopped())return;const E=(0,inspector_secret_1.redactInspectorUrls)(h.toString());u(E);const k=`[${e.project}] ${E}`;m.write(k),R?.(k)},"relay");return l.stdout?.on("data",M(process.stdout)),l.stderr?.on("data",M(process.stderr)),l.on("exit",(m,h)=>{if(e.proc!==l||(e.proc=void 0,e.state="down",t.stopped()))return;const E=(0,exit_reason_1.describeExit)(m,h);if(console.log(`[gateway] ${r} exited (${m??"no code"}${h?`, ${h}`:""}${e.inflight?`, ${e.inflight} request(s) in flight`:""})${E?` \u2014 ${E}`:""}`),w(e,m===0?"info":"warning",`service down: ${e.project} (exit ${m})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:m,...h?{signal:h}:{}}),m===0||e.buildFailed)return;const k=Date.now();if(e.restarts=(e.restarts??[]).filter(F=>k-F<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){w(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(k);const A=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),w(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),V(e))},A)}),l},"launchServiceProcess"),V=s(e=>{if(t.stopped())return;const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,n=Math.max(1,e.decl.minPods??1);i.reapForeign!==!1&&B(e,o),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${o} (pods=${n})\u2026`),w(e,"info",`service starting: ${e.project} (${n} pod${n>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:n}),e.state="starting",e.spawnAt=Date.now(),e.generation=(e.generation??0)+1,e.proc=X(e,e.generation,"active")},"spawnService"),Ee=s(async e=>{const o=e.pool.socks[0];if(!o)return;const n=await(0,pod_probe_1.probePodAlive)(o,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);n&&(e.activeWork=n.activeWork,n.watcher&&(e.watcher=n.watcher))},"refreshActiveWork"),ke=s(e=>{try{const o=Number(fs.readFileSync(`${e}.owner`,"utf8").trim());return Number.isInteger(o)&&o>1?o:void 0}catch{return}},"readOwnerSidecar"),je=s(async e=>{const o=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!o)return[];const n=(0,node_crypto_1.createHash)("sha256").update(o).digest("hex").slice(0,8),r=[];for(const c of e.pool.socks){const l=await(0,pod_probe_1.probePodAlive)(c,void 0,o);l&&(!l.keyFingerprint||l.keyFingerprint!==n)&&r.push({sock:c,pid:l.pid})}return r},"staleAdoptedSocks"),Re=s((e,o)=>{const n=o.map(r=>r.sock);for(const{sock:r,pid:c}of o){const l=chooseStalePodKillTarget(c,ke(r),process.pid);if(l!==void 0)try{process.kill(l,"SIGTERM")}catch{}try{fs.unlinkSync(r)}catch{}try{fs.unlinkSync(`${r}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(r=>!n.includes(r)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),ce=s((e,o)=>{if(e.state==="up")return!1;e.state="up",e.wedgeRecycles=0;const n=e.pool.socks.length;return w(e,"info",`service up: ${e.project} (${n} pods, ${o})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:n,adopted:o==="adopted"}),!0},"markUp"),de=s(async e=>{if(O(e.pool)>0){const r=await je(e);if(r.length>0&&(console.error(`[gateway] ${e.project}: ${r.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),w(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:r.length}),e.proc&&K(e,"stale adoption (foreign east-west key)"),Re(e,r)),e.pool.socks.length>0)return ce(e,"adopted"),!0}if(t.stopped())return!1;let o=!1;const n=s(()=>e.state!=="starting"&&!e.proc,"notStarted");if(n())if(e.coldWait)await e.coldWait;else{$>=S&&(console.log(`[gateway] cold start queued: ${e.project}${e.wsKey&&e.wsKey!==t.wsKey?` (${e.wsKey})`:""} \u2014 ${$} starting, ${L.length} ahead (LENSMCP_MAX_COLD_STARTS=${S}).`),w(e,"info",`cold start queued: ${e.project}`,`cluster-service:${e.project}`,{kind:"cold-start-queued",project:e.project,inFlight:$,ahead:L.length}));const r=I();if(e.coldWait=r,await r,e.coldWait=void 0,o=!0,t.stopped())return P(),!1;n()&&V(e)}try{return await Ce(e)}finally{o&&P()}},"ensureUp"),Ce=s(async e=>{const o=Date.now()+t.startupTimeoutMs,n=Date.now()+12e3;for(;!t.stopped()&&Date.now()<o;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>n)return!1;if(O(e.pool)>0){e.state="up",e.buildFailed=!1;const r=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${r?`, cold start ${r}ms`:""})`),w(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${r??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:r}),!0}if(e.state==="down")return!1}return!1},"waitForPods"),Te=s(e=>{const o=e.decl.maxPods??e.decl.minPods??1,n=e.pool.socks.length,r=scaleTarget(e);!r||n===0||n>=o||e.inflight<=n||Date.now()-e.lastScaleAt<5e3||(e.lastScaleAt=Date.now(),postScale(r,n+1,i.scaleLookup),console.log(`[gateway] autoscale ${e.project}: ${n} \u2192 ${n+1} pods (inflight ${e.inflight})`),w(e,"info",`autoscale: ${e.project} ${n} \u2192 ${n+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:n,to:n+1,inflight:e.inflight}))},"scaleUp"),K=s((e,o,n)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),n?.announce!==!1&&w(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const r=e.proc;try{r.stdout?.removeAllListeners("data"),r.stderr?.removeAllListeners("data"),r.removeAllListeners("exit")}catch{}(0,discovery_1.stopRoutingTo)(e.pool),_(r,"SIGTERM"),e.proc=void 0,e.state="down",setTimeout(()=>{ie(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),ie=s(e=>{try{for(const o of fs.readdirSync(e.pool.dir)){if(!o.endsWith(".sock"))continue;const n=path.join(e.pool.dir,o);try{const r=Number(fs.readFileSync(`${n}.owner`,"utf8").trim());if(Number.isFinite(r)&&r>0)try{process.kill(r,0);continue}catch{}}catch{}try{fs.unlinkSync(n)}catch{}try{fs.unlinkSync(`${n}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks");let H=0;const pe=[],Ae=s(()=>H<U?(H+=1,Promise.resolve()):new Promise(e=>{pe.push(()=>{H+=1,e()})}),"acquireRollSlot"),Me=s(()=>{H-=1,pe.shift()?.()},"releaseRollSlot"),ye=s(async(e,o,n)=>{O(e.pool);for(const r of e.pool.socks){const c=await f(r,void 0,n);if(c&&o(c.podGeneration))return!0}return!1},"anyPodAnswers"),ue=new Map,xe=s((e,o,n)=>{const r=C>0&&(0,handoff_retire_1.requestHandoffRetire)(e.pool.dir,n);if(_(o,"SIGTERM"),!r)return;const c=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0,l=s(k=>A=>{if(t.stopped())return;const F=(0,inspector_secret_1.redactInspectorUrls)(A.toString()).replace(/^(?=.)/gm,`[${e.project} g${n} retiring] `);k.write(F),c?.(F)},"say"),p=l(process.stdout),u=l(process.stderr);o.stdout?.on("data",p),o.stderr?.on("data",u);let R=!1;const M=s(()=>{R||(R=!0,clearTimeout(m),clearInterval(E),o.stdout?.removeListener("data",p),o.stderr?.removeListener("data",u),ue.delete(o),(0,handoff_retire_1.clearHandoffRetire)(e.pool.dir,n),ie(e),e.pool.scannedAt=0)},"end"),m=setTimeout(()=>{console.error(`[gateway] pod handoff: ${e.project} g${n} was still running ${C}ms after it was retired \u2014 killing it.`),T(o,"SIGKILL"),M()},C);m.unref?.();const h=s(()=>{if(!R){if(t.stopped()){M();return}b(o)||(console.log(`[gateway] pod handoff: ${e.project} g${n} finished its in-flight work and exited.`),M())}},"look"),E=setInterval(h,se);E.unref?.(),o.once("exit",h),ue.set(o,()=>{T(o,"SIGTERM"),M()})},"retireReplacedTree"),Pe=s(()=>{for(const e of[...ue.values()])e()},"stopRetiringTrees"),he=s((e,o)=>{if(e.rolling)return e.rolling;const n=e.proc;if(!n||t.stopped())return Promise.resolve(!1);if(e.launcher==="nx")return console.log(`[gateway] pod handoff skipped: ${e.project} runs through nx run, which starts one instance of a target at a time \u2014 no standby can overlap it (reason=${o}).`),Promise.resolve(!1);H>=U&&console.log(`[gateway] pod handoff queued: ${e.project} reason=${o} \u2014 ${H} roll(s) in flight, ${pe.length} ahead (LENSMCP_POD_MAX_CONCURRENT_ROLLS=${U}).`);const r=(async()=>{await Ae();try{return await De(e,n,o)}finally{Me()}})().finally(()=>{e.rolling=void 0});return e.rolling=r,r},"rollPod"),De=s(async(e,o,n)=>{const r=e.proc;if(r!==o||t.stopped())return!1;const c=e.generation??0,l=c+1,p=Date.now();e.lastStaleRecycleAt=p,console.log(`[gateway] pod handoff start: ${e.project} g${c}\u2192g${l} reason=${n} \u2014 standby spawning, old generation keeps serving.`),w(e,"info",`pod handoff starting: ${e.project} (${n})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:n,generation:l,fromGeneration:c});const u=X(e,l,"standby"),R=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],M=p+J;let m;for(;!t.stopped()&&Date.now()<M&&!(u.exitCode!==null||u.signalCode!==null);){O(e.pool);for(const h of e.pool.socks)if((await f(h,void 0,R))?.podGeneration===l){m=h;break}if(m)break;await sleep(ne)}if(!m||t.stopped()){const h=t.stopped()?"gateway stopping":u.exitCode!==null||u.signalCode!==null?`standby exited (${u.exitCode??u.signalCode})`:`standby not ready within ${Math.round(J/1e3)}s`;try{u.stdout?.removeAllListeners("data"),u.stderr?.removeAllListeners("data")}catch{}(0,discovery_1.stopRoutingTo)(e.pool,A=>(0,pod_sock_1.podSockGeneration)(A)===l),_(u,"SIGTERM");const E=!t.stopped()&&e.proc===r&&r.exitCode===null&&r.signalCode===null&&await ye(e,A=>A!==l,R),k=E?`old generation g${c} kept serving`:`and the old generation g${c} is NOT serving (${e.proc!==r||r.exitCode!==null||r.signalCode!==null?"its tree is gone":"none of its pods answers"}) \u2014 the service is down until it recovers`;return console.error(`[gateway] pod handoff failed: ${e.project} g${l}: ${h} \u2014 ${k}.`),e.rollFailure=`${h} \u2014 ${k}`,w(e,E?"warning":"error",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:n,generation:l,error:h,durationMs:Date.now()-p,oldServing:E}),setTimeout(()=>ie(e),1200).unref?.(),!1}if(e.proc!==r)return(0,discovery_1.stopRoutingTo)(e.pool,h=>(0,pod_sock_1.podSockGeneration)(h)===l),_(u,"SIGTERM"),!1;e.proc=u,e.launcher=ee.get(u),e.generation=l,e.spawnAt=p,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{r.stdout?.removeAllListeners("data"),r.stderr?.removeAllListeners("data"),r.removeAllListeners("exit")}catch{}return(0,discovery_1.stopRoutingTo)(e.pool,h=>(0,pod_sock_1.podSockGeneration)(h)!==l&&path.join(e.pool.dir,h)!==m),xe(e,r,c),setTimeout(()=>{ie(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${l} answered on ${path.basename(m)} in ${Date.now()-p}ms; retiring g${c}.`),w(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:n,generation:l,fromGeneration:c,durationMs:Date.now()-p}),!0},"runRoll"),Le=s((e,o)=>{if(t.stopped())return{refused:"the gateway is stopping"};if(e.restart&&!e.restart.done)return{restart:e.restart,already:!0};if(!e.proc&&e.state!=="down")return{refused:`${e.project} is served by pods this gateway adopted from an earlier one \u2014 it has no tree of its own to replace. \`lensmcp gateway restart\` starts every service under this gateway.`};const n=e.proc?o.hard||e.launcher==="nx"||e.state!=="up"?"hard":"rolling":"start",r={how:n,reason:o.reason,startedAt:Date.now(),...e.proc&&e.generation!==void 0?{fromGeneration:e.generation}:{}};return e.restart=r,console.log(`[gateway] restart asked: ${e.project}${e.wsKey!==t.wsKey?` (${e.wsKey})`:""} \u2014 ${n} (${o.reason}).`),w(e,"info",`restart asked: ${e.project} (${n})`,`cluster-service:${e.project}`,{kind:"service-restart",phase:"started",project:e.project,how:n,reason:o.reason,...r.fromGeneration!==void 0?{fromGeneration:r.fromGeneration}:{}}),s(async()=>{if(n==="start")return await de(e)?{ok:!0}:{ok:!1,error:fe(e)};if(n==="rolling")return e.rolling&&await e.rolling,e.proc?(e.rollFailure=void 0,await he(e,o.reason)?{ok:!0}:{ok:!1,error:e.rollFailure??"the new tree did not take over"}):await de(e)?{ok:!0}:{ok:!1,error:fe(e)};if(K(e,`restart asked (${o.reason})`,{announce:!1}),await sleep(1500),t.stopped())return{ok:!1,error:"the gateway is stopping"};let c=!1;e.proc||(await I(),c=!0,!e.proc&&e.state!=="starting"&&V(e));try{const l=e.proc,p=e.generation,u=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],R=Date.now()+t.startupTimeoutMs;for(;!t.stopped()&&Date.now()<R;){if(!l||e.proc!==l||l.exitCode!==null||l.signalCode!==null)return{ok:!1,error:fe(e)};if(await ye(e,M=>M===p,u))return ce(e,"observed"),e.buildFailed=!1,{ok:!0};await sleep(ne)}return{ok:!1,error:t.stopped()?"the gateway is stopping":`no pod of the new tree answered within ${Math.round(t.startupTimeoutMs/1e3)}s`}}finally{c&&P()}},"run")().catch(c=>({ok:!1,error:c.message})).then(c=>{r.done={ok:c.ok,at:Date.now(),...e.generation!==void 0?{generation:e.generation}:{},...c.error?{error:c.error}:{}};const l=Math.round((r.done.at-r.startedAt)/100)/10;c.ok?console.log(`[gateway] restarted: ${e.project} \u2014 g${r.done.generation??"?"} serving after ${l}s (${n}).`):console.error(`[gateway] restart failed: ${e.project} \u2014 ${c.error} (${n}, after ${l}s).`),w(e,c.ok?"info":"error",c.ok?`restarted: ${e.project}`:`restart failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-restart",phase:c.ok?"done":"failed",project:e.project,how:n,reason:o.reason,...r.fromGeneration!==void 0?{fromGeneration:r.fromGeneration}:{},...r.done.generation!==void 0?{generation:r.done.generation}:{},durationMs:r.done.at-r.startedAt,...c.error?{error:c.error}:{}})}),{restart:r,already:!1}},"restartService"),fe=s(e=>e.buildFailed?`its build failed${e.lastErrors?.length?`: ${e.lastErrors[e.lastErrors.length-1]}`:""}`:e.proc&&(e.proc.exitCode!==null||e.proc.signalCode!==null)?`its tree exited (${e.proc.exitCode??e.proc.signalCode})`:!e.proc&&e.state==="down"?"its tree exited before a pod answered":`no pod answered within ${Math.round(t.startupTimeoutMs/1e3)}s`,"startFailure"),$e=s(e=>{const o=Date.now();e.restarts=(e.restarts??[]).filter(r=>o-r<types_1.SERVICE_RESTART_WINDOW_MS);const n=(e.wedgeRecycles??0)+1;if(e.restarts.length>=types_1.SERVICE_MAX_RESTARTS||n>types_1.SERVICE_MAX_RESTARTS){const r=e.restarts.length>=types_1.SERVICE_MAX_RESTARTS?`wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min`:`recycled ${types_1.SERVICE_MAX_RESTARTS}\xD7 in a row and no pod came up`;w(e,"error",`service crash-looping: ${e.project} \u2014 ${r}, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project,recycles:n-1}),K(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.wedgeRecycles=n,e.restarts.push(o),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; attempt ${n}/${types_1.SERVICE_MAX_RESTARTS} with no pod up since the first, ${e.restarts.length} in the last minute; ${e.inflight??0} request(s) in flight \u2014 jobs held by the dead pod keep their locks until they expire)`),w(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:n,inWindow:e.restarts.length}),K(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&V(e)},1500).unref?.()},"recycleWedged"),be={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let ge;const te=new Map,_e=new Map;let we=!1,Se=0,le=0;const Ie=s(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(r=>r.project));for(const r of t.routes)r.lens&&o.add(r.lens.project);const n=new Set;for(const r of o){const c=e[r];if(!c)continue;const l=path.relative(t.root,c).split(path.sep).filter(Boolean)[0];l&&!l.startsWith("..")&&n.add(l)}return[...n]},"managedBuckets"),Oe=s((e,o,n,r)=>{const c=t.projectSourceRoots?.[e];if(c?.length)return c;const l=t.projectRoots?.[e];if(!l)return;const p=sourceScopeDirs(t.root,l,n,o,r);return p.length>0?p:void 0},"scopeFor"),Ne=s(()=>{if(we||Date.now()-Se<types_1.POD_STALE_SCAN_MS)return;we=!0,Se=Date.now();const e=Se;(async()=>{const o=(0,scope_1.readSourceSetOptions)(t.root),n=o.exclude,r={ignoreTests:o.ignoreTests},c=sourceScanPass(t.root,n,r),l=await c.snapshot(),p=Date.now(),u=sourceChangeWindowMs(p,le,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),R=ge?diffSourceSets(ge,l,p,u):null;if(R&&(t.sourceSetChangedAt=p,t.sourceSetChange=R,G("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:R.added,removed:R.removed,truncated:R.truncated})),ge=l,!t.projectRoots){le=e;return}let M;try{M=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(j=>j.isDirectory()).map(j=>j.name)}catch{return}const m=Ie(),h=new Set(t.services.map(j=>j.project));for(const j of t.routes)j.lens&&h.add(j.lens.project);const E=new Map;for(const j of h){const W=Oe(j,M,m,n);if(!W)continue;const x=W.join("|"),D=E.get(x)??{dirs:W,projects:[]};D.projects.push(j),E.set(x,D)}const k=Date.now(),A=sourceChangeWindowMs(k,le,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);le=e;const F=s((j,W,x)=>{t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const D of j)t.sourceSetChangedFor.set(D,W),t.sourceSetChangesFor.set(D,x)},"stampScoped");for(const[j,{dirs:W,projects:x}]of E){const D=await c.snapshot(W),me=te.get(j);te.set(j,D);const ve=new Map;for(const Y of x){const q=_e.get(Y);if(q===void 0||q===j)continue;const z=ve.get(q)??[];z.push(Y),ve.set(q,z)}for(const Y of x)_e.set(Y,j);for(const[Y,q]of ve){const z=te.get(Y),oe=z?diffSourceSets(z,D,k,A):R;oe&&(F(q,z?k:p,oe),G("info",`source file set changed \u2014 ${q.join(", ")}'s watch scope was re-keyed`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,rescoped:!0,projects:q,added:oe.added,removed:oe.removed,truncated:oe.truncated}))}if(!me)continue;const re=diffSourceSets(me,D,k,A);re&&(F(x,k,re),G("info",`source file set changed in ${x.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:x,added:re.added,removed:re.removed,truncated:re.truncated}))}for(const j of[...te.keys()])E.has(j)||te.delete(j)})().catch(()=>{}).finally(()=>{we=!1})},"maybeScanSourceSet"),Fe=s((e,o)=>{const n=(t.sourceSetChangesFor?.get(e.project)??t.sourceSetChange)?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,r=podWatcherVerdict(e.watcher,o,n),c=s(l=>(e.staleKeptAt!==o&&(e.staleKeptAt=o,console.log(`[gateway] stale source set: pod ${e.project} ${l} \u2014 no recycle.`)),!1),"keep");return r==="heard"?(e.staleVerifiedAt=o,w(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:r}),c(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`)):r==="degraded"?(console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0):r==="silent"?types_1.POD_WATCHER_SILENT==="keep"?c("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):!0:types_1.POD_WATCHER_UNVERIFIED==="keep"?c("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):!0},"podStaleRecycleAllowed"),We=s((e,o)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc&&e.launcher!=="nx"){he(e,o);return}console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),w(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const n=!!e.decl.eager;K(e,`${o} \u2014 recycling stale pod`,{announce:!1}),n&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&V(e)},1500).unref?.()},"recyclePod"),Ke=s(()=>{const e=setInterval(()=>{qe().catch(o=>console.error("[gateway] sweep error:",o))},t.sweepMs);return e.unref(),e},"startSweeper"),qe=s(async()=>{{if(t.stopped())return;for(const e of t.graphStores?.all()??[])e.refreshIfStale();types_1.POD_RECYCLE_ENABLED&&Ne();for(const e of t.registry?t.registry.allServices():t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const o=path.resolve(e.root)===path.resolve(t.root),n=e.decl.idleKillSec;if(n&&e.inflight===0&&Date.now()-e.lastUsed>n*1e3){K(e,`idle ${n}s`),e.wedgedSince=void 0;continue}const r=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,c=O(e.pool)>0;if(c||r){if(e.wedgedSince=void 0,c&&ce(e,"observed"),c&&!r&&types_1.POD_RECYCLE_ENABLED&&o){const l=t.sourceSetChangedFor?.get(e.project)??t.sourceSetChangedAt??0;await Ee(e);const p=(e.staleVerifiedAt??0)>=l&&l>0,u=podRecycleReason(e,Date.now(),p?0:l,be);if(u==="stale-source-set"&&!Fe(e,l))continue;u&&We(e,u)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,$e(e))}}},"sweepOnce");return{scanNow:O,spawnService:V,ensureUp:de,scaleUp:Te,killSpawned:K,markUp:ce,startSweeper:Ke,rollPod:he,restartService:Le,recycleWedged:$e,stopRetiringTrees:Pe,stopBuildHost:s(e=>e===void 0?y.stop():y.release(e),"stopBuildHost")}}v(createServiceLayer,"createServiceLayer"),s(createServiceLayer,"createServiceLayer");function chooseStalePodKillTarget(t,a,i){if(!(t===void 0||!Number.isInteger(t)||t<=1)&&!(a!==void 0&&a!==t)&&t!==i)return t}v(chooseStalePodKillTarget,"chooseStalePodKillTarget"),s(chooseStalePodKillTarget,"chooseStalePodKillTarget");
|
|
1
|
+
"use strict";var Ue=Object.defineProperty;var v=(t,a)=>Ue(t,"name",{value:a,configurable:!0});var Ve=Object.defineProperty,s=v((t,a)=>Ve(t,"name",{value:a,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.scaleTarget=scaleTarget,exports.isLoopbackHostname=isLoopbackHostname,exports.isOwnRunnerCommand=isOwnRunnerCommand,exports.isRunnerOf=isRunnerOf,exports.isTestSourcePath=isTestSourcePath,exports.sourceScanPass=sourceScanPass,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.podChangeClock=podChangeClock,exports.createServiceLayer=createServiceLayer,exports.chooseStalePodKillTarget=chooseStalePodKillTarget;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),pod_sock_1=require("../../../pod-sock"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),own_log_1=require("./own-log"),inspector_secret_1=require("../../../inspector-secret"),pod_output_1=require("./pod-output"),serve_launch_1=require("./serve-launch"),shared_build_1=require("./shared-build"),protocol_1=require("../../../shared-build/protocol"),types_1=require("./types"),exit_reason_1=require("../../../exit-reason"),handoff_retire_1=require("../../../handoff-retire"),process_name_1=require("../../../process-name");function eastWestEnv(t,a){const c={},d=t.serviceKeys[(0,service_keys_1.serviceKeyId)(a.wsKey,a.project)];if(d&&(c.LENSMCP_SERVICE_KEY=d),t.https){const g=require("../../../basic-ssl").caCertPath();fs.existsSync(g)&&(c.NODE_EXTRA_CA_CERTS=g)}return c}v(eastWestEnv,"eastWestEnv"),s(eastWestEnv,"eastWestEnv");const sleep=s(t=>new Promise(a=>setTimeout(a,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let a=2166136261;for(let c=0;c<t.length;c+=1)a^=t.charCodeAt(c),a=Math.imul(a,16777619);return a>>>0}v(fnv1a,"fnv1a"),s(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:a}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(a.split("\0").filter(Boolean))}catch{return new Set}}v(gitIgnoredPaths,"gitIgnoredPaths"),s(gitIgnoredPaths,"gitIgnoredPaths");function scaleTarget(t,a=fs.existsSync){if(t.decl.adminUrl)return{url:t.decl.adminUrl};const c=path.join(t.pool.dir,"parent.sock");return a(c)?{socketPath:c}:t.decl.port?{url:`http://localhost:${t.decl.port}`}:void 0}v(scaleTarget,"scaleTarget"),s(scaleTarget,"scaleTarget");function isLoopbackHostname(t){const a=t.toLowerCase().replace(/^\[(.*)\]$/,"$1").replace(/\.$/,"");return a==="localhost"||a.endsWith(".localhost")?!0:net.isIPv4(a)?a.split(".")[0]==="127":net.isIPv6(a)&&new URL(`http://[${a}]/`).hostname==="[::1]"}v(isLoopbackHostname,"isLoopbackHostname"),s(isLoopbackHostname,"isLoopbackHostname");function postScale(t,a,c){let d;if("socketPath"in t)d=http.request({socketPath:t.socketPath,path:"/webpack/scale",method:"POST",headers:{"content-type":"application/json"}},g=>g.resume());else{const g=t.url.startsWith("https"),M=g?require("node:https"):http,m={method:"POST"};g&&isLoopbackHostname(new URL(t.url).hostname)&&(m.rejectUnauthorized=!1),c&&(m.lookup=c),d=M.request(`${t.url}/webpack/scale`,m,E=>E.resume())}return d.on("error",()=>{}),d.end(JSON.stringify({pods:a})),d}v(postScale,"postScale"),s(postScale,"postScale");function isOwnRunnerCommand(t,a){return(0,serve_launch_1.isServeRunnerCommand)(t)?(0,serve_launch_1.isServeRunnerCommandOf)(t,a):t.includes("/node_modules/nx/")?t.includes(`${a.replace(/\/+$/,"")}/node_modules/nx/`):!0}v(isOwnRunnerCommand,"isOwnRunnerCommand"),s(isOwnRunnerCommand,"isOwnRunnerCommand");function isRunnerOf(t,a,c){const d=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return!new RegExp(`(^|\\s)run ${d}(:|\\s|$)`).test(t)||!(0,serve_launch_1.isServeRunnerCommand)(t)&&!/\bnx(\.js)?\b/.test(t)?!1:isOwnRunnerCommand(t,c)}v(isRunnerOf,"isRunnerOf"),s(isRunnerOf,"isRunnerOf");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const a=t.split("/"),c=a[a.length-1]??"";if(TEST_FILE_RE.test(c))return!0;for(let d=0;d<a.length-1;d+=1)if(TEST_DIR_SEGMENTS.has(a[d]))return!0;return!1}v(isTestSourcePath,"isTestSourcePath"),s(isTestSourcePath,"isTestSourcePath");function sourceScanPass(t,a,c){const d=c?.ignoreTests??!0;let g;const M=new Map,m=`tree:${path.resolve(t)}`,E=s(w=>path.relative(t,w).split(path.sep).join("/"),"rel"),C=s(async(w,y)=>{const _=await(g??=gitIgnoredPaths(t)),D=[],O=new Set;let P=!1;const V=s(async(Q,S)=>{if(D.length>=SCAN_FILE_CAP){P=!0;return}let N;try{N=await fs.promises.readdir(Q,{withFileTypes:!0})}catch{return}S&&O.add(E(Q));for(const X of N){if(D.length>=SCAN_FILE_CAP){P=!0;return}const F=X.name,Z=path.join(Q,F);if(X.isDirectory()){if(!S||F.startsWith(".")||SRC_EXCLUDE.has(F)||a?.has(F)||F.endsWith("-devserver")||_.has(`${E(Z)}/`))continue;await V(Z,!0)}else if(X.isFile()){const ee=F.lastIndexOf(".");if(ee<=0||!SRC_EXT.has(F.slice(ee)))continue;const H=E(Z);if(_.has(H)||d&&isTestSourcePath(H))continue;D.push(H)}}},"walk");return await V(w,y),{paths:D,entered:O,truncated:P}},"walkDir"),I=s((w,y)=>{let _=M.get(w);return _||(_=y(),M.set(w,_)),_},"memo"),se=s(async()=>{const w=M.get(m);if(!w)return;const y=await w;return y.truncated?void 0:y},"wideWalk"),J=new Set,ne=s(()=>I(`files:${path.resolve(t)}`,async()=>{const w=await se();return w?{paths:w.paths.filter(y=>!y.includes("/")),entered:J,truncated:!1}:C(t,!1)}),"rootFiles"),ae=s(w=>I(`tree:${path.resolve(w)}`,async()=>{const y=await se(),_=E(w);if(y?.entered.has(_)){const D=`${_}/`;return{paths:_===""?y.paths:y.paths.filter(O=>O.startsWith(D)),entered:J,truncated:!1}}return C(w,!0)}),"subtree"),B=s(w=>{const y=new Set;let _=0,D=w.some(P=>P.truncated);const O=s(P=>y.has(P)?!0:y.size>=SCAN_FILE_CAP?(D=!0,!1):(y.add(P),_^=fnv1a(P),!0),"add");for(const P of w)if(!P.paths.every(O))break;return{signature:`${y.size}:${(_>>>0).toString(36)}`,paths:y,truncated:D}},"compose");return{async snapshot(w){if(!w||w.length===0)return B([await I(m,()=>C(t,!0))]);const y=[await ne()];for(const _ of w)y.push(await ae(_));return B(y)}}}v(sourceScanPass,"sourceScanPass"),s(sourceScanPass,"sourceScanPass");async function sourceSetSnapshot(t,a,c,d){return sourceScanPass(t,c,d).snapshot(a)}v(sourceSetSnapshot,"sourceSetSnapshot"),s(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,a,c,d){return(await sourceSetSnapshot(t,a,c,d)).signature}v(sourceSetSignature,"sourceSetSignature"),s(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,a,c,d){if(t.paths.size===a.paths.size){let E=!0;for(const C of t.paths)if(!a.paths.has(C)){E=!1;break}if(E)return null}const g=[],M=[];let m=t.truncated||a.truncated;for(const E of a.paths)t.paths.has(E)||(g.length+M.length<SOURCE_DIFF_PATH_CAP?g.push(E):m=!0);for(const E of t.paths)a.paths.has(E)||(g.length+M.length<SOURCE_DIFF_PATH_CAP?M.push(E):m=!0);return{changedAt:c,added:g,removed:M,truncated:m,...d!==void 0?{windowMs:d}:{}}}v(diffSourceSets,"diffSourceSets"),s(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,a,c,d){const g=a&&a>0?t-a:c;return Math.max(g,c)+d}v(sourceChangeWindowMs,"sourceChangeWindowMs"),s(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,a,c,d,g){const M=s(C=>{const I=path.relative(t,C).split(path.sep).filter(Boolean);return I.length===0||I[0].startsWith("..")?"":I[0]},"bucketOf")(a);if(!M)return[];const m=new Set(c.filter(Boolean)),E=new Set([M]);for(const C of d)C.startsWith(".")||SRC_EXCLUDE.has(C)||g?.has(C)||C.endsWith("-devserver")||m.has(C)||E.add(C);return[...E].sort().map(C=>path.join(t,C))}v(sourceScopeDirs,"sourceScopeDirs"),s(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,a,c,d){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||a-t.lastUsed<d.settleMs||t.lastStaleRecycleAt!=null&&a-t.lastStaleRecycleAt<d.cooldownMs?null:c>0&&c-t.spawnAt>d.graceMs?"stale-source-set":d.maxAgeMs>0&&a-t.spawnAt>d.maxAgeMs?"max-age":null}v(podRecycleReason,"podRecycleReason"),s(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,a,c,d,g){return!(d-a<g.cooldownMs||c<=0||c-t<=g.graceMs||d-c<g.settleMs)}v(lensFrontendStale,"lensFrontendStale"),s(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,a,c,d=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&d.length>0){const g=a-c,M=new Set(t.recentEvents.filter(m=>m.at>=g).map(m=>path.resolve(m.path)));return d.every(m=>M.has(path.resolve(m)))?"heard":"missed"}return t.lastFsSetEventAt>=a-c?"heard":"missed"}v(lensWatcherVerdict,"lensWatcherVerdict"),s(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,a,c){if(!t)return"unknown";if(t.degraded)return"degraded";const d=a-c;return t.lastWatchRunAt>=d||t.lastBuildAt>=d?"heard":"silent"}v(podWatcherVerdict,"podWatcherVerdict"),s(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,a,c,d){const g=t.hasListened?d.graceMs:Math.max(d.graceMs,d.neverListenedGraceMs);return c-t.spawnedAt<g||c-a<d.cooldownMs||t.probeFails<Math.max(2,d.failureThreshold)?null:"wedged-no-listener"}v(lensChildWedged,"lensChildWedged"),s(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,a){return t.signal||t.code===null||t.code===0||t.ranMs>a.fastFailMs?!1:t.portHeld}v(isPortConflictExit,"isPortConflictExit"),s(isPortConflictExit,"isPortConflictExit");function podChangeClock(t,a){const c=t.sourceSetChangedFor?.get(a);if(c!==void 0){const d=t.sourceSetChangesFor?.get(a);return{at:c,...d?{change:d}:{},scope:"project"}}return t.sourceSetScoped?.has(a)?{at:0,scope:"none"}:{at:t.sourceSetChangedAt??0,...t.sourceSetChange?{change:t.sourceSetChange}:{},scope:"workspace"}}v(podChangeClock,"podChangeClock"),s(podChangeClock,"podChangeClock");const changeEvidence=s(t=>{if(!t)return"no path evidence";const a=[...t.added.map(c=>`+${c}`),...t.removed.map(c=>`-${c}`)];return a.length?`${a.slice(0,3).join(", ")}${a.length>3?` (+${a.length-3} more)`:""}`:"no path evidence"},"changeEvidence");function createServiceLayer(t,a,c={}){const d=c.spawnProcess??node_child_process_1.spawn,g=c.probeAlive??pod_probe_1.probePodAlive,M=c.killTree??((e,r="SIGTERM")=>{try{process.kill(-e.pid,r)}catch{try{e.kill(r)}catch{}}}),m=c.sharedBuild??(0,shared_build_1.createSharedBuildRelay)(t),E=s((e,r="SIGTERM")=>{m.detach(e),M(e,r)},"killTree"),C=c.handoffDrainMs??(0,handoff_retire_1.handoffDrainBoundMs)(),I=c.treeAlive??(e=>{if(!e.pid)return!1;try{return process.kill(-e.pid,0),!0}catch(r){return r.code==="EPERM"}}),se=c.retirePollMs??1e3,J=c.rollReadyMs??types_1.POD_ROLL_READY_MS,ne=c.rollPollMs??750,ae=c.maxConcurrentRolls??types_1.POD_MAX_CONCURRENT_ROLLS,B=ae>0?ae:Number.POSITIVE_INFINITY,w=c.maxConcurrentColdStarts??types_1.MAX_COLD_STARTS,y=w>0?w:Number.POSITIVE_INFINITY;let _=0;const D=[],O=s(()=>_<y?(_+=1,Promise.resolve()):new Promise(e=>{D.push(()=>{_+=1,e()})}),"acquireColdSlot"),P=s(()=>{_-=1,D.shift()?.()},"releaseColdSlot"),{emit:V}=a,Q=s(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),S=s((e,r,n,o,i)=>V(r,n,o,{wsKey:e.wsKey,...i},void 0,Q(e)),"emitSvc"),N=s(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),X=s((e,r)=>{let n;try{n=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const o=e.proc?.pid;let i=0;for(const l of n.split(`
|
|
2
|
+
`)){const p=l.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!p)continue;const u=Number(p[1]),$=Number(p[2]);if(isRunnerOf(p[3],r,e.root)&&!(u===process.pid||u===o)&&!(o!==void 0&&$===o)){try{process.kill(-$,"SIGKILL")}catch{try{process.kill(u,"SIGKILL")}catch{}}i+=1}}return i>0&&(console.log(`[gateway] ${r}: reaped ${i} stale runner tree(s) \u2014 a foreign generation of this service (holding nx's task lock, or its pool)`),S(e,"warning",`stale-runner reap: ${e.project} (${i} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:i})),i},"reapForeignRunners"),F=s((e,r)=>({...(0,pod_env_1.podBaseEnv)(process.env,t.root,e.root),...e.instanceEnv?{...e.instanceEnv,LENSMCP_SOCKET_ONLY:"1"}:{},...e.frozen?{LENSMCP_FREEZE:"1",LENSMCP_TYPECHECK:"off"}:{},NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false",...e.wsKey?{LENSMCP_WS_KEY:e.wsKey}:{},...e.sockNs?{LENSMCP_SOCK_NS:e.sockNs}:{},LENSMCP_EVENT_FILE:e.wsKey===t.wsKey?t.eventFile:path.join(e.root,".lensmcp","events.jsonl"),LENSMCP_GATEWAY_PID:String(process.pid),LENSMCP_POD_GENERATION:String(r),...eastWestEnv(t,e)}),"serviceEnv"),Z=s((e,r,n,o)=>{const i=F(e,r),l=e.wsKey||path.basename(e.root),p=(0,serve_launch_1.planServeLaunch)({stores:t.graphStores,root:e.root,project:e.project,target:e.decl.serveTarget??"serve-hmr",childCount:o,baseEnv:i});if(p.kind==="runner"){const h=m.offeredTo(e),j=d((0,process_name_1.namedExecPath)((0,process_name_1.processName)(l,e.project,serve_launch_1.SERVE_RUNNER_ROLE)),[...p.script.execArgv,p.script.path,...(0,serve_launch_1.serveRunnerArgs)(n,o,e.root)],{cwd:e.root,detached:!0,env:h?{...p.env,[protocol_1.SHARED_BUILD_ENV]:"1"}:p.env,stdio:["ignore","pipe","pipe","ipc"]});return h&&m.attach(e,r,j),j.send?.(p.message,R=>{R&&console.error(`[gateway] ${n}: could not hand the task to its runner (${R.message})`)}),{proc:j,launcher:"runner"}}console.log(`[gateway] ${n}: starting through nx run \u2014 ${p.reason}`);const u=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),$=(0,process_name_1.namedExecPath)((0,process_name_1.processName)(l,e.project,"nx")),[A,k]=fs.existsSync(u)?[$,[u,"run",n,`--childCount=${o}`]]:["yarn",["nx","run",n,`--childCount=${o}`]];return{proc:d(A,k,{cwd:e.root,detached:!0,env:i,stdio:["ignore","pipe","pipe"]}),launcher:"nx"}},"startServiceTree"),ee=new WeakMap,H=s((e,r,n)=>{const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,i=Math.max(1,e.decl.minPods??1),{proc:l,launcher:p}=Z(e,r,o,i);ee.set(l,p),n==="active"&&(e.launcher=p),n==="active"&&(e.lastErrors=[],e.buildFailed=!1,e.faults=new pod_output_1.PodFaults);const u=s(k=>{const h=k.toString();for(const j of h.split(`
|
|
3
|
+
`)){const R=(0,pod_output_1.classifyPodLine)(j),{clean:T,broken:K,healthy:ye}=R,f=T;if(K){e.lastErrors.push(T),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const x=Date.now();(T!==e.lastErrorEmitSig||x-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=T,e.lastErrorEmitAt=x,S(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:T,fault:R.fault}))}ye&&(e.buildFailed=!1);const L=(e.faults??=new pod_output_1.PodFaults).observe(R);if(L){e.lastErrorEmitSig=void 0;const x=L.open.length===0;S(e,"info",`${x?"service recovered":`${L.closed.join(" + ")} recovered`}: ${e.project}`,`cluster-service:${e.project}`,{kind:x?"service-recovered":"service-fault-closed",project:e.project,line:T,closed:L.closed,open:L.open})}!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(f)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${o}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),S(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(q(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),X(e,o),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&U(e)},1500).unref?.())},250).unref?.())}},"sniff"),$=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0,A=s(k=>{const h=(0,own_log_1.linePrefixer)(e.project);return j=>{if(t.stopped())return;const R=(0,inspector_secret_1.redactInspectorUrls)(j.toString());u(R);const T=h(R);k.write(T),$?.(T)}},"relay");return l.stdout?.on("data",A(process.stdout)),l.stderr?.on("data",A(process.stderr)),l.on("exit",(k,h)=>{if(e.proc!==l||(e.proc=void 0,e.state="down",t.stopped()))return;const j=(0,exit_reason_1.describeExit)(k,h);if(console.log(`[gateway] ${o} exited (${k??"no code"}${h?`, ${h}`:""}${e.inflight?`, ${e.inflight} request(s) in flight`:""})${j?` \u2014 ${j}`:""}`),S(e,k===0?"info":"warning",`service down: ${e.project} (exit ${k})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:k,...h?{signal:h}:{}}),k===0||e.buildFailed)return;const R=Date.now();if(e.restarts=(e.restarts??[]).filter(K=>R-K<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){S(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(R);const T=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),S(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),U(e))},T)}),l},"launchServiceProcess"),U=s(e=>{if(t.stopped())return;const r=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,n=Math.max(1,e.decl.minPods??1);c.reapForeign!==!1&&X(e,r),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${r} (pods=${n})\u2026`),S(e,"info",`service starting: ${e.project} (${n} pod${n>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:n}),e.state="starting",e.spawnAt=Date.now(),e.generation=(e.generation??0)+1,e.proc=H(e,e.generation,"active")},"spawnService"),ke=s(async e=>{const r=e.pool.socks[0];if(!r)return;const n=await(0,pod_probe_1.probePodAlive)(r,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);n&&(e.activeWork=n.activeWork,n.watcher&&(e.watcher=n.watcher))},"refreshActiveWork"),je=s(e=>{try{const r=Number(fs.readFileSync(`${e}.owner`,"utf8").trim());return Number.isInteger(r)&&r>1?r:void 0}catch{return}},"readOwnerSidecar"),Re=s(async e=>{const r=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!r)return[];const n=(0,node_crypto_1.createHash)("sha256").update(r).digest("hex").slice(0,8),o=[];for(const i of e.pool.socks){const l=await(0,pod_probe_1.probePodAlive)(i,void 0,r);l&&(!l.keyFingerprint||l.keyFingerprint!==n)&&o.push({sock:i,pid:l.pid})}return o},"staleAdoptedSocks"),Ce=s((e,r)=>{const n=r.map(o=>o.sock);for(const{sock:o,pid:i}of r){const l=chooseStalePodKillTarget(i,je(o),process.pid);if(l!==void 0)try{process.kill(l,"SIGTERM")}catch{}try{fs.unlinkSync(o)}catch{}try{fs.unlinkSync(`${o}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(o=>!n.includes(o)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),ce=s((e,r)=>{if(e.state==="up")return!1;e.state="up",e.wedgeRecycles=0;const n=e.pool.socks.length;return S(e,"info",`service up: ${e.project} (${n} pods, ${r})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:n,adopted:r==="adopted"}),!0},"markUp"),de=s(async e=>{if(N(e.pool)>0){const o=await Re(e);if(o.length>0&&(console.error(`[gateway] ${e.project}: ${o.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),S(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:o.length}),e.proc&&q(e,"stale adoption (foreign east-west key)"),Ce(e,o)),e.pool.socks.length>0)return ce(e,"adopted"),!0}if(t.stopped())return!1;let r=!1;const n=s(()=>e.state!=="starting"&&!e.proc,"notStarted");if(n())if(e.coldWait)await e.coldWait;else{_>=y&&(console.log(`[gateway] cold start queued: ${e.project}${e.wsKey&&e.wsKey!==t.wsKey?` (${e.wsKey})`:""} \u2014 ${_} starting, ${D.length} ahead (LENSMCP_MAX_COLD_STARTS=${y}).`),S(e,"info",`cold start queued: ${e.project}`,`cluster-service:${e.project}`,{kind:"cold-start-queued",project:e.project,inFlight:_,ahead:D.length}));const o=O();if(e.coldWait=o,await o,e.coldWait=void 0,r=!0,t.stopped())return P(),!1;n()&&U(e)}try{return await Ae(e)}finally{r&&P()}},"ensureUp"),Ae=s(async e=>{const r=Date.now()+t.startupTimeoutMs,n=Date.now()+12e3;for(;!t.stopped()&&Date.now()<r;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>n)return!1;if(N(e.pool)>0){e.state="up",e.buildFailed=!1;const o=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${o?`, cold start ${o}ms`:""})`),S(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${o??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:o}),!0}if(e.state==="down")return!1}return!1},"waitForPods"),Te=s(e=>{const r=e.decl.maxPods??e.decl.minPods??1,n=e.pool.socks.length,o=scaleTarget(e);!o||n===0||n>=r||e.inflight<=n||Date.now()-e.lastScaleAt<5e3||(e.lastScaleAt=Date.now(),postScale(o,n+1,c.scaleLookup),console.log(`[gateway] autoscale ${e.project}: ${n} \u2192 ${n+1} pods (inflight ${e.inflight})`),S(e,"info",`autoscale: ${e.project} ${n} \u2192 ${n+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:n,to:n+1,inflight:e.inflight}))},"scaleUp"),q=s((e,r,n)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${r} \u2014 scaling to zero.`),n?.announce!==!1&&S(e,"info",`idle kill: ${e.project} (${r})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:r});const o=e.proc;try{o.stdout?.removeAllListeners("data"),o.stderr?.removeAllListeners("data"),o.removeAllListeners("exit")}catch{}(0,discovery_1.stopRoutingTo)(e.pool),E(o,"SIGTERM"),e.proc=void 0,e.state="down",setTimeout(()=>{ie(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),ie=s(e=>{try{for(const r of fs.readdirSync(e.pool.dir)){if(!r.endsWith(".sock"))continue;const n=path.join(e.pool.dir,r);try{const o=Number(fs.readFileSync(`${n}.owner`,"utf8").trim());if(Number.isFinite(o)&&o>0)try{process.kill(o,0);continue}catch{}}catch{}try{fs.unlinkSync(n)}catch{}try{fs.unlinkSync(`${n}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks");let Y=0;const pe=[],Me=s(()=>Y<B?(Y+=1,Promise.resolve()):new Promise(e=>{pe.push(()=>{Y+=1,e()})}),"acquireRollSlot"),xe=s(()=>{Y-=1,pe.shift()?.()},"releaseRollSlot"),_e=s(async(e,r,n)=>{N(e.pool);for(const o of e.pool.socks){const i=await g(o,void 0,n);if(i&&r(i.podGeneration))return!0}return!1},"anyPodAnswers"),ue=new Map,Pe=s((e,r,n)=>{const o=C>0&&(0,handoff_retire_1.requestHandoffRetire)(e.pool.dir,n);if(E(r,"SIGTERM"),!o)return;const i=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0,l=s(R=>T=>{if(t.stopped())return;const K=(0,inspector_secret_1.redactInspectorUrls)(T.toString()).replace(/^(?=.)/gm,`[${e.project} g${n} retiring] `);R.write(K),i?.(K)},"say"),p=l(process.stdout),u=l(process.stderr);r.stdout?.on("data",p),r.stderr?.on("data",u);let $=!1;const A=s(()=>{$||($=!0,clearTimeout(k),clearInterval(j),r.stdout?.removeListener("data",p),r.stderr?.removeListener("data",u),ue.delete(r),(0,handoff_retire_1.clearHandoffRetire)(e.pool.dir,n),ie(e),e.pool.scannedAt=0)},"end"),k=setTimeout(()=>{console.error(`[gateway] pod handoff: ${e.project} g${n} was still running ${C}ms after it was retired \u2014 killing it.`),M(r,"SIGKILL"),A()},C);k.unref?.();const h=s(()=>{if(!$){if(t.stopped()){A();return}I(r)||(console.log(`[gateway] pod handoff: ${e.project} g${n} finished its in-flight work and exited.`),A())}},"look"),j=setInterval(h,se);j.unref?.(),r.once("exit",h),ue.set(r,()=>{M(r,"SIGTERM"),A()})},"retireReplacedTree"),De=s(()=>{for(const e of[...ue.values()])e()},"stopRetiringTrees"),he=s((e,r)=>{if(e.rolling)return e.rolling;const n=e.proc;if(!n||t.stopped())return Promise.resolve(!1);if(e.launcher==="nx")return console.log(`[gateway] pod handoff skipped: ${e.project} runs through nx run, which starts one instance of a target at a time \u2014 no standby can overlap it (reason=${r}).`),Promise.resolve(!1);Y>=B&&console.log(`[gateway] pod handoff queued: ${e.project} reason=${r} \u2014 ${Y} roll(s) in flight, ${pe.length} ahead (LENSMCP_POD_MAX_CONCURRENT_ROLLS=${B}).`);const o=(async()=>{await Me();try{return await Le(e,n,r)}finally{xe()}})().finally(()=>{e.rolling=void 0});return e.rolling=o,o},"rollPod"),Le=s(async(e,r,n)=>{const o=e.proc;if(o!==r||t.stopped())return!1;const i=e.generation??0,l=i+1,p=Date.now();e.lastStaleRecycleAt=p,console.log(`[gateway] pod handoff start: ${e.project} g${i}\u2192g${l} reason=${n} \u2014 standby spawning, old generation keeps serving.`),S(e,"info",`pod handoff starting: ${e.project} (${n})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:n,generation:l,fromGeneration:i});const u=H(e,l,"standby"),$=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],A=p+J;let k;for(;!t.stopped()&&Date.now()<A&&!(u.exitCode!==null||u.signalCode!==null);){N(e.pool);for(const h of e.pool.socks)if((await g(h,void 0,$))?.podGeneration===l){k=h;break}if(k)break;await sleep(ne)}if(!k||t.stopped()){const h=t.stopped()?"gateway stopping":u.exitCode!==null||u.signalCode!==null?`standby exited (${u.exitCode??u.signalCode})`:`standby not ready within ${Math.round(J/1e3)}s`;try{u.stdout?.removeAllListeners("data"),u.stderr?.removeAllListeners("data")}catch{}(0,discovery_1.stopRoutingTo)(e.pool,T=>(0,pod_sock_1.podSockGeneration)(T)===l),E(u,"SIGTERM");const j=!t.stopped()&&e.proc===o&&o.exitCode===null&&o.signalCode===null&&await _e(e,T=>T!==l,$),R=j?`old generation g${i} kept serving`:`and the old generation g${i} is NOT serving (${e.proc!==o||o.exitCode!==null||o.signalCode!==null?"its tree is gone":"none of its pods answers"}) \u2014 the service is down until it recovers`;return console.error(`[gateway] pod handoff failed: ${e.project} g${l}: ${h} \u2014 ${R}.`),e.rollFailure=`${h} \u2014 ${R}`,S(e,j?"warning":"error",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:n,generation:l,error:h,durationMs:Date.now()-p,oldServing:j}),setTimeout(()=>ie(e),1200).unref?.(),!1}if(e.proc!==o)return(0,discovery_1.stopRoutingTo)(e.pool,h=>(0,pod_sock_1.podSockGeneration)(h)===l),E(u,"SIGTERM"),!1;e.proc=u,e.launcher=ee.get(u),e.generation=l,e.spawnAt=p,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{o.stdout?.removeAllListeners("data"),o.stderr?.removeAllListeners("data"),o.removeAllListeners("exit")}catch{}return(0,discovery_1.stopRoutingTo)(e.pool,h=>(0,pod_sock_1.podSockGeneration)(h)!==l&&path.join(e.pool.dir,h)!==k),Pe(e,o,i),setTimeout(()=>{ie(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${l} answered on ${path.basename(k)} in ${Date.now()-p}ms; retiring g${i}.`),S(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:n,generation:l,fromGeneration:i,durationMs:Date.now()-p}),!0},"runRoll"),be=s((e,r)=>{if(t.stopped())return{refused:"the gateway is stopping"};if(e.restart&&!e.restart.done)return{restart:e.restart,already:!0};if(!e.proc&&e.state!=="down")return{refused:`${e.project} is served by pods this gateway adopted from an earlier one \u2014 it has no tree of its own to replace. \`lensmcp gateway restart\` starts every service under this gateway.`};const n=e.proc?r.hard||e.launcher==="nx"||e.state!=="up"?"hard":"rolling":"start",o={how:n,reason:r.reason,startedAt:Date.now(),...e.proc&&e.generation!==void 0?{fromGeneration:e.generation}:{}};return e.restart=o,console.log(`[gateway] restart asked: ${e.project}${e.wsKey!==t.wsKey?` (${e.wsKey})`:""} \u2014 ${n} (${r.reason}).`),S(e,"info",`restart asked: ${e.project} (${n})`,`cluster-service:${e.project}`,{kind:"service-restart",phase:"started",project:e.project,how:n,reason:r.reason,...o.fromGeneration!==void 0?{fromGeneration:o.fromGeneration}:{}}),s(async()=>{if(n==="start")return await de(e)?{ok:!0}:{ok:!1,error:ge(e)};if(n==="rolling")return e.rolling&&await e.rolling,e.proc?(e.rollFailure=void 0,await he(e,r.reason)?{ok:!0}:{ok:!1,error:e.rollFailure??"the new tree did not take over"}):await de(e)?{ok:!0}:{ok:!1,error:ge(e)};if(q(e,`restart asked (${r.reason})`,{announce:!1}),await sleep(1500),t.stopped())return{ok:!1,error:"the gateway is stopping"};let i=!1;e.proc||(await O(),i=!0,!e.proc&&e.state!=="starting"&&U(e));try{const l=e.proc,p=e.generation,u=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],$=Date.now()+t.startupTimeoutMs;for(;!t.stopped()&&Date.now()<$;){if(!l||e.proc!==l||l.exitCode!==null||l.signalCode!==null)return{ok:!1,error:ge(e)};if(await _e(e,A=>A===p,u))return ce(e,"observed"),e.buildFailed=!1,{ok:!0};await sleep(ne)}return{ok:!1,error:t.stopped()?"the gateway is stopping":`no pod of the new tree answered within ${Math.round(t.startupTimeoutMs/1e3)}s`}}finally{i&&P()}},"run")().catch(i=>({ok:!1,error:i.message})).then(i=>{o.done={ok:i.ok,at:Date.now(),...e.generation!==void 0?{generation:e.generation}:{},...i.error?{error:i.error}:{}};const l=Math.round((o.done.at-o.startedAt)/100)/10;i.ok?console.log(`[gateway] restarted: ${e.project} \u2014 g${o.done.generation??"?"} serving after ${l}s (${n}).`):console.error(`[gateway] restart failed: ${e.project} \u2014 ${i.error} (${n}, after ${l}s).`),S(e,i.ok?"info":"error",i.ok?`restarted: ${e.project}`:`restart failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-restart",phase:i.ok?"done":"failed",project:e.project,how:n,reason:r.reason,...o.fromGeneration!==void 0?{fromGeneration:o.fromGeneration}:{},...o.done.generation!==void 0?{generation:o.done.generation}:{},durationMs:o.done.at-o.startedAt,...i.error?{error:i.error}:{}})}),{restart:o,already:!1}},"restartService"),ge=s(e=>e.buildFailed?`its build failed${e.lastErrors?.length?`: ${e.lastErrors[e.lastErrors.length-1]}`:""}`:e.proc&&(e.proc.exitCode!==null||e.proc.signalCode!==null)?`its tree exited (${e.proc.exitCode??e.proc.signalCode})`:!e.proc&&e.state==="down"?"its tree exited before a pod answered":`no pod answered within ${Math.round(t.startupTimeoutMs/1e3)}s`,"startFailure"),Ee=s(e=>{const r=Date.now();e.restarts=(e.restarts??[]).filter(o=>r-o<types_1.SERVICE_RESTART_WINDOW_MS);const n=(e.wedgeRecycles??0)+1;if(e.restarts.length>=types_1.SERVICE_MAX_RESTARTS||n>types_1.SERVICE_MAX_RESTARTS){const o=e.restarts.length>=types_1.SERVICE_MAX_RESTARTS?`wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min`:`recycled ${types_1.SERVICE_MAX_RESTARTS}\xD7 in a row and no pod came up`;S(e,"error",`service crash-looping: ${e.project} \u2014 ${o}, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project,recycles:n-1}),q(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.wedgeRecycles=n,e.restarts.push(r),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; attempt ${n}/${types_1.SERVICE_MAX_RESTARTS} with no pod up since the first, ${e.restarts.length} in the last minute; ${e.inflight??0} request(s) in flight \u2014 jobs held by the dead pod keep their locks until they expire)`),S(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:n,inWindow:e.restarts.length}),q(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&U(e)},1500).unref?.()},"recycleWedged"),Ie={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let fe;const te=new Map,we=new Map;let Se=!1,ve=0,le=0;const Oe=s(()=>{const e=t.projectRoots??{},r=new Set(t.services.map(o=>o.project));for(const o of t.routes)o.lens&&r.add(o.lens.project);const n=new Set;for(const o of r){const i=e[o];if(!i)continue;const l=path.relative(t.root,i).split(path.sep).filter(Boolean)[0];l&&!l.startsWith("..")&&n.add(l)}return[...n]},"managedBuckets"),Ne=s((e,r,n,o)=>{const i=t.projectSourceRoots?.[e];if(i?.length)return i;const l=t.projectRoots?.[e];if(!l)return;const p=sourceScopeDirs(t.root,l,n,r,o);return p.length>0?p:void 0},"scopeFor"),Fe=s(()=>{if(Se||Date.now()-ve<types_1.POD_STALE_SCAN_MS)return;Se=!0,ve=Date.now();const e=ve;(async()=>{const r=(0,scope_1.readSourceSetOptions)(t.root),n=r.exclude,o={ignoreTests:r.ignoreTests},i=sourceScanPass(t.root,n,o),l=await i.snapshot(),p=Date.now(),u=sourceChangeWindowMs(p,le,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),$=fe?diffSourceSets(fe,l,p,u):null;if($&&(t.sourceSetChangedAt=p,t.sourceSetChange=$,V("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:$.added,removed:$.removed,truncated:$.truncated})),fe=l,!t.projectRoots){le=e;return}let A;try{A=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(f=>f.isDirectory()).map(f=>f.name)}catch{return}const k=Oe(),h=new Set(t.services.map(f=>f.project));for(const f of t.routes)f.lens&&h.add(f.lens.project);const j=new Map;for(const f of h){const L=Ne(f,A,k,n);if(!L)continue;const x=L.join("|"),W=j.get(x)??{dirs:L,projects:[]};W.projects.push(f),j.set(x,W)}const R=Date.now(),T=sourceChangeWindowMs(R,le,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);le=e;const K=s((f,L,x)=>{t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const W of f)t.sourceSetChangedFor.set(W,L),t.sourceSetChangesFor.set(W,x)},"stampScoped"),ye=new Set([...j.values()].flatMap(f=>f.projects));for(const f of[...t.sourceSetScoped??[]])ye.has(f)||t.sourceSetScoped.delete(f);for(const[f,{dirs:L,projects:x}]of j){const W=await i.snapshot(L),$e=te.get(f);if($e||x.some(b=>we.has(b))){t.sourceSetScoped??=new Set;for(const b of x)t.sourceSetScoped.add(b)}te.set(f,W);const me=new Map;for(const b of x){const G=we.get(b);if(G===void 0||G===f)continue;const z=me.get(G)??[];z.push(b),me.set(G,z)}for(const b of x)we.set(b,f);for(const[b,G]of me){const z=te.get(b),oe=z?diffSourceSets(z,W,R,T):$;oe&&(K(G,z?R:p,oe),V("info",`source file set changed \u2014 ${G.join(", ")}'s watch scope was re-keyed`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,rescoped:!0,projects:G,added:oe.added,removed:oe.removed,truncated:oe.truncated}))}if(!$e)continue;const re=diffSourceSets($e,W,R,T);re&&(K(x,R,re),V("info",`source file set changed in ${x.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:x,added:re.added,removed:re.removed,truncated:re.truncated}))}for(const f of[...te.keys()])j.has(f)||te.delete(f)})().catch(()=>{}).finally(()=>{Se=!1})},"maybeScanSourceSet"),Ke=s((e,r)=>{const n=podChangeClock(t,e.project),o=n.change,i=`${n.scope==="project"?"in its scope":"in the workspace (its scope is not known)"}: ${changeEvidence(o)}`,l=o?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,p=podWatcherVerdict(e.watcher,r,l),u=s(A=>(e.staleKeptAt!==r&&(e.staleKeptAt=r,console.log(`[gateway] stale source set: pod ${e.project} ${A} \u2014 no recycle.`)),!1),"keep");if(p==="heard")return e.staleVerifiedAt=r,S(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:p}),u(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`);if(p==="degraded")return console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0;const $=s(A=>(e.staleKeptAt!==r&&(e.staleKeptAt=r,console.log(`[gateway] stale source set: pod ${e.project} ${A}; the change ${i} \u2014 recycling.`)),!0),"recycling");return p==="silent"?types_1.POD_WATCHER_SILENT==="keep"?u("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):$("watcher alive but quiet across the change"):types_1.POD_WATCHER_UNVERIFIED==="keep"?u("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):$("watcher unverifiable (no testimony yet \u2014 a pod that never rebuilt, or an older devserver bundle)")},"podStaleRecycleAllowed"),We=s((e,r)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc&&e.launcher!=="nx"){he(e,r);return}console.log(`[gateway] zombie recycle: ${e.project} (${r}) \u2014 stale fs view; fresh pod incoming`),S(e,"warning",`zombie recycle: ${e.project} (${r})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:r});const n=!!e.decl.eager;q(e,`${r} \u2014 recycling stale pod`,{announce:!1}),n&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&U(e)},1500).unref?.()},"recyclePod"),qe=s(()=>{const e=setInterval(()=>{Ge().catch(r=>console.error("[gateway] sweep error:",r))},t.sweepMs);return e.unref(),e},"startSweeper"),Ge=s(async()=>{{if(t.stopped())return;for(const e of t.graphStores?.all()??[])e.refreshIfStale();types_1.POD_RECYCLE_ENABLED&&Fe();for(const e of t.registry?t.registry.allServices():t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const r=path.resolve(e.root)===path.resolve(t.root),n=e.decl.idleKillSec;if(n&&e.inflight===0&&Date.now()-e.lastUsed>n*1e3){q(e,`idle ${n}s`),e.wedgedSince=void 0;continue}const o=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,i=N(e.pool)>0;if(i||o){if(e.wedgedSince=void 0,i&&ce(e,"observed"),i&&!o&&types_1.POD_RECYCLE_ENABLED&&r){const l=podChangeClock(t,e.project).at;await ke(e);const p=(e.staleVerifiedAt??0)>=l&&l>0,u=podRecycleReason(e,Date.now(),p?0:l,Ie);if(u==="stale-source-set"&&!Ke(e,l))continue;u&&We(e,u)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,Ee(e))}}},"sweepOnce");return{scanNow:N,spawnService:U,ensureUp:de,scaleUp:Te,killSpawned:q,markUp:ce,startSweeper:qe,rollPod:he,restartService:be,recycleWedged:Ee,stopRetiringTrees:De,stopBuildHost:s(e=>e===void 0?m.stop():m.release(e),"stopBuildHost")}}v(createServiceLayer,"createServiceLayer"),s(createServiceLayer,"createServiceLayer");function chooseStalePodKillTarget(t,a,c){if(!(t===void 0||!Number.isInteger(t)||t<=1)&&!(a!==void 0&&a!==t)&&t!==c)return t}v(chooseStalePodKillTarget,"chooseStalePodKillTarget"),s(chooseStalePodKillTarget,"chooseStalePodKillTarget");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep the daemon's own `gateway.log` bounded while it runs (issues/closed/foodguard-77).
|
|
3
|
+
*
|
|
4
|
+
* `gateway.log` is not a file the gateway opens: `lensmcp gateway start` opens it for append and hands it to the daemon
|
|
5
|
+
* as stdout and stderr (`cli.ts`), and the nx wrapper, the gateway and every service line relayed through it write
|
|
6
|
+
* there. The CLI rotated it only at start, so a long run grew without bound — FoodGuard's 1.25.0 run left 410 MB in
|
|
7
|
+
* 19 hours, its 1.25.2 run ~108 MB an hour (TypeORM's slow-query SQL, ~40 KB a statement), and the next start
|
|
8
|
+
* overwrote the one generation it kept.
|
|
9
|
+
*
|
|
10
|
+
* A rename would not do — every writer's descriptor would follow the renamed file. So: copy, then truncate. The
|
|
11
|
+
* descriptor is opened for append, so every writer goes on at the new end. Generations shift `.1` → `.2` → … → `.N`,
|
|
12
|
+
* the oldest dropped (the CLI's start-time sweep shifts the same way). Lines written between the copy and the
|
|
13
|
+
* truncate — a few milliseconds of a 50 MB file — are lost; that is the price of not owning the descriptor.
|
|
14
|
+
*
|
|
15
|
+
* Only when this process's stdout IS `<root>/.lensmcp/gateway.log` (same device and inode): a gateway run in a
|
|
16
|
+
* terminal, or under a test, has nothing to rotate.
|
|
17
|
+
*/
|
|
18
|
+
export declare const OWN_LOG_MAX_BYTES: number;
|
|
19
|
+
export declare const OWN_LOG_GENERATIONS = 3;
|
|
20
|
+
/** `<file>.1` … `.N-1` move up one; the oldest goes. */
|
|
21
|
+
export declare function shiftGenerations(file: string, keep: number): void;
|
|
22
|
+
export interface OwnLogOptions {
|
|
23
|
+
/** The descriptor to watch and truncate — this process's stdout unless a spec hands another. */
|
|
24
|
+
fd?: number;
|
|
25
|
+
maxBytes?: number;
|
|
26
|
+
keep?: number;
|
|
27
|
+
/** A line in the new file saying where the old lines went. */
|
|
28
|
+
note?: (line: string) => void;
|
|
29
|
+
}
|
|
30
|
+
/** One look: rotates when `fd` is `file` and over the cap. Returns whether it rotated. */
|
|
31
|
+
export declare function rotateOwnLog(file: string, opts?: OwnLogOptions): boolean;
|
|
32
|
+
/** Look every 30 s while the gateway runs; the returned function stops it. */
|
|
33
|
+
export declare function startOwnLogRotation(root: string, opts?: OwnLogOptions & {
|
|
34
|
+
everyMs?: number;
|
|
35
|
+
}): () => void;
|
|
36
|
+
/**
|
|
37
|
+
* A service's output, every line tagged with it. The relay used to tag a chunk — so a multi-line record (TypeORM's
|
|
38
|
+
* «query is slow» with its SQL and parameters, a stack) had one `[engine]` on its first line and nothing on the rest,
|
|
39
|
+
* and a chunk that began mid-line put the tag in the middle of one. Per stream: whether the next byte starts a line.
|
|
40
|
+
*/
|
|
41
|
+
export declare function linePrefixer(project: string): (text: string) => string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";var _=Object.defineProperty;var s=(e,t)=>_(e,"name",{value:t,configurable:!0});var u=Object.defineProperty,a=s((e,t)=>u(e,"name",{value:t,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.OWN_LOG_GENERATIONS=exports.OWN_LOG_MAX_BYTES=void 0,exports.shiftGenerations=shiftGenerations,exports.rotateOwnLog=rotateOwnLog,exports.startOwnLogRotation=startOwnLogRotation,exports.linePrefixer=linePrefixer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));exports.OWN_LOG_MAX_BYTES=50*1024*1024,exports.OWN_LOG_GENERATIONS=3;const CHECK_EVERY_MS=3e4;function shiftGenerations(e,t){try{fs.unlinkSync(`${e}.${t}`)}catch{}for(let r=t-1;r>=1;r--)try{fs.renameSync(`${e}.${r}`,`${e}.${r+1}`)}catch{}}s(shiftGenerations,"shiftGenerations"),a(shiftGenerations,"shiftGenerations");function rotateOwnLog(e,t={}){const r=t.fd??1,o=t.maxBytes??exports.OWN_LOG_MAX_BYTES,i=t.keep??exports.OWN_LOG_GENERATIONS;let n,c;try{n=fs.fstatSync(r),c=fs.statSync(e)}catch{return!1}if(!n.isFile()||n.dev!==c.dev||n.ino!==c.ino||n.size<=o)return!1;try{shiftGenerations(e,i),fs.copyFileSync(e,`${e}.1`);try{fs.chmodSync(`${e}.1`,384)}catch{}fs.ftruncateSync(r,0)}catch{return!1}return(t.note??(f=>{try{fs.writeSync(r,`${f}
|
|
2
|
+
`)}catch{}}))(`[gateway] ${path.basename(e)} passed ${Math.round(o/1048576)} MB \u2014 rotated; the lines before are in ${path.basename(e)}.1 (\u2026${i} kept)`),!0}s(rotateOwnLog,"rotateOwnLog"),a(rotateOwnLog,"rotateOwnLog");function startOwnLogRotation(e,t={}){const r=path.join(e,".lensmcp","gateway.log"),o=setInterval(()=>rotateOwnLog(r,t),t.everyMs??CHECK_EVERY_MS);return o.unref?.(),()=>clearInterval(o)}s(startOwnLogRotation,"startOwnLogRotation"),a(startOwnLogRotation,"startOwnLogRotation");function linePrefixer(e){let t=!0;const r=`[${e}] `;return o=>{let i="";for(const n of o.split(/(?<=\n)/))n&&(i+=(t?r:"")+n,t=n.endsWith(`
|
|
3
|
+
`));return i}}s(linePrefixer,"linePrefixer"),a(linePrefixer,"linePrefixer");
|
|
@@ -56,6 +56,7 @@ export interface WedgeDetail {
|
|
|
56
56
|
/** Why the last probe found none — `deadline` when no probe is configured (`LENSMCP_POD_SLOW_FIRST_BYTE_MS=0`). */
|
|
57
57
|
failure: PodProbeFailure | 'deadline';
|
|
58
58
|
}
|
|
59
|
+
export declare function upstreamOpen(req: http.IncomingMessage): boolean;
|
|
59
60
|
/** What the client reads in the 502 — what happened, and what it means for the call (issues/closed/foodguard-54). */
|
|
60
61
|
export declare function wedgedMessage(podName: string, project: string, d: WedgeDetail): string;
|
|
61
62
|
export declare function createProxy(rt: GatewayRuntime, obs: Observability, svcLayer: ServiceLayer): ProxyLayer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var ee=Object.defineProperty;var D=(t,d)=>ee(t,"name",{value:d,configurable:!0});var Z=Object.defineProperty,s=D((t,d)=>Z(t,"name",{value:d,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.wedgedMessage=wedgedMessage,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"),gc_notice_1=require("../../../gc-notice"),service_keys_1=require("./service-keys");function isConnError(t){const d=t.code??"";return d==="ECONNREFUSED"||d==="ECONNRESET"||d==="ETIMEDOUT"||d==="EPIPE"||d==="EHOSTUNREACH"||d==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(t.message)}D(isConnError,"isConnError"),s(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(t){if(!IDEMPOTENT_METHODS.has((t.method??"GET").toUpperCase())||t.headers["transfer-encoding"]!==void 0)return!1;const d=t.headers["content-length"];return!(d!==void 0&&String(d).trim()!=="0")}D(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(t,d){return d.headersSent||d.writableEnded||d.destroyed===!0||t.aborted===!0}D(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"];function returnRequestId(t,d){if(t["x-request-id"]!==void 0)return;const f=d.headers["x-request-id"],O=Array.isArray(f)?f[0]:f;typeof O=="string"&&O!==""&&(t["x-request-id"]=O)}D(returnRequestId,"returnRequestId"),s(returnRequestId,"returnRequestId");const httpProxy=require("http-proxy");function declaredFirstByteMs(t){const d=Number(t);return Number.isFinite(d)&&d>0?d:types_1.POD_RESPONSE_TIMEOUT_MS}D(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");const secs=s(t=>`${(t/1e3).toFixed(1)}s`,"secs");function wedgedMessage(t,d,f){const O=f.failure==="gone"?"its process is gone":f.failure==="silent"?`it did not answer its liveness check for ${secs(f.silentMs)}`:"it gave no liveness answer";return`Pod ${t} of ${d} sent no response for ${secs(f.heldMs)} and ${O} \u2014 taken out of rotation, and the service is being restarted, so work it was still doing for this request may have been cut short. Retry if the call is safe to repeat.`}D(wedgedMessage,"wedgedMessage"),s(wedgedMessage,"wedgedMessage");function armFirstByteGuard(t){let d,f=!1,O=!1,x,R=0,$=(0,body_stall_1.requestBodyArriving)(t.req)?void 0:Date.now();const q=s(()=>{$??=Date.now()},"onBodyEnd");$===void 0&&t.req.once("end",q);const L=s(()=>($===void 0&&!(0,body_stall_1.requestBodyArriving)(t.req)&&($=Date.now()),$===void 0?0:Date.now()-$),"heldMs"),U=s(b=>{d=setTimeout(V,b),d.unref?.()},"arm"),V=s(()=>{if(f||!t.pending())return;const b=L();if(t.ceilingMs<=0){if($!==void 0&&b>=t.firstByteMs){t.onWedged(`no response within ${b}ms (wedged socket) \u2014 evicted + respawning`,{heldMs:b,silentMs:0,probes:0,failure:"deadline"});return}U($===void 0?t.firstByteMs:t.firstByteMs-b);return}(0,pod_probe_1.probePodAliveVerdict)(t.socketPath,void 0,t.serviceKey).then(({alive:A,failure:C})=>{if(f||!t.pending())return;const T=L();if(!A){if(R+=1,C==="silent"&&(0,gc_notice_1.inAnnouncedGc)(t.socketPath)){x=void 0,U(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}if(C==="silent"&&types_1.POD_WEDGE_CONFIRM_MS>0&&(x??=Date.now(),Date.now()-x<types_1.POD_WEDGE_CONFIRM_MS)){U(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}const N=x===void 0?0:Date.now()-x,H=C==="gone"?"its socket no longer accepts (the pod process is gone)":C==="silent"?`nothing answered on ${types_1.POD_ALIVE_PATH} for ${N}ms (${R} probe${R===1?"":"s"})`:`no liveness answer on ${types_1.POD_ALIVE_PATH}`;t.onWedged(`no response within ${T}ms and ${H} (wedged socket) \u2014 evicted + respawning`,{heldMs:T,silentMs:N,probes:R,failure:C??"no-route"});return}if(x=void 0,T>=t.ceilingMs){t.onCeiling(`alive on ${types_1.POD_ALIVE_PATH} but no response within ${T}ms (ceiling ${t.ceilingMs}ms) \u2014 giving up on this request, pod kept`,A);return}!O&&$!==void 0&&(O=!0,t.onSlow(T,A)),U(Math.max(t.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))})},"onDeadline");return U(t.firstByteMs),{clear:s(()=>{f=!0,d&&clearTimeout(d),t.req.removeListener("end",q)},"clear")}}D(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(t,d,f){const{recordEdge:O,finishTrace:x,traceStep:R,emit:$}=d,q=s((e,r,o,c)=>{const p=c.kind==="pod-evicted";$(p?"error":"warning",`${p?"pod evicted":"request given up"}: ${e.project} (${o}) \u2014 ${c.status}`,`cluster-pod-incident:${e.project}`,{kind:c.kind,project:e.project,pod:o,status:c.status,reason:c.reason,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0],...c.detail??{}})},"reportPodIncident"),L=s(async(e,r)=>{const o=e.pool;if(!o)return;o.scannedAt=0;let c=(0,discovery_1.pickSock)(o,t.scanTtlMs);return!c&&e.svc&&(R(r,"cold-start",{project:e.project}),await f.ensureUp(e.svc)&&(o.scannedAt=0,c=(0,discovery_1.pickSock)(o,t.scanTtlMs))),c},"anotherPod"),U=s((e,r,o,c,p)=>{console.log(`[gateway] pod ${o} of ${e.project} is gone \u2014 its ${r.method} was not answered, sending it to ${c}`),$("info",`request moved to another pod: ${e.project} (${o} \u2192 ${c})`,`cluster-pod-moved:${e.project}`,{kind:"pod-gone-rerouted",project:e.project,pod:o,to:c,reason:p,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0]})},"reportPodMoved"),V=s(async e=>{if(!e.lens||!t.lensSupervisor)return"unsupported";let r;const o=new Promise(g=>{r=setTimeout(()=>g("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),c=t.lensSupervisor.recover(e.lens,"proxy-connection").then(g=>g?"ready":"failed").catch(()=>"failed"),p=await Promise.race([c,o]);r&&clearTimeout(r);const S=t.lensSupervisor.status(e.lens);return p==="failed"&&(!S||S.state==="ready")?"unsupported":p},"waitForManagedRecovery"),b=s((e,r,o,c,p,S)=>{if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}const g=e.lens?t.lensSupervisor?.status(e.lens):void 0,_=S==="timeout"||g?.state==="starting"||g?.state==="replacing",P=(0,gateway_errors_1.ensureRequestId)(r),l=e.lens?{project:e.project,reason:g?.reason??"connection-failure",generation:g?.generation??0,..._?{retryAfterMs:2e3}:{}}:void 0,a=_?503:502,y=_?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${P} project=${e.project} target=${String(c)} state=${g?.state??"unmanaged"} generation=${g?.generation??0}: ${p.message}`),$("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:P,project:e.project,target:String(c),state:g?.state??"unmanaged",generation:g?.generation,error:p.message}),(0,edge_1.sendError)(o,r,a,y,_?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,l,_?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),A=(0,body_stall_1.createBodyStallGuard)(t.bodyStallMs??types_1.BODY_STALL_MS),C={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},T=new http.Agent(C),N=new(require("node:https")).Agent(C),H=s(e=>typeof e=="string"&&e.startsWith("https:")?N:T,"agentForTarget"),B=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:T}),Y=s((e,r,o)=>{const c=s(()=>{o()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",c),e.once("close",()=>r.removeListener("close",c))},"propagateEdgeCancel");B.on("proxyReq",(e,r,o)=>{for(const c of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(c);e.setHeader("connection","keep-alive"),Y(e,o,()=>o.writableEnded)}),B.on("proxyRes",(e,r,o)=>{const c=r.__lensmcpPod;c&&(e.headers["x-lensmcp-pod"]=c),delete e.headers["x-powered-by"],e.headers.server="LensMCP",returnRequestId(e.headers,r),o&&Y(e,o,()=>o.writableEnded),o&&A.armResponse(o)});const Q=s((e,r,o,c)=>{(0,edge_1.stampForwardingHeaders)(t,r),A.arm(r);const p={},S=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[n,i]of Object.entries(r.headers))i===void 0||n.startsWith(":")||HOP_BY_HOP.has(n)||S&&(n==="content-length"||n==="transfer-encoding")||(p[n]=i);let g=http,_;if(typeof e=="object")_={socketPath:e.socketPath,path:r.url,method:r.method,headers:p,agent:T};else{const n=new URL(e),i=n.protocol==="https:";g=i?require("node:https"):http,p.host=n.host,_={protocol:n.protocol,hostname:n.hostname,port:n.port||(i?443:80),path:r.url,method:r.method,headers:p,agent:H(e)}}const P=o;P.__lensmcpErrContained||(P.__lensmcpErrContained=!0,o.on("error",()=>{}));let l;const a=s(()=>{o.writableEnded||u.destroy()},"onReqGone"),y=s(()=>{l&&!l.destroyed&&l.destroy()},"onEdgeClose"),v=s(()=>{r.removeListener("error",a),r.removeListener("aborted",a),r.removeListener("close",a),o.removeListener("close",y)},"detach"),u=g.request(_,n=>{if(l=n,c.onResponse(),o.headersSent||o.writableEnded||o.destroyed){n.destroy();return}const i={};for(const[k,I]of Object.entries(n.headers))I===void 0||HOP_BY_HOP.has(k)||(i[k]=I);delete i["x-powered-by"],i.server="LensMCP",returnRequestId(i,r);const w=r.__lensmcpPod;w&&(i["x-lensmcp-pod"]=w);try{o.writeHead(n.statusCode??502,i)}catch(k){console.error("[gateway] h2 relay: bad response header, resetting stream:",k.message),n.destroy(),o.destroyed||o.destroy(k);return}n.pipe(o),A.armResponse(o);const j=s(()=>{!o.writableEnded&&!o.destroyed&&o.destroy()},"abortEdge");n.once("error",j),n.once("close",j),o.on("close",y)});u.once("error",n=>c.onError(n)),u.once("close",v),r.on("error",a),r.on("aborted",a),r.on("close",a),S?u.end():r.pipe(u)},"pipeUpstream"),X=s(async(e,r,o)=>{const c=r.__lensmcpTrace;if(e.pool){const l=e.svc;l&&(l.lastUsed=Date.now(),l.inflight+=1,o.on("close",()=>{l.inflight-=1,l.lastUsed=Date.now()}));let a=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs);if(!a&&l&&(R(c,"cold-start",{project:e.project}),await f.ensureUp(l)&&(a=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs))),l&&a&&f.markUp(l,"observed"),l&&f.scaleUp(l),!a){const v=l?.lastErrors?.length?` Service errors: ${l.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${v} The watcher recovers and the next request retries automatically.`);return}const y=s((v,u)=>{const n=path.basename(v,".sock");r.__lensmcpPod=n,R(c,"pod-select",{pod:n,pods:e.pool.socks.length,strategy:"round-robin",...u?{movedFrom:u}:{}});let i=!1;const w=s((h,M="unlink")=>{if(e.pool.socks=e.pool.socks.filter(E=>E!==v),e.pool.idx=-1,M==="unlink")try{fs.unlinkSync(v)}catch{}console.error(`[gateway] pod ${n} of ${e.project}: ${h}`)},"evictPod"),j=armFirstByteGuard({socketPath:v,serviceKey:t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!i&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:s((h,M)=>{i=!0,w(h),q(e,r,n,{kind:"pod-evicted",status:502,reason:h,detail:M}),(0,edge_1.sendError)(o,r,502,"unavailable",wedgedMessage(n,e.project,M))},"onWedged"),onCeiling:s(h=>{i=!0,w(h,"keep"),q(e,r,n,{kind:"request-gave-up",status:504,reason:h}),(0,edge_1.sendError)(o,r,504,"unavailable",`Pod ${n} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((h,M)=>{console.log(`[gateway] pod ${n} of ${e.project}: slow first byte, pod alive (${h}ms, ${M.inflight} in-flight, ${M.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>j.clear());const k=v,I=isReplayableRequest(r);let F=0;const W=s(()=>{F+=1,Q({socketPath:k},r,o,{onResponse:s(()=>{j.clear(),i=!0},"onResponse"),onError:s(h=>{if(!i){if(edgeGone(r,o)){j.clear(),i=!0;return}(0,pod_probe_1.podConnectionVerdict)(k,h).then(M=>{if(!i){if(M==="alive"&&F<2&&I&&!edgeGone(r,o)){console.log(`[gateway] pod ${n} of ${e.project}: ${h.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),W();return}if(j.clear(),i=!0,M==="gone"){w(h.message);const E=s(()=>{q(e,r,n,{kind:"pod-evicted",status:502,reason:`${h.message} \u2014 its socket no longer accepts (the pod process is gone)${u?`; the request had already been moved here from ${u}`:""}`}),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${n} of ${e.project} is gone (${h.message}) \u2014 retry.`)},"giveUp");if(u||!I){E();return}L(e,c).then(m=>{if(!edgeGone(r,o)){if(!m){E();return}U(e,r,n,path.basename(m,".sock"),h.message),y(m,n)}},E);return}console.error(`[gateway] pod ${n} of ${e.project}: ${h.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${n} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");W()},"onPod");y(a);return}const p=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let S=0,g=!1;const _=isReplayableRequest(r),P=s(()=>{if(edgeGone(r,o))return;const l=e.target;Q(l,r,o,{onResponse:s(()=>{},"onResponse"),onError:s(a=>{if(isConnError(a)&&(S+=1),e.lens&&isConnError(a)&&_&&S>=2&&!g&&!edgeGone(r,o)){g=!0,V(e).then(y=>{if(!edgeGone(r,o)){if(y==="ready"){P();return}if(y==="unsupported"&&Date.now()<p){setTimeout(P,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}b(e,r,o,e.target,a,y)}});return}if(isConnError(a)&&_&&Date.now()<p&&!edgeGone(r,o)){setTimeout(P,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}b(e,r,o,l,a)},"onError")})},"tryTcp");P()},"forwardNative");return{proxy:B,forward:s(async(e,r,o)=>{const c=Date.now(),p=r.__lensmcpTrace;if(o.on("close",()=>{O(e,Date.now()-c,o.statusCode??0,r.__lensmcpCaller),x(p,e.project,o.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)(t,r),A.arm(r),(r.httpVersionMajor??1)>=2){await X(e,r,o);return}if(e.pool){const a=e.svc;a&&(a.lastUsed=Date.now(),a.inflight+=1,o.on("close",()=>{a.inflight-=1,a.lastUsed=Date.now()}));let y=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs);if(!y&&a&&(R(p,"cold-start",{project:e.project}),await f.ensureUp(a)&&(y=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs))),a&&y&&f.markUp(a,"observed"),a&&f.scaleUp(a),!y){const u=a?.lastErrors?.length?` Service errors: ${a.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${u} The watcher recovers and the next request retries automatically.`);return}const v=s((u,n)=>{const i=path.basename(u,".sock");r.__lensmcpPod=i,R(p,"pod-select",{pod:i,pods:e.pool.socks.length,strategy:"round-robin",...n?{movedFrom:n}:{}});let w=!1;const j=s((E,m="unlink")=>{if(e.pool.socks=e.pool.socks.filter(G=>G!==u),e.pool.idx=-1,m==="unlink")try{fs.unlinkSync(u)}catch{}console.error(`[gateway] pod ${i} of ${e.project}: ${E}`)},"evictPod"),k=armFirstByteGuard({socketPath:u,serviceKey:t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!w&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:s((E,m)=>{w=!0,j(E),q(e,r,i,{kind:"pod-evicted",status:502,reason:E,detail:m}),(0,edge_1.sendError)(o,r,502,"unavailable",wedgedMessage(i,e.project,m))},"onWedged"),onCeiling:s(E=>{w=!0,j(E,"keep"),q(e,r,i,{kind:"request-gave-up",status:504,reason:E}),(0,edge_1.sendError)(o,r,504,"unavailable",`Pod ${i} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((E,m)=>{console.log(`[gateway] pod ${i} of ${e.project}: slow first byte, pod alive (${E}ms, ${m.inflight} in-flight, ${m.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>k.clear());const I=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),F=u,W=isReplayableRequest(r);let h=0;const M=s(()=>{h+=1;const E=Date.now();B.web(r,o,{target:{socketPath:F},agent:T,proxyTimeout:I},m=>{if(!w){if(edgeGone(r,o)){k.clear(),w=!0;return}(0,pod_probe_1.podConnectionVerdict)(F,m).then(G=>{if(w)return;const J=Date.now()-E>=I-250;if(G==="alive"&&h<2&&W&&!J&&!edgeGone(r,o)){console.log(`[gateway] pod ${i} of ${e.project}: ${m.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),M();return}if(k.clear(),w=!0,G==="gone"){j(m.message);const K=s(()=>{q(e,r,i,{kind:"pod-evicted",status:502,reason:`${m.message} \u2014 its socket no longer accepts (the pod process is gone)${n?`; the request had already been moved here from ${n}`:""}`}),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${i} of ${e.project} is gone (${m.message}) \u2014 retry.`)},"giveUp");if(n||!W||J){K();return}L(e,p).then(z=>{if(!edgeGone(r,o)){if(!z){K();return}U(e,r,i,path.basename(z,".sock"),m.message),v(z,i)}},K);return}console.error(`[gateway] pod ${i} of ${e.project}: ${m.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${i} of ${e.project} dropped the connection \u2014 retry.`)})}})},"sendToPod");M()},"onPod");v(y);return}const S=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let g=0,_=!1;const P=isReplayableRequest(r),l=s(()=>{if(edgeGone(r,o))return;const a=e.target,y={target:a,autoRewrite:typeof a=="string",changeOrigin:!0,agent:H(a)};B.web(r,o,y,v=>{if(isConnError(v)&&(g+=1),e.lens&&isConnError(v)&&P&&g>=2&&!_&&!edgeGone(r,o)){_=!0,V(e).then(u=>{if(!edgeGone(r,o)){if(u==="ready"){l();return}if(u==="unsupported"&&Date.now()<S){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}b(e,r,o,e.target,v,u)}});return}if(isConnError(v)&&P&&Date.now()<S&&!edgeGone(r,o)){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}o.destroyed||b(e,r,o,a,v)})},"tryTcp");l()},"forward"),agentForTarget:H,destroyAgents:s(()=>{A.stop(),T.destroy(),N.destroy()},"destroyAgents"),closeProxy:s(()=>{try{B.close?.()}catch{}},"closeProxy")}}D(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
1
|
+
"use strict";var ee=Object.defineProperty;var D=(o,c)=>ee(o,"name",{value:c,configurable:!0});var Z=Object.defineProperty,s=D((o,c)=>Z(o,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.upstreamOpen=upstreamOpen,exports.wedgedMessage=wedgedMessage,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"),gc_notice_1=require("../../../gc-notice"),service_keys_1=require("./service-keys");function isConnError(o){const c=o.code??"";return c==="ECONNREFUSED"||c==="ECONNRESET"||c==="ETIMEDOUT"||c==="EPIPE"||c==="EHOSTUNREACH"||c==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(o.message)}D(isConnError,"isConnError"),s(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(o){if(!IDEMPOTENT_METHODS.has((o.method??"GET").toUpperCase())||o.headers["transfer-encoding"]!==void 0)return!1;const c=o.headers["content-length"];return!(c!==void 0&&String(c).trim()!=="0")}D(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(o,c){return c.headersSent||c.writableEnded||c.destroyed===!0||o.aborted===!0}D(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"];function returnRequestId(o,c){if(o["x-request-id"]!==void 0)return;const u=c.headers["x-request-id"],q=Array.isArray(u)?u[0]:u;typeof q=="string"&&q!==""&&(o["x-request-id"]=q)}D(returnRequestId,"returnRequestId"),s(returnRequestId,"returnRequestId");const httpProxy=require("http-proxy");function declaredFirstByteMs(o){const c=Number(o);return Number.isFinite(c)&&c>0?c:types_1.POD_RESPONSE_TIMEOUT_MS}D(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");const secs=s(o=>`${(o/1e3).toFixed(1)}s`,"secs"),upstreams=new WeakMap;function upstreamOpen(o){const c=upstreams.get(o),u=c?.socket;return!!c&&!c.destroyed&&!!u&&!u.destroyed}D(upstreamOpen,"upstreamOpen"),s(upstreamOpen,"upstreamOpen");function wedgedMessage(o,c,u){const q=u.failure==="gone"?"its process is gone":u.failure==="silent"?`it did not answer its liveness check for ${secs(u.silentMs)}`:"it gave no liveness answer";return`Pod ${o} of ${c} sent no response for ${secs(u.heldMs)} and ${q} \u2014 taken out of rotation, and the service is being restarted, so work it was still doing for this request may have been cut short. Retry if the call is safe to repeat.`}D(wedgedMessage,"wedgedMessage"),s(wedgedMessage,"wedgedMessage");function armFirstByteGuard(o){let c,u=!1,q=!1,V=!1,k,x=0,E=(0,body_stall_1.requestBodyArriving)(o.req)?void 0:Date.now();const N=s(()=>{E??=Date.now()},"onBodyEnd");E===void 0&&o.req.once("end",N);const H=s(()=>(E===void 0&&!(0,body_stall_1.requestBodyArriving)(o.req)&&(E=Date.now()),E===void 0?0:Date.now()-E),"heldMs"),A=s(T=>{c=setTimeout(C,T),c.unref?.()},"arm"),C=s(()=>{if(u||!o.pending())return;const T=H();if(o.ceilingMs<=0){if(E!==void 0&&T>=o.firstByteMs){o.onWedged(`no response within ${T}ms (wedged socket) \u2014 evicted + respawning`,{heldMs:T,silentMs:0,probes:0,failure:"deadline"});return}A(E===void 0?o.firstByteMs:o.firstByteMs-T);return}(0,pod_probe_1.probePodAliveVerdict)(o.socketPath,void 0,o.serviceKey).then(({alive:L,failure:b})=>{if(u||!o.pending())return;const j=H();if(!L){if(b==="gone"&&o.upstreamOpen?.()){if(j>=o.ceilingMs){o.onCeiling(`retiring (its socket is gone) and no response within ${j}ms (ceiling ${o.ceilingMs}ms) \u2014 giving up on this request, pod kept`);return}V||(V=!0,o.onRetiring?.(j)),A(Math.max(o.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS));return}if(x+=1,b==="silent"&&(0,gc_notice_1.inAnnouncedGc)(o.socketPath)){k=void 0,A(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}if(b==="silent"&&types_1.POD_WEDGE_CONFIRM_MS>0&&(k??=Date.now(),Date.now()-k<types_1.POD_WEDGE_CONFIRM_MS)){A(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}const B=k===void 0?0:Date.now()-k,I=b==="gone"?"its socket no longer accepts (the pod process is gone)":b==="silent"?`nothing answered on ${types_1.POD_ALIVE_PATH} for ${B}ms (${x} probe${x===1?"":"s"})`:`no liveness answer on ${types_1.POD_ALIVE_PATH}`;o.onWedged(`no response within ${j}ms and ${I} (wedged socket) \u2014 evicted + respawning`,{heldMs:j,silentMs:B,probes:x,failure:b??"no-route"});return}if(k=void 0,j>=o.ceilingMs){o.onCeiling(`alive on ${types_1.POD_ALIVE_PATH} but no response within ${j}ms (ceiling ${o.ceilingMs}ms) \u2014 giving up on this request, pod kept`,L);return}!q&&E!==void 0&&(q=!0,o.onSlow(j,L)),A(Math.max(o.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))})},"onDeadline");return A(o.firstByteMs),{clear:s(()=>{u=!0,c&&clearTimeout(c),o.req.removeListener("end",N)},"clear")}}D(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(o,c,u){const{recordEdge:q,finishTrace:V,traceStep:k,emit:x}=c,E=s((e,r,t,d)=>{const g=d.kind==="pod-evicted";x(g?"error":"warning",`${g?"pod evicted":"request given up"}: ${e.project} (${t}) \u2014 ${d.status}`,`cluster-pod-incident:${e.project}`,{kind:d.kind,project:e.project,pod:t,status:d.status,reason:d.reason,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0],...d.detail??{}})},"reportPodIncident"),N=s(async(e,r)=>{const t=e.pool;if(!t)return;t.scannedAt=0;let d=(0,discovery_1.pickSock)(t,o.scanTtlMs);return!d&&e.svc&&(k(r,"cold-start",{project:e.project}),await u.ensureUp(e.svc)&&(t.scannedAt=0,d=(0,discovery_1.pickSock)(t,o.scanTtlMs))),d},"anotherPod"),H=s((e,r,t,d,g)=>{console.log(`[gateway] pod ${t} of ${e.project} is gone \u2014 its ${r.method} was not answered, sending it to ${d}`),x("info",`request moved to another pod: ${e.project} (${t} \u2192 ${d})`,`cluster-pod-moved:${e.project}`,{kind:"pod-gone-rerouted",project:e.project,pod:t,to:d,reason:g,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0]})},"reportPodMoved"),A=s(async e=>{if(!e.lens||!o.lensSupervisor)return"unsupported";let r;const t=new Promise(f=>{r=setTimeout(()=>f("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),d=o.lensSupervisor.recover(e.lens,"proxy-connection").then(f=>f?"ready":"failed").catch(()=>"failed"),g=await Promise.race([d,t]);r&&clearTimeout(r);const S=o.lensSupervisor.status(e.lens);return g==="failed"&&(!S||S.state==="ready")?"unsupported":g},"waitForManagedRecovery"),C=s((e,r,t,d,g,S)=>{if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}const f=e.lens?o.lensSupervisor?.status(e.lens):void 0,$=S==="timeout"||f?.state==="starting"||f?.state==="replacing",P=(0,gateway_errors_1.ensureRequestId)(r),l=e.lens?{project:e.project,reason:f?.reason??"connection-failure",generation:f?.generation??0,...$?{retryAfterMs:2e3}:{}}:void 0,i=$?503:502,y=$?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${P} project=${e.project} target=${String(d)} state=${f?.state??"unmanaged"} generation=${f?.generation??0}: ${g.message}`),x("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:P,project:e.project,target:String(d),state:f?.state??"unmanaged",generation:f?.generation,error:g.message}),(0,edge_1.sendError)(t,r,i,y,$?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,l,$?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),T=(0,body_stall_1.createBodyStallGuard)(o.bodyStallMs??types_1.BODY_STALL_MS),L={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},b=new http.Agent(L),j=new(require("node:https")).Agent(L),B=s(e=>typeof e=="string"&&e.startsWith("https:")?j:b,"agentForTarget"),I=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:b}),Y=s((e,r,t)=>{const d=s(()=>{t()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",d),e.once("close",()=>r.removeListener("close",d))},"propagateEdgeCancel");I.on("proxyReq",(e,r,t)=>{upstreams.set(r,e);for(const d of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(d);e.setHeader("connection","keep-alive"),Y(e,t,()=>t.writableEnded)}),I.on("proxyRes",(e,r,t)=>{const d=r.__lensmcpPod;d&&(e.headers["x-lensmcp-pod"]=d),delete e.headers["x-powered-by"],e.headers.server="LensMCP",returnRequestId(e.headers,r),t&&Y(e,t,()=>t.writableEnded),t&&T.armResponse(t)});const Q=s((e,r,t,d)=>{(0,edge_1.stampForwardingHeaders)(o,r),T.arm(r);const g={},S=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[n,a]of Object.entries(r.headers))a===void 0||n.startsWith(":")||HOP_BY_HOP.has(n)||S&&(n==="content-length"||n==="transfer-encoding")||(g[n]=a);let f=http,$;if(typeof e=="object")$={socketPath:e.socketPath,path:r.url,method:r.method,headers:g,agent:b};else{const n=new URL(e),a=n.protocol==="https:";f=a?require("node:https"):http,g.host=n.host,$={protocol:n.protocol,hostname:n.hostname,port:n.port||(a?443:80),path:r.url,method:r.method,headers:g,agent:B(e)}}const P=t;P.__lensmcpErrContained||(P.__lensmcpErrContained=!0,t.on("error",()=>{}));let l;const i=s(()=>{t.writableEnded||p.destroy()},"onReqGone"),y=s(()=>{l&&!l.destroyed&&l.destroy()},"onEdgeClose"),_=s(()=>{r.removeListener("error",i),r.removeListener("aborted",i),r.removeListener("close",i),t.removeListener("close",y)},"detach"),p=f.request($,n=>{if(l=n,d.onResponse(),t.headersSent||t.writableEnded||t.destroyed){n.destroy();return}const a={};for(const[O,U]of Object.entries(n.headers))U===void 0||HOP_BY_HOP.has(O)||(a[O]=U);delete a["x-powered-by"],a.server="LensMCP",returnRequestId(a,r);const w=r.__lensmcpPod;w&&(a["x-lensmcp-pod"]=w);try{t.writeHead(n.statusCode??502,a)}catch(O){console.error("[gateway] h2 relay: bad response header, resetting stream:",O.message),n.destroy(),t.destroyed||t.destroy(O);return}n.pipe(t),T.armResponse(t);const R=s(()=>{!t.writableEnded&&!t.destroyed&&t.destroy()},"abortEdge");n.once("error",R),n.once("close",R),t.on("close",y)});p.once("error",n=>d.onError(n)),upstreams.set(r,p),p.once("close",_),r.on("error",i),r.on("aborted",i),r.on("close",i),S?p.end():r.pipe(p)},"pipeUpstream"),X=s(async(e,r,t)=>{const d=r.__lensmcpTrace;if(e.pool){const l=e.svc;l&&(l.lastUsed=Date.now(),l.inflight+=1,t.on("close",()=>{l.inflight-=1,l.lastUsed=Date.now()}));let i=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs);if(!i&&l&&(k(d,"cold-start",{project:e.project}),await u.ensureUp(l)&&(i=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs))),l&&i&&u.markUp(l,"observed"),l&&u.scaleUp(l),!i){const _=l?.lastErrors?.length?` Service errors: ${l.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${_} The watcher recovers and the next request retries automatically.`);return}const y=s((_,p)=>{const n=path.basename(_,".sock");r.__lensmcpPod=n,k(d,"pod-select",{pod:n,pods:e.pool.socks.length,strategy:"round-robin",...p?{movedFrom:p}:{}});let a=!1;const w=s((h,M="unlink")=>{if(e.pool.socks=e.pool.socks.filter(m=>m!==_),e.pool.idx=-1,M==="unlink")try{fs.unlinkSync(_)}catch{}console.error(`[gateway] pod ${n} of ${e.project}: ${h}`)},"evictPod"),R=armFirstByteGuard({socketPath:_,serviceKey:o.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!a&&!t.headersSent&&!t.writableEnded,"pending"),upstreamOpen:s(()=>upstreamOpen(r),"upstreamOpen"),onRetiring:s(h=>{console.log(`[gateway] pod ${n} of ${e.project}: retiring (its socket is gone) but still holds this ${r.method} after ${h}ms \u2014 waiting for it, not evicting`)},"onRetiring"),onWedged:s((h,M)=>{a=!0,w(h),E(e,r,n,{kind:"pod-evicted",status:502,reason:h,detail:M}),(0,edge_1.sendError)(t,r,502,"unavailable",wedgedMessage(n,e.project,M))},"onWedged"),onCeiling:s(h=>{a=!0,w(h,"keep"),E(e,r,n,{kind:"request-gave-up",status:504,reason:h}),(0,edge_1.sendError)(t,r,504,"unavailable",`Pod ${n} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((h,M)=>{console.log(`[gateway] pod ${n} of ${e.project}: slow first byte, pod alive (${h}ms, ${M.inflight} in-flight, ${M.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>R.clear());const O=_,U=isReplayableRequest(r);let W=0;const F=s(()=>{W+=1,Q({socketPath:O},r,t,{onResponse:s(()=>{R.clear(),a=!0},"onResponse"),onError:s(h=>{if(!a){if(edgeGone(r,t)){R.clear(),a=!0;return}(0,pod_probe_1.podConnectionVerdict)(O,h).then(M=>{if(!a){if(M==="alive"&&W<2&&U&&!edgeGone(r,t)){console.log(`[gateway] pod ${n} of ${e.project}: ${h.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),F();return}if(R.clear(),a=!0,M==="gone"){w(h.message);const m=s(()=>{E(e,r,n,{kind:"pod-evicted",status:502,reason:`${h.message} \u2014 its socket no longer accepts (the pod process is gone)${p?`; the request had already been moved here from ${p}`:""}`}),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${n} of ${e.project} is gone (${h.message}) \u2014 retry.`)},"giveUp");if(p||!U){m();return}N(e,d).then(v=>{if(!edgeGone(r,t)){if(!v){m();return}H(e,r,n,path.basename(v,".sock"),h.message),y(v,n)}},m);return}console.error(`[gateway] pod ${n} of ${e.project}: ${h.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 ${n} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");F()},"onPod");y(i);return}const g=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let S=0,f=!1;const $=isReplayableRequest(r),P=s(()=>{if(edgeGone(r,t))return;const l=e.target;Q(l,r,t,{onResponse:s(()=>{},"onResponse"),onError:s(i=>{if(isConnError(i)&&(S+=1),e.lens&&isConnError(i)&&$&&S>=2&&!f&&!edgeGone(r,t)){f=!0,A(e).then(y=>{if(!edgeGone(r,t)){if(y==="ready"){P();return}if(y==="unsupported"&&Date.now()<g){setTimeout(P,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}C(e,r,t,e.target,i,y)}});return}if(isConnError(i)&&$&&Date.now()<g&&!edgeGone(r,t)){setTimeout(P,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}C(e,r,t,l,i)},"onError")})},"tryTcp");P()},"forwardNative");return{proxy:I,forward:s(async(e,r,t)=>{const d=Date.now(),g=r.__lensmcpTrace;if(t.on("close",()=>{q(e,Date.now()-d,t.statusCode??0,r.__lensmcpCaller),V(g,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)(o,r),T.arm(r),(r.httpVersionMajor??1)>=2){await X(e,r,t);return}if(e.pool){const i=e.svc;i&&(i.lastUsed=Date.now(),i.inflight+=1,t.on("close",()=>{i.inflight-=1,i.lastUsed=Date.now()}));let y=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs);if(!y&&i&&(k(g,"cold-start",{project:e.project}),await u.ensureUp(i)&&(y=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs))),i&&y&&u.markUp(i,"observed"),i&&u.scaleUp(i),!y){const p=i?.lastErrors?.length?` Service errors: ${i.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${p} The watcher recovers and the next request retries automatically.`);return}const _=s((p,n)=>{const a=path.basename(p,".sock");r.__lensmcpPod=a,k(g,"pod-select",{pod:a,pods:e.pool.socks.length,strategy:"round-robin",...n?{movedFrom:n}:{}});let w=!1;const R=s((m,v="unlink")=>{if(e.pool.socks=e.pool.socks.filter(G=>G!==p),e.pool.idx=-1,v==="unlink")try{fs.unlinkSync(p)}catch{}console.error(`[gateway] pod ${a} of ${e.project}: ${m}`)},"evictPod"),O=armFirstByteGuard({socketPath:p,serviceKey:o.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!w&&!t.headersSent&&!t.writableEnded,"pending"),upstreamOpen:s(()=>upstreamOpen(r),"upstreamOpen"),onRetiring:s(m=>{console.log(`[gateway] pod ${a} of ${e.project}: retiring (its socket is gone) but still holds this ${r.method} after ${m}ms \u2014 waiting for it, not evicting`)},"onRetiring"),onWedged:s((m,v)=>{w=!0,R(m),E(e,r,a,{kind:"pod-evicted",status:502,reason:m,detail:v}),(0,edge_1.sendError)(t,r,502,"unavailable",wedgedMessage(a,e.project,v))},"onWedged"),onCeiling:s(m=>{w=!0,R(m,"keep"),E(e,r,a,{kind:"request-gave-up",status:504,reason:m}),(0,edge_1.sendError)(t,r,504,"unavailable",`Pod ${a} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((m,v)=>{console.log(`[gateway] pod ${a} of ${e.project}: slow first byte, pod alive (${m}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")});t.on("close",()=>O.clear());const U=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),W=p,F=isReplayableRequest(r);let h=0;const M=s(()=>{h+=1;const m=Date.now();I.web(r,t,{target:{socketPath:W},agent:b,proxyTimeout:U},v=>{if(!w){if(edgeGone(r,t)){O.clear(),w=!0;return}(0,pod_probe_1.podConnectionVerdict)(W,v).then(G=>{if(w)return;const J=Date.now()-m>=U-250;if(G==="alive"&&h<2&&F&&!J&&!edgeGone(r,t)){console.log(`[gateway] pod ${a} of ${e.project}: ${v.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),M();return}if(O.clear(),w=!0,G==="gone"){R(v.message);const K=s(()=>{E(e,r,a,{kind:"pod-evicted",status:502,reason:`${v.message} \u2014 its socket no longer accepts (the pod process is gone)${n?`; the request had already been moved here from ${n}`:""}`}),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} is gone (${v.message}) \u2014 retry.`)},"giveUp");if(n||!F||J){K();return}N(e,g).then(z=>{if(!edgeGone(r,t)){if(!z){K();return}H(e,r,a,path.basename(z,".sock"),v.message),_(z,a)}},K);return}console.error(`[gateway] pod ${a} of ${e.project}: ${v.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.`)})}})},"sendToPod");M()},"onPod");_(y);return}const S=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let f=0,$=!1;const P=isReplayableRequest(r),l=s(()=>{if(edgeGone(r,t))return;const i=e.target,y={target:i,autoRewrite:typeof i=="string",changeOrigin:!0,agent:B(i)};I.web(r,t,y,_=>{if(isConnError(_)&&(f+=1),e.lens&&isConnError(_)&&P&&f>=2&&!$&&!edgeGone(r,t)){$=!0,A(e).then(p=>{if(!edgeGone(r,t)){if(p==="ready"){l();return}if(p==="unsupported"&&Date.now()<S){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}C(e,r,t,e.target,_,p)}});return}if(isConnError(_)&&P&&Date.now()<S&&!edgeGone(r,t)){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}t.destroyed||C(e,r,t,i,_)})},"tryTcp");l()},"forward"),agentForTarget:B,destroyAgents:s(()=>{T.stop(),b.destroy(),j.destroy()},"destroyAgents"),closeProxy:s(()=>{try{I.close?.()}catch{}},"closeProxy")}}D(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var Se=Object.defineProperty;var T=(s,r)=>Se(s,"name",{value:r,configurable:!0});var _e=Object.defineProperty,n=T((s,r)=>_e(s,"name",{value:r,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.retirePreviousWorkspace=retirePreviousWorkspace,exports.resolveMaxConnections=resolveMaxConnections,exports.h2BudgetFromEnv=h2BudgetFromEnv,exports.createH2Governor=createH2Governor,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),control_1=require("./control"),workspace_middleware_1=require("./workspace-middleware"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),client_ip_1=require("../client-ip"),workspace_log_1=require("./workspace-log"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),process_names_1=require("./process-names"),project_graph_store_1=require("./project-graph-store"),serve_launch_1=require("./serve-launch"),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),types_1=require("./types");function insecureLocalFetch(s,r){return new Promise((d,h)=>{const a=(s.startsWith("https:")?require("node:https"):require("node:http")).request(s,{method:"GET",headers:r?.headers,rejectUnauthorized:!1},y=>{const _=[];y.on("data",S=>_.push(S)),y.on("end",()=>{const S=y.statusCode??0,p=Buffer.concat(_).toString("utf8");d({ok:S>=200&&S<300,status:S,json:n(async()=>JSON.parse(p),"json")})})});a.on("error",h),a.end()})}T(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(s){const r=process.env.LENSMCP_GW_JWKS_URL,d=process.env.LENSMCP_GW_JWT_ISS,h=new Map;let a;const y=n((_,S)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${_}${S}`);const p=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:_,...d?{issuer:d}:{},fetchImpl:insecureLocalFetch});return p.refresh().catch(o=>console.warn("[gateway] edge JWKS warm failed (will retry):",o.message)),p},"make");return{for(_){if(r)return a??=y(r,"");const S=_.split(":")[0].toLowerCase();let p;for(const c of s()){const i=c.host;if(typeof i!="string"||!/^auth\./.test(i)||i.startsWith("internal."))continue;const A=i.slice(5);(S===A||S.endsWith(`.${A}`))&&(!p||A.length>p.length)&&(p=A)}if(!p)return;let o=h.get(p);return o||(o=y(`https://auth.${p}/jwks.json`,` (auto-derived for apex ${p})`),h.set(p,o)),o},stop(){a?.stop();for(const _ of h.values())_.stop()}}}T(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(s,r){return s&&exports.BENIGN_SOCKET_ERRNOS.has(s)?{status:null,report:!1,cause:"client-abort"}:s==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:s==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:s==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:s?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:s??r??"unknown"}}T(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(s=1e4){const r=new Map;return d=>{const h=Date.now(),a=r.get(d)??0;return h-a<s?!1:(r.set(d,h),!0)}}T(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(s){s.on("error",r=>{exports.BENIGN_SOCKET_ERRNOS.has(r.code??"")||console.warn(`[gateway] inbound socket error (${r.code??r.message}) \u2014 dropping this connection only`),s.destroy()})}T(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(s,r,d){const h=s.get(r);if(!h)return!1;s.delete(r),d==="re-registered"&&console.log(`[gateway] workspace '${r}' re-registered \u2014 retiring the children of its previous registration first.`);try{h()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${r}' (${d}): ${a.message}`)}return!0}T(retirePreviousWorkspace,"retirePreviousWorkspace"),n(retirePreviousWorkspace,"retirePreviousWorkspace"),exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=512,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=64,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=4096,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=3e5;function parseBudget(s,r){if(s===void 0||s.trim()==="")return r;const d=Number(s);return!Number.isFinite(d)||d<0?r:Math.floor(d)}T(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(s){return parseBudget(s,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}T(resolveMaxConnections,"resolveMaxConnections"),n(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(s=process.env){return{maxSessions:parseBudget(s.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(s.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(s.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}T(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(s,r){const{NGHTTP2_ENHANCE_YOUR_CALM:d,NGHTTP2_REFUSED_STREAM:h}=require("node:http2").constants,a=new Map;let y=0;const _=n(o=>{o.idle&&(clearTimeout(o.idle),o.idle=void 0)},"disarm"),S=n((o,c)=>{_(c),!(s.idleSessionMs<=0||c.open>0)&&(c.idle=setTimeout(()=>{o.destroyed||o.closed||(r("session-reclaimed",{idleMs:s.idleSessionMs,sessions:a.size}),a.delete(o),o.close())},s.idleSessionMs),c.idle.unref?.())},"arm"),p=n(()=>{for(const[o,c]of a)if(!(c.open>0||o.destroyed))return _(c),a.delete(o),r("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:s.maxSessions}),o.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(o){if(s.maxSessions>0&&a.size>=s.maxSessions&&!p()){r("session-refused",{sessions:a.size,maxSessions:s.maxSessions,streams:y});try{o.goaway(d)}catch{}try{o.close()}catch{}return!1}const c={open:0};return a.set(o,c),S(o,c),o.once("close",()=>{_(c),a.delete(o)}),!0},admitStream(o,c){const i=a.get(o);if(!i)return!0;if(s.maxStreams>0&&y>=s.maxStreams){r("stream-refused",{streams:y,maxStreams:s.maxStreams,sessions:a.size});try{c.close(h)}catch{}return!1}return y+=1,i.open+=1,_(i),c.once("close",()=>{y=Math.max(0,y-1),i.open=Math.max(0,i.open-1),i.open===0&&a.has(o)&&S(o,i)}),!0},dispose(){for(const o of a.values())_(o);a.clear()}}}T(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(s,r){const d=s.https!==!1,h=s.ports?.length?s.ports:[443],a=s.scanTtlMs??1500,y=s.startupTimeoutMs??12e4,_=s.sweepMs??1e4,S=s.trafficFlushMs??5e3;let p=!1;const o=process.env.LENSMCP_EVENT_FILE??path.join(r.root,".lensmcp","events.jsonl"),c=(0,scope_1.readLensScope)(r.root),i=c.key,A=c.sockNs!==i?c.sockNs:void 0,Y=r.projectsConfigurations?.projects??{},B=(0,discovery_1.projectSourceRoots)(r.root,Y,r.projectDependencies),{routes:H,services:N,instance:X}=(0,discovery_1.discoverRoutes)(r.root,Y,void 0,i,B,A,c.instance);if(H.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const $=new route_registry_1.RouteRegistry;$.register({wsKey:i,...A?{sockNs:A}:{},root:r.root,routes:H,services:N,...X?{instance:X}:{}});const R=[];let J=n(()=>{},"refreshCert");const V=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,$),J()},"rebuildRoutes");V();const{serviceKeys:te,keyToProject:oe,keyToWorkspace:ne}=(0,service_keys_1.loadServiceKeys)(r.root,N,i),ae=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...s.trustProxyHops!==void 0?{trustProxyHops:s.trustProxyHops}:{},...s.clientIpHeader?{clientIpHeader:s.clientIpHeader}:{},...s.clientIpTrustedProxies?{clientIpTrustedProxies:s.clientIpTrustedProxies}:{}}),m={root:r.root,wsKey:i,...A?{sockNs:A}:{},eventFile:o,routes:R,registry:$,rebuildRoutes:V,services:N,serviceKeys:te,keyToProject:oe,keyToWorkspace:ne,clientIp:ae,bodyStallMs:s.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:y,sweepMs:_,trafficFlushMs:S,https:d,projectRoots:Object.fromEntries(Object.entries(r.projectsConfigurations?.projects??{}).map(([e,t])=>[e,path.resolve(r.root,t.root)])),projectSourceRoots:B,...r.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(r.projectGraphStore)}:{},...s.sharedCompiler!==void 0?{sharedCompiler:s.sharedCompiler}:{},stopped:n(()=>p,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(N,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!r.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,s.sharedCompiler),label:i||path.basename(r.root),lens:{key:i,dashboard:s.dashboard!==!1,mcp:s.mcp!==!1&&H.some(e=>!!e.lens)}}));const w=(0,observability_1.createObservability)(o,{trafficFlushMs:S}),ie=createClientErrorLogGate(),x=h2BudgetFromEnv(),ce=createClientErrorLogGate(3e4),G=createH2Governor(x,(e,t)=>{ce(e)&&console.warn(`[gateway] h2 aggregate budget: ${e} (${JSON.stringify(t)}). These are the FRONT DOOR's ceilings, not an upstream's \u2014 raise/disable with LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS / LENSMCP_MAX_CONNECTIONS (0 = unbounded).`),w.emit(e==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${e}`,`gateway-h2-budget:${e}`,{kind:"h2-aggregate-budget",event:e,project:"gateway",...t})}),M=(0,lifecycle_1.createServiceLayer)(m,w),k=(0,lens_children_1.createLensChildren)(m,w,s),j=new Map;let q;if(s.middleware){const e=require(path.resolve(r.root,s.middleware));q=e.default??e}const W=(0,workspace_middleware_1.createWorkspaceMiddleware)({ownerKey:i,...q?{owner:q}:{},policy:(0,workspace_middleware_1.guestMiddlewarePolicy)()});let O;try{O=(0,control_1.startControlServer)({rt:m,killService:n((e,t)=>M.killSpawned(e,t),"killService"),restartService:n((e,t)=>M.restartService(e,t),"restartService"),onRegister:n(async e=>{retirePreviousWorkspace(j,e.wsKey,"re-registered"),W.set(e.wsKey,e.root,e.middleware);const t=await k.hostWorkspaceDashboard(e),f=await k.hostWorkspaceApps(e);j.set(e.wsKey,()=>{try{t?.()}finally{f()}})},"onRegister"),onUnregister:n(e=>{retirePreviousWorkspace(j,e.wsKey,"unregistered"),W.drop(e.wsKey),M.stopBuildHost(e.root)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const b=(0,proxy_1.createProxy)(m,w,M),z=buildEdgeVerifierPool(()=>m.routes),Q=(0,auth_1.createEdgeAuth)(m,w,z),le=n(e=>{const t=e.route?.wsKey,f=W.for(t);if(f)try{const l=t?m.registry.get(t):void 0;f(e.req,{...t?{wsKey:t}:{},...l?{root:l.root}:{},env:l?.instance?.env??{}})}catch(l){w.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:l.message}),e.route&&w.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${l.message}`)}},"builtinMiddleware"),Z=s.hooks??{},ee=(0,hooks_1.createHooks)({...Z,afterAuth:[le,...Z.afterAuth??[]]}),se=(0,handler_1.createHandler)({rt:m,obs:w,auth:Q,proxy:b,hooks:ee}),re=(0,upgrade_1.createUpgrade)({rt:m,auth:Q,proxy:b,hooks:ee,obs:w}),ue=w.startEdgeFlusher(),de=M.startSweeper();await k.bootSingletonsAndApps();for(const e of N)e.decl.eager&&M.ensureUp(e);let P,D,L;if(d){const e=require("../../../basic-ssl"),t=(0,service_keys_1.gatewayCacheDir)(r.root);L=n(()=>e.getCertificateSync(t,"gateway",m.routes.map(f=>f.host).filter(f=>!!f)),"mintCert"),P=L(),D=e.caCertPath()}const F=[],I=[],U=s.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const e of h){const t=P?require("node:http2").createSecureServer({key:P,cert:P,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:s.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:s.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(l,E)=>se(l,E)):http.createServer(se);t.on("upgrade",re),t.on("error",l=>console.error(`[gateway] port ${e}: ${l.code}`)),t.on("connection",guardInboundSocket),t.on("secureConnection",guardInboundSocket),t.on("tlsClientError",(l,E)=>E.destroy()),t.on("clientError",(l,E)=>{const u=l,g=classifyClientError(u.code,u.message);if(g.report&&ie(g.cause)){const v=E.remoteAddress??"unknown",C=g.status===null?"dropped":String(g.status);console.warn(`[gateway] inbound H1 clientError from ${v}: ${g.cause} (${u.code??u.message}${u.bytesParsed!==void 0?`, bytesParsed=${u.bytesParsed}`:""}) \u2192 ${C}. This status is the GATEWAY's, not the upstream service's.`),w.emit("warning",`inbound clientError: ${g.cause} \u2192 ${C}`,`gateway-client-error:${g.cause}`,{kind:"gateway-client-error",cause:g.cause,code:u.code??null,status:g.status,peer:v,bytesParsed:u.bytesParsed??null})}g.status!==null&&E.writable&&E.end(`HTTP/1.1 ${g.status} ${CLIENT_ERROR_REASON[g.status]}\r
|
|
1
|
+
"use strict";var Ee=Object.defineProperty;var A=(s,r)=>Ee(s,"name",{value:r,configurable:!0});var Se=Object.defineProperty,n=A((s,r)=>Se(s,"name",{value:r,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.retirePreviousWorkspace=retirePreviousWorkspace,exports.resolveMaxConnections=resolveMaxConnections,exports.h2BudgetFromEnv=h2BudgetFromEnv,exports.createH2Governor=createH2Governor,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),control_1=require("./control"),workspace_middleware_1=require("./workspace-middleware"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),client_ip_1=require("../client-ip"),workspace_log_1=require("./workspace-log"),own_log_1=require("./own-log"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),process_names_1=require("./process-names"),project_graph_store_1=require("./project-graph-store"),serve_launch_1=require("./serve-launch"),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),types_1=require("./types");function insecureLocalFetch(s,r){return new Promise((l,h)=>{const a=(s.startsWith("https:")?require("node:https"):require("node:http")).request(s,{method:"GET",headers:r?.headers,rejectUnauthorized:!1},y=>{const p=[];y.on("data",S=>p.push(S)),y.on("end",()=>{const S=y.statusCode??0,u=Buffer.concat(p).toString("utf8");l({ok:S>=200&&S<300,status:S,json:n(async()=>JSON.parse(u),"json")})})});a.on("error",h),a.end()})}A(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(s){const r=process.env.LENSMCP_GW_JWKS_URL,l=process.env.LENSMCP_GW_JWT_ISS,h=new Map;let a;const y=n((p,S)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${p}${S}`);const u=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:p,...l?{issuer:l}:{},fetchImpl:insecureLocalFetch});return u.refresh().catch(o=>console.warn("[gateway] edge JWKS warm failed (will retry):",o.message)),u},"make");return{for(p){if(r)return a??=y(r,"");const S=p.split(":")[0].toLowerCase();let u;for(const d of s()){const _=d.host;if(typeof _!="string"||!/^auth\./.test(_)||_.startsWith("internal."))continue;const m=_.slice(5);(S===m||S.endsWith(`.${m}`))&&(!u||m.length>u.length)&&(u=m)}if(!u)return;let o=h.get(u);return o||(o=y(`https://auth.${u}/jwks.json`,` (auto-derived for apex ${u})`),h.set(u,o)),o},stop(){a?.stop();for(const p of h.values())p.stop()}}}A(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(s,r){return s&&exports.BENIGN_SOCKET_ERRNOS.has(s)?{status:null,report:!1,cause:"client-abort"}:s==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:s==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:s==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:s?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:s??r??"unknown"}}A(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(s=1e4){const r=new Map;return l=>{const h=Date.now(),a=r.get(l)??0;return h-a<s?!1:(r.set(l,h),!0)}}A(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(s){s.on("error",r=>{exports.BENIGN_SOCKET_ERRNOS.has(r.code??"")||console.warn(`[gateway] inbound socket error (${r.code??r.message}) \u2014 dropping this connection only`),s.destroy()})}A(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(s,r,l){const h=s.get(r);if(!h)return!1;s.delete(r),l==="re-registered"&&console.log(`[gateway] workspace '${r}' re-registered \u2014 retiring the children of its previous registration first.`);try{h()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${r}' (${l}): ${a.message}`)}return!0}A(retirePreviousWorkspace,"retirePreviousWorkspace"),n(retirePreviousWorkspace,"retirePreviousWorkspace"),exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=512,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=64,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=4096,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=3e5;function parseBudget(s,r){if(s===void 0||s.trim()==="")return r;const l=Number(s);return!Number.isFinite(l)||l<0?r:Math.floor(l)}A(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(s){return parseBudget(s,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}A(resolveMaxConnections,"resolveMaxConnections"),n(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(s=process.env){return{maxSessions:parseBudget(s.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(s.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(s.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}A(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(s,r){const{NGHTTP2_ENHANCE_YOUR_CALM:l,NGHTTP2_REFUSED_STREAM:h}=require("node:http2").constants,a=new Map;let y=0;const p=n(o=>{o.idle&&(clearTimeout(o.idle),o.idle=void 0)},"disarm"),S=n((o,d)=>{p(d),!(s.idleSessionMs<=0||d.open>0)&&(d.idle=setTimeout(()=>{o.destroyed||o.closed||(r("session-reclaimed",{idleMs:s.idleSessionMs,sessions:a.size}),a.delete(o),o.close())},s.idleSessionMs),d.idle.unref?.())},"arm"),u=n(()=>{for(const[o,d]of a)if(!(d.open>0||o.destroyed))return p(d),a.delete(o),r("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:s.maxSessions}),o.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(o){if(s.maxSessions>0&&a.size>=s.maxSessions&&!u()){r("session-refused",{sessions:a.size,maxSessions:s.maxSessions,streams:y});try{o.goaway(l)}catch{}try{o.close()}catch{}return!1}const d={open:0};return a.set(o,d),S(o,d),o.once("close",()=>{p(d),a.delete(o)}),!0},admitStream(o,d){const _=a.get(o);if(!_)return!0;if(s.maxStreams>0&&y>=s.maxStreams){r("stream-refused",{streams:y,maxStreams:s.maxStreams,sessions:a.size});try{d.close(h)}catch{}return!1}return y+=1,_.open+=1,p(_),d.once("close",()=>{y=Math.max(0,y-1),_.open=Math.max(0,_.open-1),_.open===0&&a.has(o)&&S(o,_)}),!0},dispose(){for(const o of a.values())p(o);a.clear()}}}A(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(s,r){const l=s.https!==!1,h=s.ports?.length?s.ports:[443],a=s.scanTtlMs??1500,y=s.startupTimeoutMs??12e4,p=s.sweepMs??1e4,S=s.trafficFlushMs??5e3;let u=!1;const o=process.env.LENSMCP_EVENT_FILE??path.join(r.root,".lensmcp","events.jsonl"),d=(0,own_log_1.startOwnLogRotation)(r.root),_=(0,scope_1.readLensScope)(r.root),m=_.key,x=_.sockNs!==m?_.sockNs:void 0,B=r.projectsConfigurations?.projects??{},X=(0,discovery_1.projectSourceRoots)(r.root,B,r.projectDependencies),{routes:$,services:N,instance:J}=(0,discovery_1.discoverRoutes)(r.root,B,void 0,m,X,x,_.instance);if($.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const G=new route_registry_1.RouteRegistry;G.register({wsKey:m,...x?{sockNs:x}:{},root:r.root,routes:$,services:N,...J?{instance:J}:{}});const R=[];let V=n(()=>{},"refreshCert");const z=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,G),V()},"rebuildRoutes");z();const{serviceKeys:oe,keyToProject:ne,keyToWorkspace:ae}=(0,service_keys_1.loadServiceKeys)(r.root,N,m),ie=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...s.trustProxyHops!==void 0?{trustProxyHops:s.trustProxyHops}:{},...s.clientIpHeader?{clientIpHeader:s.clientIpHeader}:{},...s.clientIpTrustedProxies?{clientIpTrustedProxies:s.clientIpTrustedProxies}:{}}),w={root:r.root,wsKey:m,...x?{sockNs:x}:{},eventFile:o,routes:R,registry:G,rebuildRoutes:z,services:N,serviceKeys:oe,keyToProject:ne,keyToWorkspace:ae,clientIp:ie,bodyStallMs:s.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:y,sweepMs:p,trafficFlushMs:S,https:l,projectRoots:Object.fromEntries(Object.entries(r.projectsConfigurations?.projects??{}).map(([e,t])=>[e,path.resolve(r.root,t.root)])),projectSourceRoots:X,...r.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(r.projectGraphStore)}:{},...s.sharedCompiler!==void 0?{sharedCompiler:s.sharedCompiler}:{},stopped:n(()=>u,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(N,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!r.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,s.sharedCompiler),label:m||path.basename(r.root),lens:{key:m,dashboard:s.dashboard!==!1,mcp:s.mcp!==!1&&$.some(e=>!!e.lens)}}));const f=(0,observability_1.createObservability)(o,{trafficFlushMs:S}),ce=createClientErrorLogGate(),k=h2BudgetFromEnv(),le=createClientErrorLogGate(3e4),q=createH2Governor(k,(e,t)=>{le(e)&&console.warn(`[gateway] h2 aggregate budget: ${e} (${JSON.stringify(t)}). These are the FRONT DOOR's ceilings, not an upstream's \u2014 raise/disable with LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS / LENSMCP_MAX_CONNECTIONS (0 = unbounded).`),f.emit(e==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${e}`,`gateway-h2-budget:${e}`,{kind:"h2-aggregate-budget",event:e,project:"gateway",...t})}),M=(0,lifecycle_1.createServiceLayer)(w,f),O=(0,lens_children_1.createLensChildren)(w,f,s),j=new Map;let W;if(s.middleware){const e=require(path.resolve(r.root,s.middleware));W=e.default??e}const D=(0,workspace_middleware_1.createWorkspaceMiddleware)({ownerKey:m,...W?{owner:W}:{},policy:(0,workspace_middleware_1.guestMiddlewarePolicy)()});let L;try{L=(0,control_1.startControlServer)({rt:w,killService:n((e,t)=>M.killSpawned(e,t),"killService"),restartService:n((e,t)=>M.restartService(e,t),"restartService"),onRegister:n(async e=>{retirePreviousWorkspace(j,e.wsKey,"re-registered"),D.set(e.wsKey,e.root,e.middleware);const t=await O.hostWorkspaceDashboard(e),T=await O.hostWorkspaceApps(e);j.set(e.wsKey,()=>{try{t?.()}finally{T()}})},"onRegister"),onUnregister:n(e=>{retirePreviousWorkspace(j,e.wsKey,"unregistered"),D.drop(e.wsKey),M.stopBuildHost(e.root)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const b=(0,proxy_1.createProxy)(w,f,M),Q=buildEdgeVerifierPool(()=>w.routes),Z=(0,auth_1.createEdgeAuth)(w,f,Q),ue=n(e=>{const t=e.route?.wsKey,T=D.for(t);if(T)try{const i=t?w.registry.get(t):void 0;T(e.req,{...t?{wsKey:t}:{},...i?{root:i.root}:{},env:i?.instance?.env??{}})}catch(i){f.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:i.message}),e.route&&f.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${i.message}`)}},"builtinMiddleware"),ee=s.hooks??{},se=(0,hooks_1.createHooks)({...ee,afterAuth:[ue,...ee.afterAuth??[]]}),re=(0,handler_1.createHandler)({rt:w,obs:f,auth:Z,proxy:b,hooks:se}),te=(0,upgrade_1.createUpgrade)({rt:w,auth:Z,proxy:b,hooks:se,obs:f}),de=f.startEdgeFlusher(),pe=M.startSweeper();await O.bootSingletonsAndApps();for(const e of N)e.decl.eager&&M.ensureUp(e);let P,F,I;if(l){const e=require("../../../basic-ssl"),t=(0,service_keys_1.gatewayCacheDir)(r.root);I=n(()=>e.getCertificateSync(t,"gateway",w.routes.map(T=>T.host).filter(T=>!!T)),"mintCert"),P=I(),F=e.caCertPath()}const U=[],H=[],K=s.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const e of h){const t=P?require("node:http2").createSecureServer({key:P,cert:P,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:s.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:s.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(i,E)=>re(i,E)):http.createServer(re);t.on("upgrade",te),t.on("error",i=>console.error(`[gateway] port ${e}: ${i.code}`)),t.on("connection",guardInboundSocket),t.on("secureConnection",guardInboundSocket),t.on("tlsClientError",(i,E)=>E.destroy()),t.on("clientError",(i,E)=>{const c=i,g=classifyClientError(c.code,c.message);if(g.report&&ce(g.cause)){const v=E.remoteAddress??"unknown",C=g.status===null?"dropped":String(g.status);console.warn(`[gateway] inbound H1 clientError from ${v}: ${g.cause} (${c.code??c.message}${c.bytesParsed!==void 0?`, bytesParsed=${c.bytesParsed}`:""}) \u2192 ${C}. This status is the GATEWAY's, not the upstream service's.`),f.emit("warning",`inbound clientError: ${g.cause} \u2192 ${C}`,`gateway-client-error:${g.cause}`,{kind:"gateway-client-error",cause:g.cause,code:c.code??null,status:g.status,peer:v,bytesParsed:c.bytesParsed??null})}g.status!==null&&E.writable&&E.end(`HTTP/1.1 ${g.status} ${CLIENT_ERROR_REASON[g.status]}\r
|
|
2
2
|
\r
|
|
3
|
-
`),E.destroy()}),t.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,t.headersTimeout=s.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,
|
|
3
|
+
`),E.destroy()}),t.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,t.headersTimeout=s.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,K>0&&(t.maxConnections=K);const T=s.connectionsCheckingIntervalMs??types_1.GATEWAY_CONN_CHECK_MS;if(T>0&&(t.connectionsCheckingInterval=T),P!=null){const i=t,{NGHTTP2_CANCEL:E}=require("node:http2").constants;i.on("session",c=>{if(!q.admitSession(c)){c.on("error",()=>{});return}let g=0,v=0;c.on("stream",C=>{q.admitStream(c,C)&&C.once("close",()=>{c.destroyed?v+=1:C.rstCode===E&&(g+=1)})}),c.once("close",()=>{v!==0&&(console.warn(`[gateway] h2 session closed with ${v} stream(s) in flight after ${g} client reset(s) \u2014 every in-flight fetch on that connection failed at once (ERR_HTTP2_PROTOCOL_ERROR in the browser). Reset budget: LENSMCP_H2_RESET_BURST/LENSMCP_H2_RESET_RATE (issues/closed/foodguard-14).`),f.emit("warning",`h2 session lost with ${v} stream(s) in flight`,"gateway-h2-session-lost",{kind:"h2-session-lost",project:"gateway",inflight:v,clientResets:g}))}),c.on("error",C=>{const _e=C.code??C.message;console.warn(`[gateway] h2 session error (${_e}) \u2014 dropping this session only`),c.destroyed||c.destroy()})}),i.on("sessionError",c=>{console.warn(`[gateway] h2 sessionError: ${c.code??c.message}`)})}await new Promise(i=>{t.listen(e,()=>{const E=t.address()?.port??e;H.push(E),console.log(`[gateway] Listening on ${P?"https":"http"}://localhost:${E}`),i()})}),U.push(t)}I&&(V=n(()=>{const e=I();for(const t of U)t.setSecureContext?.({key:e,cert:e})},"refreshCert")),console.log("[gateway] Routes:");for(const e of R){const t=e.pool?`pods@${e.pool.dir}`:e.target;console.log(`[gateway] ${e.host??"(default)"} \u2192 ${t}${e.prependPrefix?` (+${e.prependPrefix})`:""}${e.internal?" [internal: no middleware]":""} [${e.project}]`)}F&&console.log(`[gateway] CA: ${F} \u2014 one-time setup via the trust executor.`);const Y=n(e=>e>0?String(e):"unbounded","unbounded");return console.log(`[gateway] front-door budget: \u2264${Y(K)} connections, \u2264${Y(k.maxSessions)} h2 sessions (each \u2264512 MB session memory, \u2264512 streams), \u2264${Y(k.maxStreams)} open streams in total, idle h2 session reclaimed after ${k.idleSessionMs>0?`${Math.round(k.idleSessionMs/1e3)}s`:"never"}. Override: LENSMCP_MAX_CONNECTIONS / LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS (0 = unbounded).`),f.emit("info",`gateway up: ${R.length} routes on ${H.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:H,routes:R.map(e=>({host:e.host??"(default)",project:e.project,mode:e.pool?"pods":"tcp",internal:!!e.internal,prependPrefix:e.prependPrefix}))}),L&&process.env.LENSMCP_GUEST_REPLAY!=="0"&&L.replay().catch(e=>console.warn("[gateway] guest replay failed:",e.message)),{ports:H,routes:R,services:N,stop:n(async()=>{if(!u){u=!0,clearInterval(de),clearInterval(pe),f.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),Q.stop(),L?.close(),(0,workspace_registry_1.unregisterWorkspace)(m);for(const e of N)M.killSpawned(e,"gateway shutdown");M.stopRetiringTrees(),await M.stopBuildHost(),await O.reapAll(),te.closeTunnels(),await Promise.all(U.map(e=>new Promise(t=>{let T=!1;const i=n(()=>{T||(T=!0,t())},"done"),E=e;e.close(()=>i());try{E.closeIdleConnections?.()}catch{}setTimeout(()=>{try{E.closeAllConnections?.()}catch{}i()},types_1.GATEWAY_STOP_GRACE_MS).unref?.()}))),b.closeProxy(),b.destroyAgents(),q.dispose(),(0,workspace_log_1.closeWorkspaceLogs)(),d()}},"stop"),...w.lensSupervisor?{lensSupervisor:w.lensSupervisor}:{},...w.graphStores?{graphStores:w.graphStores}:{}}}A(startGateway,"startGateway"),n(startGateway,"startGateway");
|
|
@@ -778,6 +778,10 @@ export interface GatewayRuntime {
|
|
|
778
778
|
sourceSetChangedFor?: Map<string, number>;
|
|
779
779
|
/** Exact path evidence paired with `sourceSetChangedFor`. */
|
|
780
780
|
sourceSetChangesFor?: Map<string, SourceSetChange>;
|
|
781
|
+
/** Projects whose own scope the scanner has diffed at least once — for them an absent `sourceSetChangedFor`
|
|
782
|
+
* entry means «nothing changed in my scope», not «fall back to the wide clock» (issues/closed/foodguard-76).
|
|
783
|
+
* Mutable, written only by the scanner. */
|
|
784
|
+
sourceSetScoped?: Set<string>;
|
|
781
785
|
/** Workspace-wide fallback evidence when a project scope is unavailable. */
|
|
782
786
|
sourceSetChange?: SourceSetChange;
|
|
783
787
|
/** Absolute project root per project name (from `projectsConfigurations`) — lets the scanner derive each
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export interface WorkerEntryPoint {
|
|
2
2
|
name: string;
|
|
3
3
|
entryPath: string;
|
|
4
|
+
/** This worker's own environment, merged over the devserver's. Pods never see it. */
|
|
5
|
+
env?: Record<string, string | number | boolean>;
|
|
6
|
+
/** The PORT it is given — 0 (a free one, the default), never the devserver's own. */
|
|
7
|
+
port?: number;
|
|
4
8
|
}
|
|
5
9
|
|
|
6
10
|
export interface NodeExternalsConfig {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"workers": {
|
|
42
42
|
"type": "array",
|
|
43
|
-
"description": "Worker entry points. Each produces a separate [name].js bundle compiled alongside the main entry in watch mode.",
|
|
43
|
+
"description": "Worker entry points. Each produces a separate [name].js bundle compiled alongside the main entry in watch mode, and runs as its own process beside the pods: started again when it crashes (the wait doubles with each crash in a row, up to 60 s), replaced on every build, stopped with its devserver, and shown on the lens (Cluster page, observe_current_status).",
|
|
44
44
|
"items": {
|
|
45
45
|
"type": "object",
|
|
46
46
|
"properties": {
|
|
@@ -51,6 +51,19 @@
|
|
|
51
51
|
"entryPath": {
|
|
52
52
|
"type": "string",
|
|
53
53
|
"description": "Path to the worker entry file. './' paths are relative to project root, otherwise relative to workspace root."
|
|
54
|
+
},
|
|
55
|
+
"env": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "Environment variables for this worker only, merged over the devserver's (e.g. { \"ROLE\": \"worker\" }). Pods never see them.",
|
|
58
|
+
"additionalProperties": {
|
|
59
|
+
"type": ["string", "number", "boolean"]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"port": {
|
|
63
|
+
"type": "integer",
|
|
64
|
+
"minimum": 0,
|
|
65
|
+
"maximum": 65535,
|
|
66
|
+
"description": "The PORT this worker is given. Default 0: the kernel picks a free one, so a worker that listens (a health endpoint) never collides with the devserver's port or with its predecessor finishing its work after a build. A fixed port makes the next build's worker wait until its predecessor has exited."
|
|
54
67
|
}
|
|
55
68
|
},
|
|
56
69
|
"required": [
|