@lensmcp/cluster 1.23.13 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-host.d.ts +1 -0
- package/build-host.js +1 -0
- package/create-webpack-dev.d.ts +74 -40
- package/create-webpack-dev.js +1 -1
- package/dev-devtool.d.ts +45 -0
- package/dev-devtool.js +1 -0
- package/devserver-notifier.d.ts +57 -0
- package/devserver-notifier.js +1 -0
- package/executors/gateway/body-stall.d.ts +13 -1
- package/executors/gateway/body-stall.js +1 -1
- package/executors/gateway/canonical-path.d.ts +1 -1
- package/executors/gateway/client-ip.d.ts +1 -1
- package/executors/gateway/gateway.impl.js +1 -1
- package/executors/gateway/jwt-claims.d.ts +1 -1
- package/executors/gateway/prod-config.d.ts +1 -1
- package/executors/gateway/prod-runtime/body-stall.d.ts +1 -1
- package/executors/gateway/rate-limit.d.ts +1 -1
- package/executors/gateway/runtime/auth.d.ts +1 -1
- package/executors/gateway/runtime/chooser.d.ts +2 -2
- package/executors/gateway/runtime/control.d.ts +3 -3
- package/executors/gateway/runtime/control.js +2 -2
- package/executors/gateway/runtime/dev-auth.d.ts +1 -1
- package/executors/gateway/runtime/edge.d.ts +3 -3
- package/executors/gateway/runtime/guest-graph.d.ts +113 -0
- package/executors/gateway/runtime/guest-graph.js +2 -0
- package/executors/gateway/runtime/lens-children.d.ts +11 -1
- package/executors/gateway/runtime/lens-children.js +1 -1
- package/executors/gateway/runtime/lifecycle.d.ts +54 -8
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/process-names.d.ts +18 -0
- package/executors/gateway/runtime/process-names.js +1 -0
- package/executors/gateway/runtime/project-graph-store.d.ts +74 -0
- package/executors/gateway/runtime/project-graph-store.js +1 -0
- package/executors/gateway/runtime/proxy.d.ts +2 -2
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/scope.d.ts +1 -1
- package/executors/gateway/runtime/serve-launch.d.ts +118 -0
- package/executors/gateway/runtime/serve-launch.js +3 -0
- package/executors/gateway/runtime/server.d.ts +3 -3
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/shared-build.d.ts +65 -0
- package/executors/gateway/runtime/shared-build.js +4 -0
- package/executors/gateway/runtime/types.d.ts +35 -9
- package/executors/gateway/runtime/types.js +1 -1
- package/executors/gateway/runtime/workspace-log.js +1 -1
- package/executors/gateway/schema.d.ts +3 -0
- package/executors/gateway/schema.json +4 -0
- package/executors/serve/schema.d.ts +2 -0
- package/executors/serve/schema.json +4 -0
- package/executors/serve/serve.impl.d.ts +12 -6
- package/executors/serve/serve.impl.js +1 -1
- package/executors/trust/hostname.d.ts +1 -1
- package/exit-reason.d.ts +1 -1
- package/fork-ts-check-reporter.d.ts +12 -0
- package/fork-ts-check-reporter.js +1 -1
- package/graph-snapshot.d.ts +1 -0
- package/graph-snapshot.js +1 -0
- package/heap-snapshot.d.ts +66 -0
- package/heap-snapshot.js +1 -0
- package/hot-swap-registries.d.ts +1 -1
- package/inspector-secret.d.ts +39 -0
- package/inspector-secret.js +1 -0
- package/main.devserver.d.ts +33 -5
- package/main.devserver.js +11 -9
- package/package.json +4 -4
- package/perf/heavy-barrel-import.d.ts +135 -0
- package/perf/heavy-barrel-import.js +1 -0
- package/perf/perf-config.d.ts +29 -0
- package/perf/perf-config.js +1 -0
- package/perf/perf-rule-events.d.ts +71 -0
- package/perf/perf-rule-events.js +2 -0
- package/perf/perf-rules-analysis.d.ts +28 -0
- package/perf/perf-rules-analysis.js +1 -0
- package/perf/perf-rules-plugin.d.ts +71 -0
- package/perf/perf-rules-plugin.js +1 -0
- package/perf/perf-rules-worker.d.ts +1 -0
- package/perf/perf-rules-worker.js +1 -0
- package/perf/source-scan.d.ts +59 -0
- package/perf/source-scan.js +5 -0
- package/perf/workspace-libraries.d.ts +52 -0
- package/perf/workspace-libraries.js +2 -0
- package/pod-memory-events.d.ts +71 -0
- package/pod-memory-events.js +2 -0
- package/pod-memory.d.ts +258 -0
- package/pod-memory.js +1 -0
- package/process-name.d.ts +45 -0
- package/process-name.js +1 -1
- package/serve-runner.d.ts +2 -0
- package/serve-runner.js +1 -0
- package/shared-build/attach.d.ts +56 -0
- package/shared-build/attach.js +3 -0
- package/shared-build/host.d.ts +52 -0
- package/shared-build/host.js +4 -0
- package/shared-build/protocol.d.ts +118 -0
- package/shared-build/protocol.js +1 -0
- package/shared-build/shared-compilation.d.ts +91 -0
- package/shared-build/shared-compilation.js +3 -0
- package/shared-build/signature.d.ts +52 -0
- package/shared-build/signature.js +1 -0
- package/stdio-guard.d.ts +1 -1
- package/swap-recycle.d.ts +21 -0
- package/swap-recycle.js +1 -1
- package/tsgo-check-plugin.d.ts +1 -1
- package/workspace-paths.d.ts +76 -0
- package/workspace-paths.js +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ServiceCtl } from './types';
|
|
2
|
+
export interface GatewayProcessNamesOptions {
|
|
3
|
+
/** Services may start through the serve runner (it is not off, and the gateway holds a project graph). A service
|
|
4
|
+
* the runner cannot start still goes through `nx run`, so both names are linked; `false`: `nx run` only. */
|
|
5
|
+
runner: boolean;
|
|
6
|
+
/** The shared compiler is on: the workspace's build host will run. */
|
|
7
|
+
sharedBuild: boolean;
|
|
8
|
+
/** The gateway's own workspace label (its key, else its directory name) — the build host's name. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** The lens scope key: the managed dashboard / MCP children's names. Absent: neither runs. */
|
|
11
|
+
lens?: {
|
|
12
|
+
key: string;
|
|
13
|
+
dashboard: boolean;
|
|
14
|
+
mcp: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** The names, as the code that spawns each child derives them (lifecycle.ts, serve.impl.ts, main.devserver.ts, lens-children.ts). */
|
|
18
|
+
export declare function gatewayProcessNames(services: ReadonlyArray<Pick<ServiceCtl, 'project' | 'wsKey' | 'root' | 'decl'>>, opts: GatewayProcessNamesOptions): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var l=Object.defineProperty;var c=(o,e)=>l(o,"name",{value:e,configurable:!0});var t=Object.defineProperty,n=c((o,e)=>t(o,"name",{value:e,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.gatewayProcessNames=gatewayProcessNames;const tslib_1=require("tslib"),path=tslib_1.__importStar(require("node:path")),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),serve_launch_1=require("./serve-launch");function gatewayProcessNames(o,e){const s=[];for(const r of o){const a=r.wsKey||path.basename(r.root),p=r.decl.service??r.project;e.runner&&s.push((0,process_name_1.processName)(a,r.project,serve_launch_1.SERVE_RUNNER_ROLE)),s.push((0,process_name_1.processName)(a,r.project,"nx"),(0,process_name_1.processName)(a,p,"dev"),(0,process_name_1.processName)(a,p,"pod"))}return e.sharedBuild&&s.push((0,process_name_1.processName)(e.label,protocol_1.BUILD_HOST_ROLE)),e.lens?.dashboard&&s.push((0,process_name_1.processName)("lensmcp",e.lens.key,"dashboard")),e.lens?.mcp&&s.push((0,process_name_1.processName)("lensmcp",e.lens.key,"mcp")),[...new Set(s.filter(Boolean))]}c(gatewayProcessNames,"gatewayProcessNames"),n(gatewayProcessNames,"gatewayProcessNames");
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ProjectGraph } from '@nx/devkit';
|
|
2
|
+
/** nx's configuration source maps: project root → property path (`targets.serve-hmr.options.x`) →
|
|
3
|
+
* `[file that declared it, plugin that read it]`. */
|
|
4
|
+
export type NxSourceMaps = Record<string, Record<string, [file: string | null, plugin: string]>>;
|
|
5
|
+
/** What one graph computation returns. `sourceMaps` is absent from an nx too old to report them. */
|
|
6
|
+
export interface ComputedProjectGraph {
|
|
7
|
+
projectGraph: ProjectGraph;
|
|
8
|
+
sourceMaps?: NxSourceMaps;
|
|
9
|
+
}
|
|
10
|
+
/** Compute the workspace's project graph — the same call `nx run` makes before it starts a task. */
|
|
11
|
+
export type ComputeProjectGraph = () => Promise<ComputedProjectGraph>;
|
|
12
|
+
export interface ProjectGraphSnapshot extends ComputedProjectGraph {
|
|
13
|
+
/** The workspace root the graph describes (absolute). */
|
|
14
|
+
readonly root: string;
|
|
15
|
+
/** When the computation that produced it started (ms epoch). */
|
|
16
|
+
readonly startedAt: number;
|
|
17
|
+
/** Declaring file (absolute) → its stat signature when the computation started. */
|
|
18
|
+
readonly files: ReadonlyMap<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export interface ProjectGraphStore {
|
|
21
|
+
readonly root: string;
|
|
22
|
+
current(): ProjectGraphSnapshot | undefined;
|
|
23
|
+
/** Why the current snapshot no longer describes the files on disk (`undefined` = it still does). One
|
|
24
|
+
* `stat` per declaring file — a few hundred on a large workspace, a few milliseconds. */
|
|
25
|
+
staleReason(): string | undefined;
|
|
26
|
+
/** Recompute now. Concurrent callers share one computation; a failure keeps the previous snapshot. */
|
|
27
|
+
refresh(): Promise<ProjectGraphSnapshot | undefined>;
|
|
28
|
+
/** Kick a refresh when the snapshot is stale. Never awaits, never throws — the sweeper calls it every tick. */
|
|
29
|
+
refreshIfStale(): void;
|
|
30
|
+
/** The last computation's error message, until one succeeds. */
|
|
31
|
+
lastError(): string | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface ProjectGraphStoreOptions {
|
|
34
|
+
/** A snapshot computed somewhere else, with its own fingerprint — what a registering workspace forwards. */
|
|
35
|
+
seed?: ProjectGraphSnapshot;
|
|
36
|
+
/** Why there is no snapshot yet (`lastError`), until a computation succeeds. */
|
|
37
|
+
error?: string;
|
|
38
|
+
/** After a failed computation, `refreshIfStale` waits this long before computing again. Default 0: every call
|
|
39
|
+
* may retry (an in-process computation); a child process per sweep tick is too much to retry that often. */
|
|
40
|
+
retryAfterFailureMs?: number;
|
|
41
|
+
/** Called after every computation that succeeded. */
|
|
42
|
+
onRefreshed?: (snapshot: ProjectGraphSnapshot) => void;
|
|
43
|
+
}
|
|
44
|
+
/** The gateway's project graphs, one store per workspace root it serves — its own and each registered one's. */
|
|
45
|
+
export interface ProjectGraphStores {
|
|
46
|
+
/** The store for this workspace root (compared as the filesystem names it), if the gateway holds one. */
|
|
47
|
+
get(root: string): ProjectGraphStore | undefined;
|
|
48
|
+
/** Every store — the sweeper keeps each one current. */
|
|
49
|
+
all(): ProjectGraphStore[];
|
|
50
|
+
/** Hold `store` for its root, replacing any store that root had (a re-registration). */
|
|
51
|
+
set(store: ProjectGraphStore): void;
|
|
52
|
+
/** Forget the store of a workspace that left. */
|
|
53
|
+
delete(root: string): void;
|
|
54
|
+
}
|
|
55
|
+
export declare function createProjectGraphStores(...initial: ProjectGraphStore[]): ProjectGraphStores;
|
|
56
|
+
/** `mtime:size:inode` of a file, or `-` when it does not exist. */
|
|
57
|
+
export declare function statSignature(file: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* "Now" on the filesystem's clock, read by writing a file — so a later write compares like with like.
|
|
60
|
+
* `Date.now()` is whole milliseconds while an mtime has a fraction, so a file written a moment BEFORE the
|
|
61
|
+
* computation began read as written during it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function filesystemNow(): number;
|
|
64
|
+
/** The files whose contents DECLARE the graph's configuration: `nx.json` plus every project's
|
|
65
|
+
* `project.json` and `package.json` (nx merges both into the project's targets). */
|
|
66
|
+
export declare function declaringFiles(root: string, graph: ProjectGraph): string[];
|
|
67
|
+
/**
|
|
68
|
+
* The graph computation the gateway runs in its own process, through the workspace's own nx (the one
|
|
69
|
+
* `nx run` there would use). With the nx daemon on this is one IPC round trip; with it off nx
|
|
70
|
+
* recomputes in-process, reusing its file-hash and plugin caches, and rewrites the on-disk graph cache
|
|
71
|
+
* the executor reads.
|
|
72
|
+
*/
|
|
73
|
+
export declare function computeProjectGraphInProcess(root: string): Promise<ComputedProjectGraph>;
|
|
74
|
+
export declare function createProjectGraphStore(root: string, compute: ComputeProjectGraph, options?: ProjectGraphStoreOptions): ProjectGraphStore;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var m=Object.defineProperty;var s=(t,e)=>m(t,"name",{value:e,configurable:!0});var g=Object.defineProperty,o=s((t,e)=>g(t,"name",{value:e,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createProjectGraphStores=createProjectGraphStores,exports.statSignature=statSignature,exports.filesystemNow=filesystemNow,exports.declaringFiles=declaringFiles,exports.computeProjectGraphInProcess=computeProjectGraphInProcess,exports.createProjectGraphStore=createProjectGraphStore;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),node_module_1=require("node:module"),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),registration_guards_1=require("./registration-guards");function createProjectGraphStores(...t){const e=new Map,n=o(r=>{e.set((0,registration_guards_1.canonicalRoot)(r.root),r)},"set");for(const r of t)n(r);return{get:o(r=>e.get((0,registration_guards_1.canonicalRoot)(r)),"get"),all:o(()=>[...e.values()],"all"),set:n,delete:o(r=>{e.delete((0,registration_guards_1.canonicalRoot)(r))},"delete")}}s(createProjectGraphStores,"createProjectGraphStores"),o(createProjectGraphStores,"createProjectGraphStores");function statSignature(t){const e=fs.statSync(t,{throwIfNoEntry:!1});return e?`${e.mtimeMs}:${e.size}:${e.ino}`:"-"}s(statSignature,"statSignature"),o(statSignature,"statSignature");const CHANGED_DURING_COMPUTE="?";function filesystemNow(){const t=path.join(os.tmpdir(),`lensmcp-graph-clock-${process.pid}`);try{fs.writeFileSync(t,"");const e=fs.statSync(t).mtimeMs;return fs.unlinkSync(t),e}catch{return Date.now()}}s(filesystemNow,"filesystemNow"),o(filesystemNow,"filesystemNow");function modifiedAfter(t,e){const n=fs.statSync(t,{throwIfNoEntry:!1});return!!n&&n.mtimeMs>e}s(modifiedAfter,"modifiedAfter"),o(modifiedAfter,"modifiedAfter");function declaringFiles(t,e){const n=[path.join(t,"nx.json")];for(const r of Object.values(e.nodes)){const c=path.join(t,r.data.root);n.push(path.join(c,"project.json"),path.join(c,"package.json"))}return[...new Set(n)]}s(declaringFiles,"declaringFiles"),o(declaringFiles,"declaringFiles");async function computeProjectGraphInProcess(t){const e=(0,node_module_1.createRequire)(path.join(t,"package.json"))("nx/src/project-graph/project-graph");return e.createProjectGraphAndSourceMapsAsync?e.createProjectGraphAndSourceMapsAsync():{projectGraph:await e.createProjectGraphAsync()}}s(computeProjectGraphInProcess,"computeProjectGraphInProcess"),o(computeProjectGraphInProcess,"computeProjectGraphInProcess");function createProjectGraphStore(t,e,n={}){let r=n.seed,c,p=n.seed?void 0:n.error,f=0;const h=o(()=>{if(!r)return"no project graph yet";for(const[u,d]of r.files)if(statSignature(u)!==d)return`${path.relative(t,u)} changed since the gateway read the project graph`},"staleReason"),j=o(()=>(c??=(async()=>{const u=Date.now(),d=filesystemNow(),y=new Map;if(r)for(const a of r.files.keys())y.set(a,statSignature(a));try{const a=await e(),i=new Map;for(const l of declaringFiles(t,a.projectGraph))i.set(l,y.get(l)??(modifiedAfter(l,d)?CHANGED_DURING_COMPUTE:statSignature(l)));r={root:t,startedAt:u,files:i,...a},p=void 0,f=0,n.onRefreshed?.(r)}catch(a){const i=a?.message??String(a);i!==p&&console.warn(`[gateway] project graph refresh failed for ${t} (its services start through \`nx run\` until it succeeds): ${i}`),p=i,f=Date.now()+(n.retryAfterFailureMs??0)}return r})().finally(()=>{c=void 0}),c),"refresh");return{root:t,current:o(()=>r,"current"),staleReason:h,refresh:j,refreshIfStale:o(()=>{c||Date.now()<f||!h()||j()},"refreshIfStale"),lastError:o(()=>p,"lastError")}}s(createProjectGraphStore,"createProjectGraphStore"),o(createProjectGraphStore,"createProjectGraphStore");
|
|
@@ -13,7 +13,7 @@ import type { ServiceLayer } from './lifecycle';
|
|
|
13
13
|
* then re-sent the same method, the same URL and the same credentials — the caller's cookies, its
|
|
14
14
|
* `authorization`, the gateway's own stamped `x-internal-token` and `x-forwarded-*` identity — every
|
|
15
15
|
* 300ms for the whole 8s heal window, i.e. up to ~26 replays of a side-effecting call the origin had
|
|
16
|
-
* already applied (issues/codex-security
|
|
16
|
+
* already applied (issues/closed/codex-security-28). Node flushes a `ClientRequest`'s header block the moment
|
|
17
17
|
* its socket connects, so those replays reach the upstream's request handler even though the retry can
|
|
18
18
|
* no longer produce a body — which is the second half of the same defect:
|
|
19
19
|
*
|
|
@@ -25,7 +25,7 @@ import type { ServiceLayer } from './lifecycle';
|
|
|
25
25
|
* byte-for-byte.
|
|
26
26
|
*
|
|
27
27
|
* The prod gateway reached the same place in round 2 by cutting its retry count to 1
|
|
28
|
-
* (issues/prod-gateway-2
|
|
28
|
+
* (issues/closed/prod-gateway-2-07); the dev path never inherited it — the *two gateways, two codebases* rule.
|
|
29
29
|
*/
|
|
30
30
|
export declare function isReplayableRequest(req: Pick<http.IncomingMessage, 'method' | 'headers'>): boolean;
|
|
31
31
|
export interface ProxyServer {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var G=Object.defineProperty;var P=(n,c)=>G(n,"name",{value:c,configurable:!0});var K=Object.defineProperty,s=P((n,c)=>K(n,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),body_stall_1=require("../body-stall"),types_1=require("./types"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys");function isConnError(n){const c=n.code??"";return c==="ECONNREFUSED"||c==="ECONNRESET"||c==="ETIMEDOUT"||c==="EPIPE"||c==="EHOSTUNREACH"||c==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(n.message)}P(isConnError,"isConnError"),s(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(n){if(!IDEMPOTENT_METHODS.has((n.method??"GET").toUpperCase())||n.headers["transfer-encoding"]!==void 0)return!1;const c=n.headers["content-length"];return!(c!==void 0&&String(c).trim()!=="0")}P(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(n,c){return c.headersSent||c.writableEnded||c.destroyed===!0||n.aborted===!0}P(edgeGone,"edgeGone"),s(edgeGone,"edgeGone");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),UPSTREAM_CONNECTION_HEADERS=["connection","keep-alive","proxy-connection"],httpProxy=require("http-proxy");function declaredFirstByteMs(n){const c=Number(n);return Number.isFinite(c)&&c>0?c:types_1.POD_RESPONSE_TIMEOUT_MS}P(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");function armFirstByteGuard(n){let c,v=0,O=!1,A=!1;const M=s(k=>{c=setTimeout(C,k),c.unref?.()},"arm"),C=s(()=>{if(!(O||!n.pending())){if(v+=n.firstByteMs,n.ceilingMs<=0||v>=n.ceilingMs){n.onWedged(`no response within ${v}ms (wedged socket) \u2014 evicted + respawning`);return}(0,pod_probe_1.probePodAlive)(n.socketPath,void 0,n.serviceKey).then(k=>{if(!(O||!n.pending())){if(!k){n.onWedged(`no response within ${v}ms and no answer on ${types_1.POD_ALIVE_PATH} (wedged socket) \u2014 evicted + respawning`);return}A||(A=!0,n.onSlow(v,k)),M(Math.max(n.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))}})}},"onDeadline");return M(n.firstByteMs),{clear:s(()=>{O=!0,c&&clearTimeout(c)},"clear")}}P(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(n,c,v){const{recordEdge:O,finishTrace:A,traceStep:M,emit:C}=c,k=s(async e=>{if(!e.lens||!n.lensSupervisor)return"unsupported";let r;const t=new Promise(p=>{r=setTimeout(()=>p("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),u=n.lensSupervisor.recover(e.lens,"proxy-connection").then(p=>p?"ready":"failed").catch(()=>"failed"),h=await Promise.race([u,t]);r&&clearTimeout(r);const m=n.lensSupervisor.status(e.lens);return h==="failed"&&(!m||m.state==="ready")?"unsupported":h},"waitForManagedRecovery"),U=s((e,r,t,u,h,m)=>{if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}const p=e.lens?n.lensSupervisor?.status(e.lens):void 0,E=m==="timeout"||p?.state==="starting"||p?.state==="replacing",_=(0,gateway_errors_1.ensureRequestId)(r),d=e.lens?{project:e.project,reason:p?.reason??"connection-failure",generation:p?.generation??0,...E?{retryAfterMs:2e3}:{}}:void 0,o=E?503:502,a=E?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${_} project=${e.project} target=${String(u)} state=${p?.state??"unmanaged"} generation=${p?.generation??0}: ${h.message}`),C("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:_,project:e.project,target:String(u),state:p?.state??"unmanaged",generation:p?.generation,error:h.message}),(0,edge_1.sendError)(t,r,o,a,E?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,d,E?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),x=(0,body_stall_1.createBodyStallGuard)(n.bodyStallMs??types_1.BODY_STALL_MS),I={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},D=new http.Agent(I),q=new(require("node:https")).Agent(I),N=s(e=>typeof e=="string"&&e.startsWith("https:")?q:D,"agentForTarget"),R=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:D}),L=s((e,r,t)=>{const u=s(()=>{t()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",u),e.once("close",()=>r.removeListener("close",u))},"propagateEdgeCancel");R.on("proxyReq",(e,r,t)=>{for(const u of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(u);e.setHeader("connection","keep-alive"),L(e,t,()=>t.writableEnded)}),R.on("proxyRes",(e,r,t)=>{const u=r.__lensmcpPod;u&&(e.headers["x-lensmcp-pod"]=u),delete e.headers["x-powered-by"],e.headers.server="LensMCP",t&&L(e,t,()=>t.writableEnded),t&&x.armResponse(t)});const B=s((e,r,t,u)=>{(0,edge_1.stampForwardingHeaders)(n,r),x.arm(r);const h={},m=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[i,y]of Object.entries(r.headers))y===void 0||i.startsWith(":")||HOP_BY_HOP.has(i)||m&&(i==="content-length"||i==="transfer-encoding")||(h[i]=y);let p=http,E;if(typeof e=="object")E={socketPath:e.socketPath,path:r.url,method:r.method,headers:h,agent:D};else{const i=new URL(e),y=i.protocol==="https:";p=y?require("node:https"):http,h.host=i.host,E={protocol:i.protocol,hostname:i.hostname,port:i.port||(y?443:80),path:r.url,method:r.method,headers:h,agent:N(e)}}const _=t;_.__lensmcpErrContained||(_.__lensmcpErrContained=!0,t.on("error",()=>{}));let d;const o=s(()=>{t.writableEnded||g.destroy()},"onReqGone"),a=s(()=>{d&&!d.destroyed&&d.destroy()},"onEdgeClose"),l=s(()=>{r.removeListener("error",o),r.removeListener("aborted",o),r.removeListener("close",o),t.removeListener("close",a)},"detach"),g=p.request(E,i=>{if(d=i,u.onResponse(),t.headersSent||t.writableEnded||t.destroyed){i.destroy();return}const y={};for(const[w,f]of Object.entries(i.headers))f===void 0||HOP_BY_HOP.has(w)||(y[w]=f);delete y["x-powered-by"],y.server="LensMCP";const j=r.__lensmcpPod;j&&(y["x-lensmcp-pod"]=j);try{t.writeHead(i.statusCode??502,y)}catch(w){console.error("[gateway] h2 relay: bad response header, resetting stream:",w.message),i.destroy(),t.destroyed||t.destroy(w);return}i.pipe(t),x.armResponse(t);const $=s(()=>{!t.writableEnded&&!t.destroyed&&t.destroy()},"abortEdge");i.once("error",$),i.once("close",$),t.on("close",a)});g.once("error",i=>u.onError(i)),g.once("close",l),r.on("error",o),r.on("aborted",o),r.on("close",o),m?g.end():r.pipe(g)},"pipeUpstream"),W=s(async(e,r,t)=>{const u=r.__lensmcpTrace;if(e.pool){const d=e.svc;d&&(d.lastUsed=Date.now(),d.inflight+=1,t.on("close",()=>{d.inflight-=1,d.lastUsed=Date.now()}));let o=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!o&&d&&(M(u,"cold-start",{project:e.project}),await v.ensureUp(d)&&(o=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),d&&o&&v.markUp(d,"observed"),d&&v.scaleUp(d),!o){const f=d?.lastErrors?.length?` Service errors: ${d.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${f} The watcher recovers and the next request retries automatically.`);return}const a=path.basename(o,".sock");r.__lensmcpPod=a,M(u,"pod-select",{pod:a,pods:e.pool.socks.length,strategy:"round-robin"});let l=!1;const g=s(f=>{e.pool.socks=e.pool.socks.filter(S=>S!==o),e.pool.idx=-1;try{fs.unlinkSync(o)}catch{}console.error(`[gateway] pod ${a} of ${e.project}: ${f}`)},"evictPod"),i=armFirstByteGuard({socketPath:o,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!l&&!t.headersSent&&!t.writableEnded,"pending"),onWedged:s(f=>{l=!0,g(f),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:s((f,S)=>{console.log(`[gateway] pod ${a} of ${e.project}: slow first byte, pod alive (${f}ms, ${S.inflight} in-flight, ${S.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>i.clear());const y=o,j=isReplayableRequest(r);let $=0;const w=s(()=>{$+=1,B({socketPath:y},r,t,{onResponse:s(()=>{i.clear(),l=!0},"onResponse"),onError:s(f=>{if(!l){if(edgeGone(r,t)){i.clear(),l=!0;return}(0,pod_probe_1.podConnectionVerdict)(y,f).then(S=>{if(!l){if(S==="alive"&&$<2&&j&&!edgeGone(r,t)){console.log(`[gateway] pod ${a} of ${e.project}: ${f.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),w();return}if(i.clear(),l=!0,S==="gone"){g(f.message),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} unavailable \u2014 retry.`);return}console.error(`[gateway] pod ${a} of ${e.project}: ${f.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");w();return}const h=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let m=0,p=!1;const E=isReplayableRequest(r),_=s(()=>{if(edgeGone(r,t))return;const d=e.target;B(d,r,t,{onResponse:s(()=>{},"onResponse"),onError:s(o=>{if(isConnError(o)&&(m+=1),e.lens&&isConnError(o)&&E&&m>=2&&!p&&!edgeGone(r,t)){p=!0,k(e).then(a=>{if(!edgeGone(r,t)){if(a==="ready"){_();return}if(a==="unsupported"&&Date.now()<h){setTimeout(_,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}U(e,r,t,e.target,o,a)}});return}if(isConnError(o)&&E&&Date.now()<h&&!edgeGone(r,t)){setTimeout(_,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}U(e,r,t,d,o)},"onError")})},"tryTcp");_()},"forwardNative");return{proxy:R,forward:s(async(e,r,t)=>{const u=Date.now(),h=r.__lensmcpTrace;if(t.on("close",()=>{O(e,Date.now()-u,t.statusCode??0,r.__lensmcpCaller),A(h,e.project,t.statusCode??0,{...r.__lensmcpPod?{pod:r.__lensmcpPod}:{},...r.__lensmcpCaller?{caller:r.__lensmcpCaller}:{}},e)}),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),(0,edge_1.stampForwardingHeaders)(n,r),x.arm(r),(r.httpVersionMajor??1)>=2){await W(e,r,t);return}if(e.pool){const o=e.svc;o&&(o.lastUsed=Date.now(),o.inflight+=1,t.on("close",()=>{o.inflight-=1,o.lastUsed=Date.now()}));let a=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs);if(!a&&o&&(M(h,"cold-start",{project:e.project}),await v.ensureUp(o)&&(a=(0,discovery_1.pickSock)(e.pool,n.scanTtlMs))),o&&a&&v.markUp(o,"observed"),o&&v.scaleUp(o),!a){const T=o?.lastErrors?.length?` Service errors: ${o.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${T} The watcher recovers and the next request retries automatically.`);return}const l=path.basename(a,".sock");r.__lensmcpPod=l,M(h,"pod-select",{pod:l,pods:e.pool.socks.length,strategy:"round-robin"});let g=!1;const i=s(T=>{e.pool.socks=e.pool.socks.filter(b=>b!==a),e.pool.idx=-1;try{fs.unlinkSync(a)}catch{}console.error(`[gateway] pod ${l} of ${e.project}: ${T}`)},"evictPod"),y=armFirstByteGuard({socketPath:a,serviceKey:n.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!g&&!t.headersSent&&!t.writableEnded,"pending"),onWedged:s(T=>{g=!0,i(T),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${l} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onSlow:s((T,b)=>{console.log(`[gateway] pod ${l} of ${e.project}: slow first byte, pod alive (${T}ms, ${b.inflight} in-flight, ${b.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>y.clear());const j=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),$=a,w=isReplayableRequest(r);let f=0;const S=s(()=>{f+=1;const T=Date.now();R.web(r,t,{target:{socketPath:$},agent:D,proxyTimeout:j},b=>{if(!g){if(edgeGone(r,t)){y.clear(),g=!0;return}(0,pod_probe_1.podConnectionVerdict)($,b).then(H=>{if(g)return;const F=Date.now()-T>=j-250;if(H==="alive"&&f<2&&w&&!F&&!edgeGone(r,t)){console.log(`[gateway] pod ${l} of ${e.project}: ${b.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),S();return}if(y.clear(),g=!0,H==="gone"){i(b.message),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${l} of ${e.project} unavailable \u2014 retry.`);return}console.error(`[gateway] pod ${l} of ${e.project}: ${b.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${l} of ${e.project} dropped the connection \u2014 retry.`)})}})},"sendToPod");S();return}const m=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let p=0,E=!1;const _=isReplayableRequest(r),d=s(()=>{if(edgeGone(r,t))return;const o=e.target,a={target:o,autoRewrite:typeof o=="string",changeOrigin:!0,agent:N(o)};R.web(r,t,a,l=>{if(isConnError(l)&&(p+=1),e.lens&&isConnError(l)&&_&&p>=2&&!E&&!edgeGone(r,t)){E=!0,k(e).then(g=>{if(!edgeGone(r,t)){if(g==="ready"){d();return}if(g==="unsupported"&&Date.now()<m){setTimeout(d,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}U(e,r,t,e.target,l,g)}});return}if(isConnError(l)&&_&&Date.now()<m&&!edgeGone(r,t)){setTimeout(d,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}t.destroyed||U(e,r,t,o,l)})},"tryTcp");d()},"forward"),agentForTarget:N,destroyAgents:s(()=>{x.stop(),D.destroy(),q.destroy()},"destroyAgents"),closeProxy:s(()=>{try{R.close?.()}catch{}},"closeProxy")}}P(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
1
|
+
"use strict";var G=Object.defineProperty;var j=(t,c)=>G(t,"name",{value:c,configurable:!0});var K=Object.defineProperty,s=j((t,c)=>K(t,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),body_stall_1=require("../body-stall"),types_1=require("./types"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys");function isConnError(t){const c=t.code??"";return c==="ECONNREFUSED"||c==="ECONNRESET"||c==="ETIMEDOUT"||c==="EPIPE"||c==="EHOSTUNREACH"||c==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(t.message)}j(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 c=t.headers["content-length"];return!(c!==void 0&&String(c).trim()!=="0")}j(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(t,c){return c.headersSent||c.writableEnded||c.destroyed===!0||t.aborted===!0}j(edgeGone,"edgeGone"),s(edgeGone,"edgeGone");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),UPSTREAM_CONNECTION_HEADERS=["connection","keep-alive","proxy-connection"],httpProxy=require("http-proxy");function declaredFirstByteMs(t){const c=Number(t);return Number.isFinite(c)&&c>0?c:types_1.POD_RESPONSE_TIMEOUT_MS}j(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");function armFirstByteGuard(t){let c,$=!1,q=!1,w=(0,body_stall_1.requestBodyArriving)(t.req)?void 0:Date.now();const O=s(()=>{w??=Date.now()},"onBodyEnd");w===void 0&&t.req.once("end",O);const U=s(()=>(w===void 0&&!(0,body_stall_1.requestBodyArriving)(t.req)&&(w=Date.now()),w===void 0?0:Date.now()-w),"heldMs"),D=s(S=>{c=setTimeout(A,S),c.unref?.()},"arm"),A=s(()=>{if($||!t.pending())return;const S=U();if(t.ceilingMs<=0){if(w!==void 0&&S>=t.firstByteMs){t.onWedged(`no response within ${S}ms (wedged socket) \u2014 evicted + respawning`);return}D(w===void 0?t.firstByteMs:t.firstByteMs-S);return}(0,pod_probe_1.probePodAlive)(t.socketPath,void 0,t.serviceKey).then(x=>{if($||!t.pending())return;const T=U();if(!x){t.onWedged(`no response within ${T}ms and no answer on ${types_1.POD_ALIVE_PATH} (wedged socket) \u2014 evicted + respawning`);return}if(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`,x);return}!q&&w!==void 0&&(q=!0,t.onSlow(T,x)),D(Math.max(t.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))})},"onDeadline");return D(t.firstByteMs),{clear:s(()=>{$=!0,c&&clearTimeout(c),t.req.removeListener("end",O)},"clear")}}j(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(t,c,$){const{recordEdge:q,finishTrace:w,traceStep:O,emit:U}=c,D=s(async e=>{if(!e.lens||!t.lensSupervisor)return"unsupported";let r;const o=new Promise(p=>{r=setTimeout(()=>p("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),g=t.lensSupervisor.recover(e.lens,"proxy-connection").then(p=>p?"ready":"failed").catch(()=>"failed"),h=await Promise.race([g,o]);r&&clearTimeout(r);const m=t.lensSupervisor.status(e.lens);return h==="failed"&&(!m||m.state==="ready")?"unsupported":h},"waitForManagedRecovery"),A=s((e,r,o,g,h,m)=>{if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}const p=e.lens?t.lensSupervisor?.status(e.lens):void 0,E=m==="timeout"||p?.state==="starting"||p?.state==="replacing",v=(0,gateway_errors_1.ensureRequestId)(r),d=e.lens?{project:e.project,reason:p?.reason??"connection-failure",generation:p?.generation??0,...E?{retryAfterMs:2e3}:{}}:void 0,n=E?503:502,a=E?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${v} project=${e.project} target=${String(g)} state=${p?.state??"unmanaged"} generation=${p?.generation??0}: ${h.message}`),U("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:v,project:e.project,target:String(g),state:p?.state??"unmanaged",generation:p?.generation,error:h.message}),(0,edge_1.sendError)(o,r,n,a,E?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,d,E?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),S=(0,body_stall_1.createBodyStallGuard)(t.bodyStallMs??types_1.BODY_STALL_MS),x={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},T=new http.Agent(x),I=new(require("node:https")).Agent(x),L=s(e=>typeof e=="string"&&e.startsWith("https:")?I:T,"agentForTarget"),C=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:T}),N=s((e,r,o)=>{const g=s(()=>{o()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",g),e.once("close",()=>r.removeListener("close",g))},"propagateEdgeCancel");C.on("proxyReq",(e,r,o)=>{for(const g of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(g);e.setHeader("connection","keep-alive"),N(e,o,()=>o.writableEnded)}),C.on("proxyRes",(e,r,o)=>{const g=r.__lensmcpPod;g&&(e.headers["x-lensmcp-pod"]=g),delete e.headers["x-powered-by"],e.headers.server="LensMCP",o&&N(e,o,()=>o.writableEnded),o&&S.armResponse(o)});const H=s((e,r,o,g)=>{(0,edge_1.stampForwardingHeaders)(t,r),S.arm(r);const h={},m=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[i,f]of Object.entries(r.headers))f===void 0||i.startsWith(":")||HOP_BY_HOP.has(i)||m&&(i==="content-length"||i==="transfer-encoding")||(h[i]=f);let p=http,E;if(typeof e=="object")E={socketPath:e.socketPath,path:r.url,method:r.method,headers:h,agent:T};else{const i=new URL(e),f=i.protocol==="https:";p=f?require("node:https"):http,h.host=i.host,E={protocol:i.protocol,hostname:i.hostname,port:i.port||(f?443:80),path:r.url,method:r.method,headers:h,agent:L(e)}}const v=o;v.__lensmcpErrContained||(v.__lensmcpErrContained=!0,o.on("error",()=>{}));let d;const n=s(()=>{o.writableEnded||u.destroy()},"onReqGone"),a=s(()=>{d&&!d.destroyed&&d.destroy()},"onEdgeClose"),l=s(()=>{r.removeListener("error",n),r.removeListener("aborted",n),r.removeListener("close",n),o.removeListener("close",a)},"detach"),u=p.request(E,i=>{if(d=i,g.onResponse(),o.headersSent||o.writableEnded||o.destroyed){i.destroy();return}const f={};for(const[P,y]of Object.entries(i.headers))y===void 0||HOP_BY_HOP.has(P)||(f[P]=y);delete f["x-powered-by"],f.server="LensMCP";const R=r.__lensmcpPod;R&&(f["x-lensmcp-pod"]=R);try{o.writeHead(i.statusCode??502,f)}catch(P){console.error("[gateway] h2 relay: bad response header, resetting stream:",P.message),i.destroy(),o.destroyed||o.destroy(P);return}i.pipe(o),S.armResponse(o);const k=s(()=>{!o.writableEnded&&!o.destroyed&&o.destroy()},"abortEdge");i.once("error",k),i.once("close",k),o.on("close",a)});u.once("error",i=>g.onError(i)),u.once("close",l),r.on("error",n),r.on("aborted",n),r.on("close",n),m?u.end():r.pipe(u)},"pipeUpstream"),W=s(async(e,r,o)=>{const g=r.__lensmcpTrace;if(e.pool){const d=e.svc;d&&(d.lastUsed=Date.now(),d.inflight+=1,o.on("close",()=>{d.inflight-=1,d.lastUsed=Date.now()}));let n=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs);if(!n&&d&&(O(g,"cold-start",{project:e.project}),await $.ensureUp(d)&&(n=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs))),d&&n&&$.markUp(d,"observed"),d&&$.scaleUp(d),!n){const y=d?.lastErrors?.length?` Service errors: ${d.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${y} The watcher recovers and the next request retries automatically.`);return}const a=path.basename(n,".sock");r.__lensmcpPod=a,O(g,"pod-select",{pod:a,pods:e.pool.socks.length,strategy:"round-robin"});let l=!1;const u=s((y,b="unlink")=>{if(e.pool.socks=e.pool.socks.filter(_=>_!==n),e.pool.idx=-1,b==="unlink")try{fs.unlinkSync(n)}catch{}console.error(`[gateway] pod ${a} of ${e.project}: ${y}`)},"evictPod"),i=armFirstByteGuard({socketPath:n,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(()=>!l&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:s(y=>{l=!0,u(y),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${a} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onCeiling:s(y=>{l=!0,u(y,"keep"),(0,edge_1.sendError)(o,r,504,"unavailable",`Pod ${a} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs.`)},"onCeiling"),onSlow:s((y,b)=>{console.log(`[gateway] pod ${a} of ${e.project}: slow first byte, pod alive (${y}ms, ${b.inflight} in-flight, ${b.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});o.on("close",()=>i.clear());const f=n,R=isReplayableRequest(r);let k=0;const P=s(()=>{k+=1,H({socketPath:f},r,o,{onResponse:s(()=>{i.clear(),l=!0},"onResponse"),onError:s(y=>{if(!l){if(edgeGone(r,o)){i.clear(),l=!0;return}(0,pod_probe_1.podConnectionVerdict)(f,y).then(b=>{if(!l){if(b==="alive"&&k<2&&R&&!edgeGone(r,o)){console.log(`[gateway] pod ${a} of ${e.project}: ${y.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),P();return}if(i.clear(),l=!0,b==="gone"){u(y.message),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${a} of ${e.project} unavailable \u2014 retry.`);return}console.error(`[gateway] pod ${a} of ${e.project}: ${y.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 ${a} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");P();return}const h=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let m=0,p=!1;const E=isReplayableRequest(r),v=s(()=>{if(edgeGone(r,o))return;const d=e.target;H(d,r,o,{onResponse:s(()=>{},"onResponse"),onError:s(n=>{if(isConnError(n)&&(m+=1),e.lens&&isConnError(n)&&E&&m>=2&&!p&&!edgeGone(r,o)){p=!0,D(e).then(a=>{if(!edgeGone(r,o)){if(a==="ready"){v();return}if(a==="unsupported"&&Date.now()<h){setTimeout(v,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}A(e,r,o,e.target,n,a)}});return}if(isConnError(n)&&E&&Date.now()<h&&!edgeGone(r,o)){setTimeout(v,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(o.headersSent||o.writableEnded||o.destroyed){o.destroyed||o.destroy();return}A(e,r,o,d,n)},"onError")})},"tryTcp");v()},"forwardNative");return{proxy:C,forward:s(async(e,r,o)=>{const g=Date.now(),h=r.__lensmcpTrace;if(o.on("close",()=>{q(e,Date.now()-g,o.statusCode??0,r.__lensmcpCaller),w(h,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),S.arm(r),(r.httpVersionMajor??1)>=2){await W(e,r,o);return}if(e.pool){const n=e.svc;n&&(n.lastUsed=Date.now(),n.inflight+=1,o.on("close",()=>{n.inflight-=1,n.lastUsed=Date.now()}));let a=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs);if(!a&&n&&(O(h,"cold-start",{project:e.project}),await $.ensureUp(n)&&(a=(0,discovery_1.pickSock)(e.pool,t.scanTtlMs))),n&&a&&$.markUp(n,"observed"),n&&$.scaleUp(n),!a){const _=n?.lastErrors?.length?` Service errors: ${n.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(o,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 l=path.basename(a,".sock");r.__lensmcpPod=l,O(h,"pod-select",{pod:l,pods:e.pool.socks.length,strategy:"round-robin"});let u=!1;const i=s((_,M="unlink")=>{if(e.pool.socks=e.pool.socks.filter(B=>B!==a),e.pool.idx=-1,M==="unlink")try{fs.unlinkSync(a)}catch{}console.error(`[gateway] pod ${l} of ${e.project}: ${_}`)},"evictPod"),f=armFirstByteGuard({socketPath:a,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(()=>!u&&!o.headersSent&&!o.writableEnded,"pending"),onWedged:s(_=>{u=!0,i(_),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${l} of ${e.project} was unresponsive \u2014 evicted, retry.`)},"onWedged"),onCeiling:s(_=>{u=!0,i(_,"keep"),(0,edge_1.sendError)(o,r,504,"unavailable",`Pod ${l} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs.`)},"onCeiling"),onSlow:s((_,M)=>{console.log(`[gateway] pod ${l} of ${e.project}: slow first byte, pod alive (${_}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",()=>f.clear());const R=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),k=a,P=isReplayableRequest(r);let y=0;const b=s(()=>{y+=1;const _=Date.now();C.web(r,o,{target:{socketPath:k},agent:T,proxyTimeout:R},M=>{if(!u){if(edgeGone(r,o)){f.clear(),u=!0;return}(0,pod_probe_1.podConnectionVerdict)(k,M).then(B=>{if(u)return;const F=Date.now()-_>=R-250;if(B==="alive"&&y<2&&P&&!F&&!edgeGone(r,o)){console.log(`[gateway] pod ${l} of ${e.project}: ${M.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),b();return}if(f.clear(),u=!0,B==="gone"){i(M.message),(0,edge_1.sendError)(o,r,502,"unavailable",`Pod ${l} of ${e.project} unavailable \u2014 retry.`);return}console.error(`[gateway] pod ${l} 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 ${l} of ${e.project} dropped the connection \u2014 retry.`)})}})},"sendToPod");b();return}const m=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let p=0,E=!1;const v=isReplayableRequest(r),d=s(()=>{if(edgeGone(r,o))return;const n=e.target,a={target:n,autoRewrite:typeof n=="string",changeOrigin:!0,agent:L(n)};C.web(r,o,a,l=>{if(isConnError(l)&&(p+=1),e.lens&&isConnError(l)&&v&&p>=2&&!E&&!edgeGone(r,o)){E=!0,D(e).then(u=>{if(!edgeGone(r,o)){if(u==="ready"){d();return}if(u==="unsupported"&&Date.now()<m){setTimeout(d,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}A(e,r,o,e.target,l,u)}});return}if(isConnError(l)&&v&&Date.now()<m&&!edgeGone(r,o)){setTimeout(d,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}o.destroyed||A(e,r,o,n,l)})},"tryTcp");d()},"forward"),agentForTarget:L,destroyAgents:s(()=>{S.stop(),T.destroy(),I.destroy()},"destroyAgents"),closeProxy:s(()=>{try{C.close?.()}catch{}},"closeProxy")}}j(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
@@ -44,7 +44,7 @@ export interface SourceSetConfig {
|
|
|
44
44
|
* `*.stories.*`, anything under `__tests__/`/`__mocks__/`) never count as a source-set change. No
|
|
45
45
|
* running app can import them, so their arrival cannot make a dev server stale — yet every spec an
|
|
46
46
|
* agent session wrote rolled BOTH lens vites and scaled six pods to zero (foodguard 2026-09-06:
|
|
47
|
-
* `shared/contracts/src/haccp/__tests__/zz-r3probe.spec.ts`; issues/dev-gateway
|
|
47
|
+
* `shared/contracts/src/haccp/__tests__/zz-r3probe.spec.ts`; issues/closed/dev-gateway-01). `false` restores
|
|
48
48
|
* the old count-everything behaviour.
|
|
49
49
|
*/
|
|
50
50
|
ignoreTests: boolean;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { NxJsonConfiguration, ProjectGraph, Task, TaskGraph } from '@nx/devkit';
|
|
2
|
+
import type { ProjectGraphStores } from './project-graph-store';
|
|
3
|
+
/** The only executor the runner starts — LensMCP's own serve. Every other one goes through `nx run`. */
|
|
4
|
+
export declare const SERVE_RUNNER_EXECUTOR = "@lensmcp/cluster:serve";
|
|
5
|
+
/** The role in the runner's process name: `<workspace>-<service>-serve` (process-name.ts). */
|
|
6
|
+
export declare const SERVE_RUNNER_ROLE = "serve";
|
|
7
|
+
/** The argument that names the runner's workspace on its command line — how the stale-runner reaper
|
|
8
|
+
* tells one checkout's runner from another's ({@link isServeRunnerCommandOf}). Always the LAST argument. */
|
|
9
|
+
export declare const RUNNER_ROOT_FLAG = "--workspace-root=";
|
|
10
|
+
/** `LENSMCP_SERVE_RUNNER=0` (or `off`/`false`/`no`/`nx`) starts every service through `nx run`. */
|
|
11
|
+
export declare function serveRunnerEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
12
|
+
/** Where the runner entry is, and what node needs to run it. */
|
|
13
|
+
export interface RunnerScript {
|
|
14
|
+
path: string;
|
|
15
|
+
/** `--require @swc-node/register` when running from source (the same arrangement serve.impl makes for main.devserver). */
|
|
16
|
+
execArgv: string[];
|
|
17
|
+
}
|
|
18
|
+
/** The built `serve-runner.js` at the package root, or the `.ts` beside this source tree; `undefined` when neither exists. */
|
|
19
|
+
export declare function serveRunnerScript(baseDir?: string): RunnerScript | undefined;
|
|
20
|
+
/** The runner's arguments after the script: nothing reads them but `ps` — they make it readable and
|
|
21
|
+
* identifiable (`run <project>:<target>` is what the stale-runner reaper matches, the root comes last). */
|
|
22
|
+
export declare function serveRunnerArgs(target: string, childCount: number, root: string): string[];
|
|
23
|
+
/** Is this command line a serve runner's? */
|
|
24
|
+
export declare function isServeRunnerCommand(cmd: string): boolean;
|
|
25
|
+
/** Is this command line a serve runner started for the workspace at `root`? Exact: the root is the last
|
|
26
|
+
* argument, so `<root>-gate1` or `<root>/.claude/worktrees/x` never reads as `<root>`. */
|
|
27
|
+
export declare function isServeRunnerCommandOf(cmd: string, root: string): boolean;
|
|
28
|
+
/** The IPC message nx's client sends run-executor — the runner takes the same, plus which `run()` to call. */
|
|
29
|
+
export interface RunnerMessage {
|
|
30
|
+
/** Absolute path of the workspace nx's `src/command-line/run/run.js`. */
|
|
31
|
+
nxRunModule: string;
|
|
32
|
+
targetDescription: Task['target'];
|
|
33
|
+
overrides: Task['overrides'];
|
|
34
|
+
isVerbose: boolean;
|
|
35
|
+
taskGraph: TaskGraph;
|
|
36
|
+
}
|
|
37
|
+
export interface RunnerLaunch {
|
|
38
|
+
kind: 'runner';
|
|
39
|
+
taskId: string;
|
|
40
|
+
env: NodeJS.ProcessEnv;
|
|
41
|
+
message: RunnerMessage;
|
|
42
|
+
script: RunnerScript;
|
|
43
|
+
}
|
|
44
|
+
export interface NxLaunch {
|
|
45
|
+
kind: 'nx';
|
|
46
|
+
/** Why the runner was not used — logged with the spawn. */
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
export type ServeLaunchPlan = RunnerLaunch | NxLaunch;
|
|
50
|
+
export interface ServeLaunchRequest {
|
|
51
|
+
/** The gateway's project graphs, one per workspace root it serves: its own, and each registered workspace's
|
|
52
|
+
* (forwarded at registration, guest-graph.ts). Absent ⇒ the gateway was started without a graph. */
|
|
53
|
+
stores?: ProjectGraphStores;
|
|
54
|
+
root: string;
|
|
55
|
+
project: string;
|
|
56
|
+
target: string;
|
|
57
|
+
childCount: number;
|
|
58
|
+
/** The environment the gateway would hand `nx run` — what nx itself starts from. */
|
|
59
|
+
baseEnv: NodeJS.ProcessEnv;
|
|
60
|
+
/** The gateway's own environment, for the `LENSMCP_SERVE_RUNNER` switch. Default `process.env`. */
|
|
61
|
+
gatewayEnv?: NodeJS.ProcessEnv;
|
|
62
|
+
/** Default {@link serveRunnerScript}. */
|
|
63
|
+
script?: RunnerScript;
|
|
64
|
+
}
|
|
65
|
+
/** The nx functions the plan uses, loaded from the WORKSPACE's nx — the one its `nx run` would run. */
|
|
66
|
+
export interface NxHelpers {
|
|
67
|
+
version: string;
|
|
68
|
+
runModule: string;
|
|
69
|
+
createTaskGraph(projectGraph: ProjectGraph, extraTargetDependencies: Record<string, never>, projectNames: string[], targets: string[], configuration: string | undefined, overrides: Task['overrides']): TaskGraph;
|
|
70
|
+
getEnvFilesForTask(task: Task, graph: ProjectGraph): string[];
|
|
71
|
+
loadAndExpandDotEnvFile(files: string | string[], env: NodeJS.ProcessEnv, override?: boolean): unknown;
|
|
72
|
+
unloadDotEnvFile(file: string, env: NodeJS.ProcessEnv): void;
|
|
73
|
+
readNxJson(root: string): NxJsonConfiguration;
|
|
74
|
+
workspaceDataDirectoryForWorkspace(root: string): string;
|
|
75
|
+
}
|
|
76
|
+
/** The workspace nx's helpers, or why they cannot be loaded. Cached per root (a module load, not a computation). */
|
|
77
|
+
export declare function loadNxHelpers(root: string): NxHelpers | string;
|
|
78
|
+
/**
|
|
79
|
+
* The environment nx 22 hands the executor of `nx run <project>:<target>` started with `base`, step by
|
|
80
|
+
* step as nx takes it (one task, output piped, no TTY — how the gateway runs it):
|
|
81
|
+
*
|
|
82
|
+
* 1. `bin/nx.js`: `FORCE_COLOR=0` becomes `NO_COLOR=1`; the root `.local.env`, `.env.local`, `.env`
|
|
83
|
+
* are loaded and expanded — a variable already set wins, the first file wins.
|
|
84
|
+
* 2. The CLI marks itself (`NX_CLI_SET`), settles `NX_VERBOSE_LOGGING`, turns the TUI off and, unless
|
|
85
|
+
* `NX_LOAD_DOT_ENV_FILES=false`, sets it to `true`.
|
|
86
|
+
* 3. The task's own env (`getTaskSpecificEnv`): every variable whose value is still the one a root
|
|
87
|
+
* dotenv file gave is removed — including one the gateway passed down with that same value — and the
|
|
88
|
+
* task's files are loaded in nx's order (`getEnvFilesForTask`: project before workspace,
|
|
89
|
+
* `.env.<target>.<configuration>` before `.env.<configuration>` before `.env.<target>` before
|
|
90
|
+
* `.env`, each `.local` first), first one wins, a variable still set wins.
|
|
91
|
+
* 4. nx's task variables on top (`getEnvVariablesForTask`): `FORCE_COLOR` (`true` unless set),
|
|
92
|
+
* `NX_WORKSPACE_ROOT`, `NX_TASK_TARGET_PROJECT/TARGET/CONFIGURATION`, `LERNA_PACKAGE_NAME`,
|
|
93
|
+
* `NX_TUI=false`, `NX_SKIP_NX_CACHE=true` when the CLI's env asked to skip the cache
|
|
94
|
+
* (`NX_SKIP_NX_CACHE` or `NX_DISABLE_NX_CACHE`, `command-line-utils.js`) and removed otherwise; the
|
|
95
|
+
* output-capture variables removed.
|
|
96
|
+
* 5. The fork adds `NX_FORKED_TASK_EXECUTOR=true`.
|
|
97
|
+
*
|
|
98
|
+
* `NX_TASK_HASH` is removed, not computed (see the module doc). The dotenv primitives are nx's own.
|
|
99
|
+
* `isVerbose` is what nx's `--verbose` middleware settled in step 2 — the flag run-executor is handed.
|
|
100
|
+
*/
|
|
101
|
+
export interface NxExecutorEnv {
|
|
102
|
+
env: NodeJS.ProcessEnv;
|
|
103
|
+
isVerbose: boolean;
|
|
104
|
+
}
|
|
105
|
+
export declare function nxExecutorEnv(nx: NxHelpers, root: string, base: NodeJS.ProcessEnv, task: Task, graph: ProjectGraph): NxExecutorEnv;
|
|
106
|
+
/**
|
|
107
|
+
* Runner or `nx run` for one spawn — and if `nx run`, why. The runner is used only when every one of these holds:
|
|
108
|
+
* - `LENSMCP_SERVE_RUNNER` is not off;
|
|
109
|
+
* - the gateway holds a project graph for THIS workspace — its own, or a registered one's that its
|
|
110
|
+
* registration forwarded or the gateway computed since (guest-graph.ts) — and no declaring file changed
|
|
111
|
+
* since it was read;
|
|
112
|
+
* - the target's executor is {@link SERVE_RUNNER_EXECUTOR}, and nothing about the target came from a
|
|
113
|
+
* plugin (its source maps name only `project.json`, `package.json` or `nx.json`);
|
|
114
|
+
* - nx's task graph for it is that one task — no `dependsOn` that would run first — and the task has
|
|
115
|
+
* no sync generator nx would run first;
|
|
116
|
+
* - nx keeps the graph cache the executor reads, and the workspace nx's helpers and the runner entry exist.
|
|
117
|
+
*/
|
|
118
|
+
export declare function planServeLaunch(req: ServeLaunchRequest): ServeLaunchPlan;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";var T=Object.defineProperty;var u=(e,r)=>T(e,"name",{value:r,configurable:!0});var h=Object.defineProperty,i=u((e,r)=>h(e,"name",{value:r,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.RUNNER_ROOT_FLAG=exports.SERVE_RUNNER_ROLE=exports.SERVE_RUNNER_EXECUTOR=void 0,exports.serveRunnerEnabled=serveRunnerEnabled,exports.serveRunnerScript=serveRunnerScript,exports.serveRunnerArgs=serveRunnerArgs,exports.isServeRunnerCommand=isServeRunnerCommand,exports.isServeRunnerCommandOf=isServeRunnerCommandOf,exports.loadNxHelpers=loadNxHelpers,exports.nxExecutorEnv=nxExecutorEnv,exports.planServeLaunch=planServeLaunch;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),node_module_1=require("node:module"),path=tslib_1.__importStar(require("node:path"));exports.SERVE_RUNNER_EXECUTOR="@lensmcp/cluster:serve",exports.SERVE_RUNNER_ROLE="serve",exports.RUNNER_ROOT_FLAG="--workspace-root=";function serveRunnerEnabled(e=process.env){const r=(e.LENSMCP_SERVE_RUNNER??"").trim().toLowerCase();return!["0","off","false","no","nx"].includes(r)}u(serveRunnerEnabled,"serveRunnerEnabled"),i(serveRunnerEnabled,"serveRunnerEnabled");function serveRunnerScript(e=path.join(__dirname,"../../..")){const r=path.join(e,"serve-runner.js");if(fs.existsSync(r))return{path:r,execArgv:[]};const s=path.join(e,"serve-runner.ts");if(fs.existsSync(s))try{return{path:s,execArgv:["--require",require.resolve("@swc-node/register")]}}catch{return}}u(serveRunnerScript,"serveRunnerScript"),i(serveRunnerScript,"serveRunnerScript");function serveRunnerArgs(e,r,s){return["run",e,`--childCount=${r}`,`${exports.RUNNER_ROOT_FLAG}${s}`]}u(serveRunnerArgs,"serveRunnerArgs"),i(serveRunnerArgs,"serveRunnerArgs");function isServeRunnerCommand(e){return/\bserve-runner\.(js|ts)\b/.test(e)&&e.includes(` ${exports.RUNNER_ROOT_FLAG}`)}u(isServeRunnerCommand,"isServeRunnerCommand"),i(isServeRunnerCommand,"isServeRunnerCommand");function isServeRunnerCommandOf(e,r){return isServeRunnerCommand(e)&&e.trimEnd().endsWith(` ${exports.RUNNER_ROOT_FLAG}${r.replace(/\/+$/,"")}`)}u(isServeRunnerCommandOf,"isServeRunnerCommandOf"),i(isServeRunnerCommandOf,"isServeRunnerCommandOf");const helpersByRoot=new Map;function loadNxHelpers(e){const r=helpersByRoot.get(e);if(r)return r;let s;try{const o=(0,node_module_1.createRequire)(path.join(e,"package.json")),_=o("nx/package.json").version,t=o("nx/src/tasks-runner/task-env"),n=o("nx/src/tasks-runner/create-task-graph"),a=o("nx/src/config/nx-json"),p=o("nx/src/utils/cache-directory"),c=o.resolve("nx/src/command-line/run/run"),d=[["getEnvFilesForTask",t.getEnvFilesForTask],["loadAndExpandDotEnvFile",t.loadAndExpandDotEnvFile],["unloadDotEnvFile",t.unloadDotEnvFile],["createTaskGraph",n.createTaskGraph],["readNxJson",a.readNxJson],["workspaceDataDirectoryForWorkspace",p.workspaceDataDirectoryForWorkspace]].filter(([,R])=>typeof R!="function").map(([R])=>R);s=d.length>0?`nx ${_} has no ${d.join(", ")}`:{version:_,runModule:c,createTaskGraph:n.createTaskGraph,getEnvFilesForTask:t.getEnvFilesForTask,loadAndExpandDotEnvFile:t.loadAndExpandDotEnvFile,unloadDotEnvFile:t.unloadDotEnvFile,readNxJson:a.readNxJson,workspaceDataDirectoryForWorkspace:p.workspaceDataDirectoryForWorkspace}}catch(o){s=`cannot load the workspace's nx (${o.message.split(`
|
|
2
|
+
`)[0]})`}return helpersByRoot.set(e,s),s}u(loadNxHelpers,"loadNxHelpers"),i(loadNxHelpers,"loadNxHelpers");const ROOT_DOTENV_LOAD=[".local.env",".env.local",".env"],ROOT_DOTENV_UNLOAD=[".env",".local.env",".env.local"];function nxExecutorEnv(e,r,s,o,_){const t={...s};t.FORCE_COLOR==="0"&&(t.NO_COLOR="1",delete t.FORCE_COLOR),e.loadAndExpandDotEnvFile(ROOT_DOTENV_LOAD.map(a=>path.join(r,a)),t),t.NX_CLI_SET="true",t.NX_VERBOSE_LOGGING=String(t.NX_VERBOSE_LOGGING==="true"),t.NX_TUI="false",t.NX_LOAD_DOT_ENV_FILES!=="false"&&(t.NX_LOAD_DOT_ENV_FILES="true");const n={...t};for(const a of ROOT_DOTENV_UNLOAD)e.unloadDotEnvFile(path.join(r,a),n);t.NX_LOAD_DOT_ENV_FILES==="true"&&e.loadAndExpandDotEnvFile(e.getEnvFilesForTask(o,_).map(a=>path.join(r,a)),n),n.FORCE_COLOR=t.FORCE_COLOR===void 0?"true":t.FORCE_COLOR,n.NX_WORKSPACE_ROOT=r,n.NX_TASK_TARGET_PROJECT=o.target.project,n.NX_TASK_TARGET_TARGET=o.target.target,o.target.configuration?n.NX_TASK_TARGET_CONFIGURATION=o.target.configuration:delete n.NX_TASK_TARGET_CONFIGURATION,n.LERNA_PACKAGE_NAME=o.target.project,n.NX_TUI="false",o.target.target==="test"&&(n.NX_TERMINAL_CAPTURE_STDERR="true");for(const a of["NX_TASK_HASH","NX_SKIP_NX_CACHE","NX_TERMINAL_OUTPUT_PATH","NX_STREAM_OUTPUT","NX_PREFIX_OUTPUT","NX_SET_CLI"])delete n[a];return(t.NX_SKIP_NX_CACHE==="true"||t.NX_DISABLE_NX_CACHE==="true")&&(n.NX_SKIP_NX_CACHE="true"),n.NX_FORKED_TASK_EXECUTOR="true",{env:n,isVerbose:t.NX_VERBOSE_LOGGING==="true"}}u(nxExecutorEnv,"nxExecutorEnv"),i(nxExecutorEnv,"nxExecutorEnv");function inferredBy(e,r,s){const o=e.sourceMaps?.[r],_=new Set([path.posix.join(r,"project.json"),path.posix.join(r,"package.json"),"nx.json"]),t=`targets.${s}`;let n=!1;for(const[a,[p,c]]of Object.entries(o??{}))if(!(a!==t&&!a.startsWith(`${t}.`))&&(n=!0,!p||!_.has(p)))return`${c}${p?` (from ${p})`:""}`;return n?void 0:"a source the project graph does not record"}u(inferredBy,"inferredBy"),i(inferredBy,"inferredBy");function planServeLaunch(e){const r=i(E=>({kind:"nx",reason:E}),"nx");if(!serveRunnerEnabled(e.gatewayEnv??process.env))return r("LENSMCP_SERVE_RUNNER is off");if(!e.stores)return r("the gateway was started without an Nx project graph");const s=e.stores.get(e.root);if(!s)return r("a workspace registered into this gateway that it holds no project graph for");const o=s.current();if(!o)return s.refreshIfStale(),r(`no project graph${s.lastError()?` (${s.lastError()})`:""}`);const _=s.staleReason();if(_)return s.refreshIfStale(),r(_);const t=o.projectGraph.nodes[e.project],n=t?.data.targets?.[e.target];if(!t||!n)return r(`${e.project}:${e.target} is not in the project graph`);if(n.executor!==exports.SERVE_RUNNER_EXECUTOR)return r(`its executor is ${n.executor??"(none)"}, not ${exports.SERVE_RUNNER_EXECUTOR}`);if(!o.sourceMaps)return r("this nx reports no source maps, so the gateway cannot tell a declared target from an inferred one");const a=inferredBy(o,t.data.root,e.target);if(a)return r(`part of the target is inferred by ${a}`);const p=e.gatewayEnv??process.env;if(e.baseEnv.NX_CACHE_PROJECT_GRAPH==="false"||p.NX_CACHE_PROJECT_GRAPH==="false")return r("NX_CACHE_PROJECT_GRAPH=false \u2014 the executor reads nx's graph cache");const c=loadNxHelpers(e.root);if(typeof c=="string")return r(c);const d=e.script??serveRunnerScript();if(!d)return r("serve-runner.js is missing from @lensmcp/cluster");if(!fs.existsSync(path.join(c.workspaceDataDirectoryForWorkspace(e.root),"project-graph.json")))return r("nx has no project-graph cache on disk for the executor to read");const R={childCount:e.childCount,__overrides_unparsed__:[`--childCount=${e.childCount}`]};let N;try{N=c.createTaskGraph(o.projectGraph,{},[e.project],[e.target],void 0,R)}catch(E){return r(`nx could not build its task graph (${E.message.split(`
|
|
3
|
+
`)[0]})`)}const l=Object.values(N.tasks).find(E=>E.target.project===e.project&&E.target.target===e.target);if(!l)return r(`nx's task graph has no ${e.project}:${e.target}`);const f=Object.keys(N.tasks).filter(E=>E!==l.id);if(f.length>0)return r(`dependsOn: nx would run ${f.join(", ")} first`);const v=new Set(c.readNxJson(e.root).sync?.disabledTaskSyncGenerators??[]),O=(n.syncGenerators??[]).filter(E=>!v.has(E));if(O.length>0)return r(`sync generators nx would run first: ${O.join(", ")}`);l.startTime=Date.now();const{env:x,isVerbose:g}=nxExecutorEnv(c,e.root,e.baseEnv,l,o.projectGraph);return{kind:"runner",taskId:l.id,env:x,script:d,message:{nxRunModule:c.runModule,targetDescription:l.target,overrides:l.overrides,isVerbose:g,taskGraph:N}}}u(planServeLaunch,"planServeLaunch"),i(planServeLaunch,"planServeLaunch");
|
|
@@ -54,7 +54,7 @@ export declare function createClientErrorLogGate(windowMs?: number): (cause: str
|
|
|
54
54
|
* The map is a per-workspace slot, and `onRegister` used to `set()` straight over it: a second
|
|
55
55
|
* `gateway start` for an already-hosted workspace left the first registration's dashboard, vites and
|
|
56
56
|
* capture children running with no handle able to name them, holding their ports for the life of the
|
|
57
|
-
* daemon (issues/codex-security
|
|
57
|
+
* daemon (issues/closed/codex-security-50). Both the register and the unregister path go through here now, so
|
|
58
58
|
* "replace a registration" and "remove a registration" retire exactly the same set.
|
|
59
59
|
*
|
|
60
60
|
* The entry is deleted BEFORE the reaper runs, so a reaper that throws part-way cannot leave a dead
|
|
@@ -66,7 +66,7 @@ export declare function retirePreviousWorkspace(reapers: Map<string, () => void>
|
|
|
66
66
|
/**
|
|
67
67
|
* `maxSessionMemory: 512` / `maxConcurrentStreams: 512` (below, in the h2 server options) are DELIBERATE
|
|
68
68
|
* and STAY — a full Vite reload streams tens of MB of transformed ESM over ONE h2 session and Node's 10 MB
|
|
69
|
-
* default turns that into a burst of `ERR_HTTP2_PROTOCOL_ERROR` (issues/foodguard
|
|
69
|
+
* default turns that into a burst of `ERR_HTTP2_PROTOCOL_ERROR` (issues/closed/foodguard-14, commit `cc6aca4`).
|
|
70
70
|
* What was missing is everything AROUND them. `maxSessionMemory` is a PER-SESSION accounting threshold, not
|
|
71
71
|
* a process budget, and nothing bounded the aggregate: no cap on concurrent h2 sessions, no cap on streams
|
|
72
72
|
* across sessions, and `GATEWAY_MAX_CONNECTIONS` defaults to `0` = unlimited, so the listener accepted
|
|
@@ -91,7 +91,7 @@ export declare const GATEWAY_MAX_CONNECTIONS_DEFAULT = 512;
|
|
|
91
91
|
export declare const GATEWAY_MAX_H2_SESSIONS_DEFAULT = 64;
|
|
92
92
|
/** Open h2 streams across ALL sessions. A full Vite reload is a few hundred; 4096 is ~8 saturated sessions
|
|
93
93
|
* and is the only thing that bounds `sessions × maxConcurrentStreams` (= 32,768) proxied requests, each of
|
|
94
|
-
* which also holds upstream state (issues/codex-security
|
|
94
|
+
* which also holds upstream state (issues/closed/codex-security-10). */
|
|
95
95
|
export declare const GATEWAY_MAX_H2_STREAMS_DEFAULT = 4096;
|
|
96
96
|
/** How long a session with ZERO open streams may sit before it is GOAWAY'd. Deliberately keyed on "no open
|
|
97
97
|
* streams", never on socket silence: a silent-but-open stream is an SSE/MCP subscription or an idle HMR
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var ue=Object.defineProperty;var m=(e,r)=>ue(e,"name",{value:r,configurable:!0});var le=Object.defineProperty,n=m((e,r)=>le(e,"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_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"),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(e,r){return new Promise((u,S)=>{const a=(e.startsWith("https:")?require("node:https"):require("node:http")).request(e,{method:"GET",headers:r?.headers,rejectUnauthorized:!1},g=>{const d=[];g.on("data",p=>d.push(p)),g.on("end",()=>{const p=g.statusCode??0,l=Buffer.concat(d).toString("utf8");u({ok:p>=200&&p<300,status:p,json:n(async()=>JSON.parse(l),"json")})})});a.on("error",S),a.end()})}m(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(e){const r=process.env.LENSMCP_GW_JWKS_URL,u=process.env.LENSMCP_GW_JWT_ISS,S=new Map;let a;const g=n((d,p)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${d}${p}`);const l=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:d,...u?{issuer:u}:{},fetchImpl:insecureLocalFetch});return l.refresh().catch(s=>console.warn("[gateway] edge JWKS warm failed (will retry):",s.message)),l},"make");return{for(d){if(r)return a??=g(r,"");const p=d.split(":")[0].toLowerCase();let l;for(const i of e()){const h=i.host;if(typeof h!="string"||!/^auth\./.test(h)||h.startsWith("internal."))continue;const C=h.slice(5);(p===C||p.endsWith(`.${C}`))&&(!l||C.length>l.length)&&(l=C)}if(!l)return;let s=S.get(l);return s||(s=g(`https://auth.${l}/jwks.json`,` (auto-derived for apex ${l})`),S.set(l,s)),s},stop(){a?.stop();for(const d of S.values())d.stop()}}}m(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(e,r){return e&&exports.BENIGN_SOCKET_ERRNOS.has(e)?{status:null,report:!1,cause:"client-abort"}:e==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:e==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:e==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:e?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:e??r??"unknown"}}m(classifyClientError,"classifyClientError"),n(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(e=1e4){const r=new Map;return u=>{const S=Date.now(),a=r.get(u)??0;return S-a<e?!1:(r.set(u,S),!0)}}m(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(e){e.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`),e.destroy()})}m(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(e,r,u){const S=e.get(r);if(!S)return!1;e.delete(r),u==="re-registered"&&console.log(`[gateway] workspace '${r}' re-registered \u2014 retiring the children of its previous registration first.`);try{S()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${r}' (${u}): ${a.message}`)}return!0}m(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(e,r){if(e===void 0||e.trim()==="")return r;const u=Number(e);return!Number.isFinite(u)||u<0?r:Math.floor(u)}m(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(e){return parseBudget(e,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}m(resolveMaxConnections,"resolveMaxConnections"),n(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(e=process.env){return{maxSessions:parseBudget(e.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(e.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(e.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}m(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(e,r){const{NGHTTP2_ENHANCE_YOUR_CALM:u,NGHTTP2_REFUSED_STREAM:S}=require("node:http2").constants,a=new Map;let g=0;const d=n(s=>{s.idle&&(clearTimeout(s.idle),s.idle=void 0)},"disarm"),p=n((s,i)=>{d(i),!(e.idleSessionMs<=0||i.open>0)&&(i.idle=setTimeout(()=>{s.destroyed||s.closed||(r("session-reclaimed",{idleMs:e.idleSessionMs,sessions:a.size}),a.delete(s),s.close())},e.idleSessionMs),i.idle.unref?.())},"arm"),l=n(()=>{for(const[s,i]of a)if(!(i.open>0||s.destroyed))return d(i),a.delete(s),r("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:e.maxSessions}),s.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(s){if(e.maxSessions>0&&a.size>=e.maxSessions&&!l()){r("session-refused",{sessions:a.size,maxSessions:e.maxSessions,streams:g});try{s.goaway(u)}catch{}try{s.close()}catch{}return!1}const i={open:0};return a.set(s,i),p(s,i),s.once("close",()=>{d(i),a.delete(s)}),!0},admitStream(s,i){const h=a.get(s);if(!h)return!0;if(e.maxStreams>0&&g>=e.maxStreams){r("stream-refused",{streams:g,maxStreams:e.maxStreams,sessions:a.size});try{i.close(S)}catch{}return!1}return g+=1,h.open+=1,d(h),i.once("close",()=>{g=Math.max(0,g-1),h.open=Math.max(0,h.open-1),h.open===0&&a.has(s)&&p(s,h)}),!0},dispose(){for(const s of a.values())d(s);a.clear()}}}m(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(e,r){const u=e.https!==!1,S=e.ports?.length?e.ports:[443],a=e.scanTtlMs??1500,g=e.startupTimeoutMs??12e4,d=e.sweepMs??1e4,p=e.trafficFlushMs??5e3;let l=!1;const s=process.env.LENSMCP_EVENT_FILE??path.join(r.root,".lensmcp","events.jsonl"),i=(0,scope_1.readLensScope)(r.root).key,h=r.projectsConfigurations?.projects??{},C=(0,discovery_1.projectSourceRoots)(r.root,h,r.projectDependencies),{routes:F,services:v}=(0,discovery_1.discoverRoutes)(r.root,h,void 0,i,C);if(F.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const b=new route_registry_1.RouteRegistry;b.register({wsKey:i,root:r.root,routes:F,services:v});const N=[];let U=n(()=>{},"refreshCert");const Y=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(N,b),U()},"rebuildRoutes");Y();const{serviceKeys:Q,keyToProject:Z,keyToWorkspace:ee}=(0,service_keys_1.loadServiceKeys)(r.root,v,i),te=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...e.trustProxyHops!==void 0?{trustProxyHops:e.trustProxyHops}:{},...e.clientIpHeader?{clientIpHeader:e.clientIpHeader}:{},...e.clientIpTrustedProxies?{clientIpTrustedProxies:e.clientIpTrustedProxies}:{}}),T={root:r.root,wsKey:i,eventFile:s,routes:N,registry:b,rebuildRoutes:Y,services:v,serviceKeys:Q,keyToProject:Z,keyToWorkspace:ee,clientIp:te,bodyStallMs:e.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:g,sweepMs:d,trafficFlushMs:p,https:u,projectRoots:Object.fromEntries(Object.entries(r.projectsConfigurations?.projects??{}).map(([t,o])=>[t,path.resolve(r.root,o.root)])),projectSourceRoots:C,stopped:n(()=>l,"stopped")},f=(0,observability_1.createObservability)(s,{trafficFlushMs:p}),re=createClientErrorLogGate(),x=h2BudgetFromEnv(),se=createClientErrorLogGate(3e4),$=createH2Governor(x,(t,o)=>{se(t)&&console.warn(`[gateway] h2 aggregate budget: ${t} (${JSON.stringify(o)}). 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(t==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${t}`,`gateway-h2-budget:${t}`,{kind:"h2-aggregate-budget",event:t,project:"gateway",...o})}),P=(0,lifecycle_1.createServiceLayer)(T,f),O=(0,lens_children_1.createLensChildren)(T,f,e),H=new Map;let B;try{B=(0,control_1.startControlServer)({rt:T,killService:n((t,o)=>P.killSpawned(t,o),"killService"),onRegister:n(async t=>{retirePreviousWorkspace(H,t.wsKey,"re-registered");const o=await O.hostWorkspaceDashboard(t),w=await O.hostWorkspaceApps(t);H.set(t.wsKey,()=>{try{o?.()}finally{w()}})},"onRegister"),onUnregister:n(t=>{retirePreviousWorkspace(H,t.wsKey,"unregistered")},"onUnregister")})}catch(t){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",t.message)}const L=(0,proxy_1.createProxy)(T,f,P),X=buildEdgeVerifierPool(()=>T.routes),K=(0,auth_1.createEdgeAuth)(T,f,X);let G;if(e.middleware){const t=require(path.resolve(r.root,e.middleware));G=t.default??t}const oe=n(t=>{if(G)try{G(t.req)}catch(o){f.traceStep(t.trace,"auth",{mode:"middleware",ok:!1,reason:o.message}),t.route&&f.finishTrace(t.trace,t.route.project,401),t.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${o.message}`)}},"builtinMiddleware"),J=e.hooks??{},V=(0,hooks_1.createHooks)({...J,afterAuth:[oe,...J.afterAuth??[]]}),z=(0,handler_1.createHandler)({rt:T,obs:f,auth:K,proxy:L,hooks:V}),ne=(0,upgrade_1.createUpgrade)({rt:T,auth:K,proxy:L,hooks:V}),ae=f.startEdgeFlusher(),ie=P.startSweeper();await O.bootSingletonsAndApps();for(const t of v)t.decl.eager&&P.ensureUp(t);let R,W,k;if(u){const t=require("../../../basic-ssl"),o=(0,service_keys_1.gatewayCacheDir)(r.root);k=n(()=>t.getCertificateSync(o,"gateway",T.routes.map(w=>w.host).filter(w=>!!w)),"mintCert"),R=k(),W=t.caCertPath()}const j=[],I=[],q=e.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const t of S){const o=R?require("node:http2").createSecureServer({key:R,cert:R,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:e.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:e.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(y,_)=>z(y,_)):http.createServer(z);o.on("upgrade",ne),o.on("error",y=>console.error(`[gateway] port ${t}: ${y.code}`)),o.on("connection",guardInboundSocket),o.on("secureConnection",guardInboundSocket),o.on("tlsClientError",(y,_)=>_.destroy()),o.on("clientError",(y,_)=>{const c=y,E=classifyClientError(c.code,c.message);if(E.report&&re(E.cause)){const M=_.remoteAddress??"unknown",A=E.status===null?"dropped":String(E.status);console.warn(`[gateway] inbound H1 clientError from ${M}: ${E.cause} (${c.code??c.message}${c.bytesParsed!==void 0?`, bytesParsed=${c.bytesParsed}`:""}) \u2192 ${A}. This status is the GATEWAY's, not the upstream service's.`),f.emit("warning",`inbound clientError: ${E.cause} \u2192 ${A}`,`gateway-client-error:${E.cause}`,{kind:"gateway-client-error",cause:E.cause,code:c.code??null,status:E.status,peer:M,bytesParsed:c.bytesParsed??null})}E.status!==null&&_.writable&&_.end(`HTTP/1.1 ${E.status} ${CLIENT_ERROR_REASON[E.status]}\r
|
|
1
|
+
"use strict";var ue=Object.defineProperty;var T=(e,t)=>ue(e,"name",{value:t,configurable:!0});var le=Object.defineProperty,n=T((e,t)=>le(e,"name",{value:t,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_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(e,t){return new Promise((u,E)=>{const a=(e.startsWith("https:")?require("node:https"):require("node:http")).request(e,{method:"GET",headers:t?.headers,rejectUnauthorized:!1},g=>{const d=[];g.on("data",p=>d.push(p)),g.on("end",()=>{const p=g.statusCode??0,l=Buffer.concat(d).toString("utf8");u({ok:p>=200&&p<300,status:p,json:n(async()=>JSON.parse(l),"json")})})});a.on("error",E),a.end()})}T(insecureLocalFetch,"insecureLocalFetch"),n(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(e){const t=process.env.LENSMCP_GW_JWKS_URL,u=process.env.LENSMCP_GW_JWT_ISS,E=new Map;let a;const g=n((d,p)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${d}${p}`);const l=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:d,...u?{issuer:u}:{},fetchImpl:insecureLocalFetch});return l.refresh().catch(s=>console.warn("[gateway] edge JWKS warm failed (will retry):",s.message)),l},"make");return{for(d){if(t)return a??=g(t,"");const p=d.split(":")[0].toLowerCase();let l;for(const i of e()){const h=i.host;if(typeof h!="string"||!/^auth\./.test(h)||h.startsWith("internal."))continue;const C=h.slice(5);(p===C||p.endsWith(`.${C}`))&&(!l||C.length>l.length)&&(l=C)}if(!l)return;let s=E.get(l);return s||(s=g(`https://auth.${l}/jwks.json`,` (auto-derived for apex ${l})`),E.set(l,s)),s},stop(){a?.stop();for(const d of E.values())d.stop()}}}T(buildEdgeVerifierPool,"buildEdgeVerifierPool"),n(buildEdgeVerifierPool,"buildEdgeVerifierPool"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(e,t){return e&&exports.BENIGN_SOCKET_ERRNOS.has(e)?{status:null,report:!1,cause:"client-abort"}:e==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:e==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:e==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:e?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:e??t??"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(e=1e4){const t=new Map;return u=>{const E=Date.now(),a=t.get(u)??0;return E-a<e?!1:(t.set(u,E),!0)}}T(createClientErrorLogGate,"createClientErrorLogGate"),n(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(e){e.on("error",t=>{exports.BENIGN_SOCKET_ERRNOS.has(t.code??"")||console.warn(`[gateway] inbound socket error (${t.code??t.message}) \u2014 dropping this connection only`),e.destroy()})}T(guardInboundSocket,"guardInboundSocket"),n(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(e,t,u){const E=e.get(t);if(!E)return!1;e.delete(t),u==="re-registered"&&console.log(`[gateway] workspace '${t}' re-registered \u2014 retiring the children of its previous registration first.`);try{E()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${t}' (${u}): ${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(e,t){if(e===void 0||e.trim()==="")return t;const u=Number(e);return!Number.isFinite(u)||u<0?t:Math.floor(u)}T(parseBudget,"parseBudget"),n(parseBudget,"parseBudget");function resolveMaxConnections(e){return parseBudget(e,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}T(resolveMaxConnections,"resolveMaxConnections"),n(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(e=process.env){return{maxSessions:parseBudget(e.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(e.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(e.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}T(h2BudgetFromEnv,"h2BudgetFromEnv"),n(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(e,t){const{NGHTTP2_ENHANCE_YOUR_CALM:u,NGHTTP2_REFUSED_STREAM:E}=require("node:http2").constants,a=new Map;let g=0;const d=n(s=>{s.idle&&(clearTimeout(s.idle),s.idle=void 0)},"disarm"),p=n((s,i)=>{d(i),!(e.idleSessionMs<=0||i.open>0)&&(i.idle=setTimeout(()=>{s.destroyed||s.closed||(t("session-reclaimed",{idleMs:e.idleSessionMs,sessions:a.size}),a.delete(s),s.close())},e.idleSessionMs),i.idle.unref?.())},"arm"),l=n(()=>{for(const[s,i]of a)if(!(i.open>0||s.destroyed))return d(i),a.delete(s),t("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:e.maxSessions}),s.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(s){if(e.maxSessions>0&&a.size>=e.maxSessions&&!l()){t("session-refused",{sessions:a.size,maxSessions:e.maxSessions,streams:g});try{s.goaway(u)}catch{}try{s.close()}catch{}return!1}const i={open:0};return a.set(s,i),p(s,i),s.once("close",()=>{d(i),a.delete(s)}),!0},admitStream(s,i){const h=a.get(s);if(!h)return!0;if(e.maxStreams>0&&g>=e.maxStreams){t("stream-refused",{streams:g,maxStreams:e.maxStreams,sessions:a.size});try{i.close(E)}catch{}return!1}return g+=1,h.open+=1,d(h),i.once("close",()=>{g=Math.max(0,g-1),h.open=Math.max(0,h.open-1),h.open===0&&a.has(s)&&p(s,h)}),!0},dispose(){for(const s of a.values())d(s);a.clear()}}}T(createH2Governor,"createH2Governor"),n(createH2Governor,"createH2Governor");async function startGateway(e,t){const u=e.https!==!1,E=e.ports?.length?e.ports:[443],a=e.scanTtlMs??1500,g=e.startupTimeoutMs??12e4,d=e.sweepMs??1e4,p=e.trafficFlushMs??5e3;let l=!1;const s=process.env.LENSMCP_EVENT_FILE??path.join(t.root,".lensmcp","events.jsonl"),i=(0,scope_1.readLensScope)(t.root).key,h=t.projectsConfigurations?.projects??{},C=(0,discovery_1.projectSourceRoots)(t.root,h,t.projectDependencies),{routes:I,services:v}=(0,discovery_1.discoverRoutes)(t.root,h,void 0,i,C);if(I.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const H=new route_registry_1.RouteRegistry;H.register({wsKey:i,root:t.root,routes:I,services:v});const R=[];let U=n(()=>{},"refreshCert");const B=n(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,H),U()},"rebuildRoutes");B();const{serviceKeys:Q,keyToProject:Z,keyToWorkspace:ee}=(0,service_keys_1.loadServiceKeys)(t.root,v,i),re=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...e.trustProxyHops!==void 0?{trustProxyHops:e.trustProxyHops}:{},...e.clientIpHeader?{clientIpHeader:e.clientIpHeader}:{},...e.clientIpTrustedProxies?{clientIpTrustedProxies:e.clientIpTrustedProxies}:{}}),y={root:t.root,wsKey:i,eventFile:s,routes:R,registry:H,rebuildRoutes:B,services:v,serviceKeys:Q,keyToProject:Z,keyToWorkspace:ee,clientIp:re,bodyStallMs:e.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:g,sweepMs:d,trafficFlushMs:p,https:u,projectRoots:Object.fromEntries(Object.entries(t.projectsConfigurations?.projects??{}).map(([r,o])=>[r,path.resolve(t.root,o.root)])),projectSourceRoots:C,...t.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(t.projectGraphStore)}:{},...e.sharedCompiler!==void 0?{sharedCompiler:e.sharedCompiler}:{},stopped:n(()=>l,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(v,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!t.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,e.sharedCompiler),label:i||path.basename(t.root),lens:{key:i,dashboard:e.dashboard!==!1,mcp:e.mcp!==!1&&I.some(r=>!!r.lens)}}));const f=(0,observability_1.createObservability)(s,{trafficFlushMs:p}),te=createClientErrorLogGate(),P=h2BudgetFromEnv(),se=createClientErrorLogGate(3e4),$=createH2Governor(P,(r,o)=>{se(r)&&console.warn(`[gateway] h2 aggregate budget: ${r} (${JSON.stringify(o)}). 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(r==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${r}`,`gateway-h2-budget:${r}`,{kind:"h2-aggregate-budget",event:r,project:"gateway",...o})}),N=(0,lifecycle_1.createServiceLayer)(y,f),O=(0,lens_children_1.createLensChildren)(y,f,e),G=new Map;let Y;try{Y=(0,control_1.startControlServer)({rt:y,killService:n((r,o)=>N.killSpawned(r,o),"killService"),onRegister:n(async r=>{retirePreviousWorkspace(G,r.wsKey,"re-registered");const o=await O.hostWorkspaceDashboard(r),w=await O.hostWorkspaceApps(r);G.set(r.wsKey,()=>{try{o?.()}finally{w()}})},"onRegister"),onUnregister:n(r=>{retirePreviousWorkspace(G,r.wsKey,"unregistered"),N.stopBuildHost(r.root)},"onUnregister")})}catch(r){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",r.message)}const b=(0,proxy_1.createProxy)(y,f,N),X=buildEdgeVerifierPool(()=>y.routes),K=(0,auth_1.createEdgeAuth)(y,f,X);let j;if(e.middleware){const r=require(path.resolve(t.root,e.middleware));j=r.default??r}const oe=n(r=>{if(j)try{j(r.req)}catch(o){f.traceStep(r.trace,"auth",{mode:"middleware",ok:!1,reason:o.message}),r.route&&f.finishTrace(r.trace,r.route.project,401),r.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${o.message}`)}},"builtinMiddleware"),J=e.hooks??{},V=(0,hooks_1.createHooks)({...J,afterAuth:[oe,...J.afterAuth??[]]}),z=(0,handler_1.createHandler)({rt:y,obs:f,auth:K,proxy:b,hooks:V}),ne=(0,upgrade_1.createUpgrade)({rt:y,auth:K,proxy:b,hooks:V}),ae=f.startEdgeFlusher(),ie=N.startSweeper();await O.bootSingletonsAndApps();for(const r of v)r.decl.eager&&N.ensureUp(r);let x,q,L;if(u){const r=require("../../../basic-ssl"),o=(0,service_keys_1.gatewayCacheDir)(t.root);L=n(()=>r.getCertificateSync(o,"gateway",y.routes.map(w=>w.host).filter(w=>!!w)),"mintCert"),x=L(),q=r.caCertPath()}const W=[],k=[],D=e.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const r of E){const o=x?require("node:http2").createSecureServer({key:x,cert:x,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:e.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:e.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(m,_)=>z(m,_)):http.createServer(z);o.on("upgrade",ne),o.on("error",m=>console.error(`[gateway] port ${r}: ${m.code}`)),o.on("connection",guardInboundSocket),o.on("secureConnection",guardInboundSocket),o.on("tlsClientError",(m,_)=>_.destroy()),o.on("clientError",(m,_)=>{const c=m,S=classifyClientError(c.code,c.message);if(S.report&&te(S.cause)){const M=_.remoteAddress??"unknown",A=S.status===null?"dropped":String(S.status);console.warn(`[gateway] inbound H1 clientError from ${M}: ${S.cause} (${c.code??c.message}${c.bytesParsed!==void 0?`, bytesParsed=${c.bytesParsed}`:""}) \u2192 ${A}. This status is the GATEWAY's, not the upstream service's.`),f.emit("warning",`inbound clientError: ${S.cause} \u2192 ${A}`,`gateway-client-error:${S.cause}`,{kind:"gateway-client-error",cause:S.cause,code:c.code??null,status:S.status,peer:M,bytesParsed:c.bytesParsed??null})}S.status!==null&&_.writable&&_.end(`HTTP/1.1 ${S.status} ${CLIENT_ERROR_REASON[S.status]}\r
|
|
2
2
|
\r
|
|
3
|
-
`),_.destroy()}),o.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,o.headersTimeout=e.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,
|
|
3
|
+
`),_.destroy()}),o.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,o.headersTimeout=e.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,D>0&&(o.maxConnections=D);const w=e.connectionsCheckingIntervalMs??types_1.GATEWAY_CONN_CHECK_MS;if(w>0&&(o.connectionsCheckingInterval=w),x!=null){const m=o,{NGHTTP2_CANCEL:_}=require("node:http2").constants;m.on("session",c=>{if(!$.admitSession(c)){c.on("error",()=>{});return}let S=0,M=0;c.on("stream",A=>{$.admitStream(c,A)&&A.once("close",()=>{c.destroyed?M+=1:A.rstCode===_&&(S+=1)})}),c.once("close",()=>{M!==0&&(console.warn(`[gateway] h2 session closed with ${M} stream(s) in flight after ${S} 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 ${M} stream(s) in flight`,"gateway-h2-session-lost",{kind:"h2-session-lost",project:"gateway",inflight:M,clientResets:S}))}),c.on("error",A=>{const ce=A.code??A.message;console.warn(`[gateway] h2 session error (${ce}) \u2014 dropping this session only`),c.destroyed||c.destroy()})}),m.on("sessionError",c=>{console.warn(`[gateway] h2 sessionError: ${c.code??c.message}`)})}await new Promise(m=>{o.listen(r,()=>{const _=o.address()?.port??r;k.push(_),console.log(`[gateway] Listening on ${x?"https":"http"}://localhost:${_}`),m()})}),W.push(o)}L&&(U=n(()=>{const r=L();for(const o of W)o.setSecureContext?.({key:r,cert:r})},"refreshCert")),console.log("[gateway] Routes:");for(const r of R){const o=r.pool?`pods@${r.pool.dir}`:r.target;console.log(`[gateway] ${r.host??"(default)"} \u2192 ${o}${r.prependPrefix?` (+${r.prependPrefix})`:""}${r.internal?" [internal: no middleware]":""} [${r.project}]`)}q&&console.log(`[gateway] CA: ${q} \u2014 one-time setup via the trust executor.`);const F=n(r=>r>0?String(r):"unbounded","unbounded");return console.log(`[gateway] front-door budget: \u2264${F(D)} connections, \u2264${F(P.maxSessions)} h2 sessions (each \u2264512 MB session memory, \u2264512 streams), \u2264${F(P.maxStreams)} open streams in total, idle h2 session reclaimed after ${P.idleSessionMs>0?`${Math.round(P.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 ${k.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:k,routes:R.map(r=>({host:r.host??"(default)",project:r.project,mode:r.pool?"pods":"tcp",internal:!!r.internal,prependPrefix:r.prependPrefix}))}),{ports:k,routes:R,services:v,stop:n(async()=>{if(!l){l=!0,clearInterval(ae),clearInterval(ie),f.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),X.stop(),Y?.close(),(0,workspace_registry_1.unregisterWorkspace)(i);for(const r of v)N.killSpawned(r,"gateway shutdown");await N.stopBuildHost(),await O.reapAll(),await Promise.all(W.map(r=>new Promise(o=>{let w=!1;const m=n(()=>{w||(w=!0,o())},"done"),_=r;r.close(()=>m());try{_.closeIdleConnections?.()}catch{}setTimeout(()=>{try{_.closeAllConnections?.()}catch{}m()},types_1.GATEWAY_STOP_GRACE_MS).unref?.()}))),b.closeProxy(),b.destroyAgents(),$.dispose(),(0,workspace_log_1.closeWorkspaceLogs)()}},"stop"),...y.lensSupervisor?{lensSupervisor:y.lensSupervisor}:{},...y.graphStores?{graphStores:y.graphStores}:{}}}T(startGateway,"startGateway"),n(startGateway,"startGateway");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway's half of the shared compiler (issues/open/pod-memory-05): it starts the workspace's BUILD HOST
|
|
3
|
+
* (build-host.ts — one webpack process for every service that can share it) and relays between it and each
|
|
4
|
+
* service's runner, over the two IPC channels it already owns. The runners never see the host and the host
|
|
5
|
+
* never sees a runner: no socket, no address, nothing another account could connect to.
|
|
6
|
+
*
|
|
7
|
+
* - A runner (serve-launch.ts) of THIS workspace is started with `LENSMCP_SHARED_BUILD=1`; its serve executor
|
|
8
|
+
* sends `join` on its channel (shared-build/attach.ts). The gateway names the member after the tree
|
|
9
|
+
* (`<project>#g<generation>`), starts the host if it is not up, and forwards the join.
|
|
10
|
+
* - The host answers per member; each answer goes to that member's runner.
|
|
11
|
+
* - A tree that dies or is killed leaves ({@link SharedBuildRelay.detach}) — the host pauses the project's
|
|
12
|
+
* entries once no tree holds it.
|
|
13
|
+
* - A host that exits unexpectedly is started again and every live join is sent to it again; runners keep
|
|
14
|
+
* their devservers serving the last good bundle meanwhile. A host that keeps dying (3 exits in a minute) is
|
|
15
|
+
* given up on: every member is refused, and each runner compiles on its own, as before.
|
|
16
|
+
* - A stale host of this same workspace root (a previous gateway's) is reaped before a start, matched exactly
|
|
17
|
+
* by its root argument — never another checkout's.
|
|
18
|
+
*
|
|
19
|
+
* One build host PER WORKSPACE the gateway serves: its own, and each REGISTERED (guest) workspace's, whose services
|
|
20
|
+
* start through the runner too since issues/closed/dev-gateway-21. A workspace's host is ITS OWN `@lensmcp/cluster`'s
|
|
21
|
+
* `build-host.js`, run in its root with its environment — the toolchain and the protocol of the executor that joins
|
|
22
|
+
* it — and it is stopped when the workspace unregisters ({@link SharedBuildRelay.release}).
|
|
23
|
+
*
|
|
24
|
+
* Not offered — the runner compiles on its own, exactly as before — to a service started through `nx run` (no
|
|
25
|
+
* channel to the gateway), to a workspace whose `@lensmcp/cluster` has no `build-host.js`, and when
|
|
26
|
+
* `LENSMCP_SHARED_COMPILER` / the gateway's `sharedCompiler` option is off.
|
|
27
|
+
*/
|
|
28
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
29
|
+
import type { RunnerScript } from './serve-launch';
|
|
30
|
+
import type { GatewayRuntime, ServiceCtl } from './types';
|
|
31
|
+
/** Exits within {@link CRASH_WINDOW_MS} after which the host is given up on. */
|
|
32
|
+
export declare const HOST_MAX_RESTARTS = 3;
|
|
33
|
+
/** The workspace a service belongs to, as the relay needs it: its root, and its key (a registered one's). */
|
|
34
|
+
export type BuildMember = Pick<ServiceCtl, 'root'> & Partial<Pick<ServiceCtl, 'wsKey'>>;
|
|
35
|
+
export interface SharedBuildRelay {
|
|
36
|
+
/** Will a runner of `svc` be compiled by its workspace's build host (it is then started with `LENSMCP_SHARED_BUILD=1`)? */
|
|
37
|
+
offeredTo(svc: BuildMember): boolean;
|
|
38
|
+
/** Relay this runner's joins: `proc` is the root of `svc`'s tree for `generation`. */
|
|
39
|
+
attach(svc: BuildMember & Pick<ServiceCtl, 'project'>, generation: number, proc: ChildProcess): void;
|
|
40
|
+
/** The tree rooted at `proc` is gone or going: its member leaves. Idempotent. */
|
|
41
|
+
detach(proc: ChildProcess): void;
|
|
42
|
+
/** The pid of the build host of the workspace at `root` (default: the gateway's own) while it runs — for the spec and `gateway status`. */
|
|
43
|
+
hostPid(root?: string): number | undefined;
|
|
44
|
+
/** A workspace left the gateway: stop its build host. */
|
|
45
|
+
release(root: string): Promise<void>;
|
|
46
|
+
/** Stop every build host (their watchers close and store the cache). */
|
|
47
|
+
stop(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export interface SharedBuildRelayDeps {
|
|
50
|
+
spawnProcess?: typeof spawn;
|
|
51
|
+
/** The gateway's own workspace's host. Default {@link buildHostScript}; `null`: there is none (never offered there). */
|
|
52
|
+
script?: RunnerScript | null;
|
|
53
|
+
/** A registered workspace's host. Default {@link workspaceBuildHostScript}. */
|
|
54
|
+
scriptFor?: (root: string) => RunnerScript | undefined;
|
|
55
|
+
/** The gateway's environment, for the switch. Default `process.env`. */
|
|
56
|
+
gatewayEnv?: NodeJS.ProcessEnv;
|
|
57
|
+
/** Skip the stale-host reap (tests). */
|
|
58
|
+
reapStale?: boolean;
|
|
59
|
+
log?: (line: string) => void;
|
|
60
|
+
}
|
|
61
|
+
/** The built `build-host.js` at the package root, or the `.ts` beside this source tree. */
|
|
62
|
+
export declare function buildHostScript(baseDir?: string): RunnerScript | undefined;
|
|
63
|
+
/** A registered workspace's own `@lensmcp/cluster/build-host.js`, or `undefined` when its package has none. */
|
|
64
|
+
export declare function workspaceBuildHostScript(root: string): RunnerScript | undefined;
|
|
65
|
+
export declare function createSharedBuildRelay(rt: GatewayRuntime, deps?: SharedBuildRelayDeps): SharedBuildRelay;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";var T=Object.defineProperty;var S=(r,t)=>T(r,"name",{value:t,configurable:!0});var L=Object.defineProperty,i=S((r,t)=>L(r,"name",{value:t,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.HOST_MAX_RESTARTS=void 0,exports.buildHostScript=buildHostScript,exports.workspaceBuildHostScript=workspaceBuildHostScript,exports.createSharedBuildRelay=createSharedBuildRelay;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),node_module_1=require("node:module"),path=tslib_1.__importStar(require("node:path")),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),pod_env_1=require("./pod-env"),registration_guards_1=require("./registration-guards");exports.HOST_MAX_RESTARTS=3;const CRASH_WINDOW_MS=6e4;function buildHostScript(r=path.join(__dirname,"../../..")){const t=path.join(r,"build-host.js");if(fs.existsSync(t))return{path:t,execArgv:[]};const u=path.join(r,"build-host.ts");if(fs.existsSync(u))try{return{path:u,execArgv:["--require",require.resolve("@swc-node/register")]}}catch{return}}S(buildHostScript,"buildHostScript"),i(buildHostScript,"buildHostScript");function workspaceBuildHostScript(r){try{return{path:(0,node_module_1.createRequire)(path.join(r,"package.json")).resolve("@lensmcp/cluster/build-host.js"),execArgv:[]}}catch{return}}S(workspaceBuildHostScript,"workspaceBuildHostScript"),i(workspaceBuildHostScript,"workspaceBuildHostScript");function createSharedBuildRelay(r,t={}){const u=t.script===void 0?buildHostScript():t.script??void 0,_=t.scriptFor??workspaceBuildHostScript,h=new Map,f=new WeakMap,d=(0,registration_guards_1.canonicalRoot)(r.root),v=i(e=>{const a=(0,registration_guards_1.canonicalRoot)(e.root);let c=h.get(a);return c||(c=a===d?createWorkspaceRelay(r,{root:r.root,...r.wsKey?{wsKey:r.wsKey}:{},eventFile:r.eventFile},u,t):createWorkspaceRelay(r,{root:e.root,...e.wsKey?{wsKey:e.wsKey}:{},eventFile:path.join(e.root,".lensmcp","events.jsonl")},_(e.root),t),h.set(a,c)),c},"relayFor");return{offeredTo:i(e=>(0,protocol_1.sharedCompilerEnabled)(t.gatewayEnv??process.env,r.sharedCompiler)&&v(e).offered(),"offeredTo"),attach(e,a,c){const y=v(e);f.set(c,y),y.attach(`${e.project}#g${a}`,c)},detach:i(e=>f.get(e)?.detach(e),"detach"),hostPid:i((e=r.root)=>h.get((0,registration_guards_1.canonicalRoot)(e))?.hostPid(),"hostPid"),release(e){const a=(0,registration_guards_1.canonicalRoot)(e),c=h.get(a);return h.delete(a),c?.stop()??Promise.resolve()},async stop(){const e=[...h.values()];h.clear(),await Promise.all(e.map(a=>a.stop()))}}}S(createSharedBuildRelay,"createSharedBuildRelay"),i(createSharedBuildRelay,"createSharedBuildRelay");function createWorkspaceRelay(r,t,u,_){const h=_.spawnProcess??node_child_process_1.spawn,f=_.log??(o=>console.log(o)),d=new Map,v=new WeakMap;let e,a=[],c,y=!1;const E=(0,registration_guards_1.canonicalRoot)(t.root)===(0,registration_guards_1.canonicalRoot)(r.root)?"this workspace":`'${t.wsKey??path.basename(t.root)}'`,b=i((o,s)=>{if(!o.connected)return;const n={channel:protocol_1.SHARED_BUILD_CHANNEL,event:s};try{o.send(n,void 0,void 0,()=>{})}catch{}},"toRunner"),R=i(o=>{if(e?.connected)try{e.send(o,void 0,void 0,()=>{})}catch{}},"toHost"),A=i(()=>{if(_.reapStale===!1)return;let o;try{o=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return}for(const s of o.split(`
|
|
2
|
+
`)){const n=s.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!n||!(0,protocol_1.isBuildHostCommandOf)(n[3],t.root))continue;const l=Number(n[1]),p=Number(n[2]);if(!(l===e?.pid||l===process.pid)){try{process.kill(-p,"SIGKILL")}catch{try{process.kill(l,"SIGKILL")}catch{}}f(`[gateway] reaped a stale build host (pid ${l}) of ${E}`)}}},"reapStaleHosts"),x=i(()=>{if(e||y||c||!u)return e;A();const o=t.wsKey||path.basename(t.root),s={...(0,pod_env_1.podBaseEnv)(process.env,r.root,t.root),NX_DAEMON:"false",NODE_ENV:"development",NX_WORKSPACE_ROOT:t.root,...t.wsKey?{LENSMCP_WS_KEY:t.wsKey}:{},LENSMCP_EVENT_FILE:t.eventFile,LENSMCP_GATEWAY_PID:String(process.pid)},n=h((0,process_name_1.namedExecPath)((0,process_name_1.processName)(o,protocol_1.BUILD_HOST_ROLE)),[...u.execArgv,u.path,...(0,protocol_1.buildHostArgs)(t.root)],{cwd:t.root,detached:!0,env:s,stdio:["ignore","pipe","pipe","ipc"]});e=n,f(`[gateway] build host starting (one compilation for the services of ${E} \u2014 LENSMCP_SHARED_COMPILER)`);const l=i(p=>m=>{if(!r.stopped())for(const g of m.toString().split(`
|
|
3
|
+
`))g.trim()&&p.write(`[build] ${g}
|
|
4
|
+
`)},"relayOut");return n.stdout?.on("data",l(process.stdout)),n.stderr?.on("data",l(process.stderr)),n.on("message",p=>{if(p?.type==="member"){const m=d.get(p.member);m&&b(m.proc,p.event)}}),n.on("exit",(p,m)=>{if(e===n&&(e=void 0),y||r.stopped())return;const g=Date.now();if(a=[...a.filter(w=>g-w<CRASH_WINDOW_MS),g],a.length>=exports.HOST_MAX_RESTARTS){c=`the build host exited ${a.length} times in a minute (last: ${p??m})`,f(`[gateway] ${c} \u2014 every service compiles on its own from now on`);for(const w of d.values())b(w.proc,{type:"refused",reason:c});d.clear();return}if(d.size!==0&&(f(`[gateway] build host exited (${p??m}) \u2014 restarting it for ${d.size} service tree(s)`),x()))for(const[w,{join:P}]of d)R({type:"join",member:w,join:P})}),n},"start"),j=i(o=>{const s=v.get(o);!s||d.get(s)?.proc!==o||(d.delete(s),R({type:"leave",member:s}))},"detach");return{offered:i(()=>!!u&&!c,"offered"),attach:i((o,s)=>{v.set(s,o),s.on("message",n=>{if((0,protocol_1.isRunnerJoinMessage)(n)){if(c||!x()){b(s,{type:"refused",reason:c??"the build host could not be started"});return}d.set(o,{proc:s,join:n.join}),R({type:"join",member:o,join:n.join})}}),s.once("exit",()=>j(s))},"attach"),detach:j,hostPid:i(()=>e?.pid,"hostPid"),stop(){y=!0,d.clear();const o=e;return e=void 0,!o||o.exitCode!==null||o.signalCode!==null?Promise.resolve():new Promise(s=>{const n=i(()=>{clearTimeout(l),s()},"done");o.once("exit",n);try{process.kill(-o.pid,"SIGTERM")}catch{try{o.kill("SIGTERM")}catch{}}const l=setTimeout(()=>{try{process.kill(-o.pid,"SIGKILL")}catch{try{o.kill("SIGKILL")}catch{}}s()},6e3);l.unref?.()})}}}S(createWorkspaceRelay,"createWorkspaceRelay"),i(createWorkspaceRelay,"createWorkspaceRelay");
|