@lensmcp/cluster 1.25.9 → 1.25.11

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/README.md CHANGED
@@ -83,7 +83,7 @@ A second backend **path-mounted** under a shared API domain:
83
83
  | `idleKillSec` | Kill a gateway-spawned (non-eager) service after this many idle seconds. |
84
84
  | `serveTarget` | Nx target that runs the service. Default `serve-hmr`. |
85
85
  | `adminUrl` | Devserver admin endpoint for scaling (defaults from `port`). |
86
- | `sources` | **Custom sources** (dev only): what the service REPORTS that the lens cannot observe — e.g. work waiting in its own outbox table. `[{ kind: "queue", name, path, intervalSec?, thresholds?: { oldestWaitMin? } }]`; `path` is a route on the service answering `lensmcp.queue/v1`, read from a running pod over its socket (a service scaled to zero is never woken). An unknown kind or a malformed entry is left out and logged. See the docs' *Custom sources* page. |
86
+ | `sources` | **Custom sources** (dev only): what the service REPORTS that the lens cannot observe — e.g. work waiting in its own outbox table, the background loop that drains it, a document store the taps do not model, a partner's API answering 429s. Entries of four kinds: `{ kind: "queue", name, path, intervalSec?, thresholds?: { oldestWaitMin?, stalledMin? } }`, `{ kind: "loop", name, path, intervalSec?, thresholds?: { idleMin? }, feeds? }`, `{ kind: "store", name, path, intervalSec?, thresholds?: { quotaPct?, errorRatePct?, orphanMin? } }`, `{ kind: "traffic", name, path?, intervalSec?, thresholds?: { failurePct?, p95Ms?, rateLimited? } }`; `path` is a route on the service answering `lensmcp.<kind>/v1` (`queue`, `loop`, `store`, `traffic`), read from a running pod over its socket every `intervalSec` (a service scaled to zero is never woken) with `x-lensmcp-probe: source:<kind>`. A `traffic` entry without a `path` is **push-only**: declared, never polled — the lens counts the `inbound`/`outbound` events the app pushes (`lensEmitCustom`); every other kind needs its route. `feeds` names the queue source on the same service a loop drains. Every threshold is filled with its default: minutes `oldestWaitMin` 10, `stalledMin` 15, `idleMin` 10, `orphanMin` 60; percents `quotaPct` 90, `errorRatePct` 5, `failurePct` 5; `p95Ms` 2000 (milliseconds); `rateLimited` 1 (a count of «too many requests» answers). A poll carries its snapshot in full only when it changed, or a minute after it last did; otherwise it says `same`. An unknown kind, a malformed entry or a threshold out of its unit's range is left out and logged; a `feeds` that names no queue source is dropped and logged. See the docs' *Custom sources* page. |
87
87
 
88
88
  A declaration must route *somewhere*: either a pod `service` or a TCP `upstream`/`port`.
89
89
 
@@ -12,22 +12,37 @@
12
12
  * the oldest dropped (the CLI's start-time sweep shifts the same way). Lines written between the copy and the
13
13
  * truncate — a few milliseconds of a 50 MB file — are lost; that is the price of not owning the descriptor.
14
14
  *
15
- * Only when this process's stdout IS `<root>/.lensmcp/gateway.log` (same device and inode): a gateway run in a
16
- * terminal, or under a test, has nothing to rotate.
15
+ * Whose file it is to rotate: this process's stdout when it IS `<root>/.lensmcp/gateway.log` (same device and inode) —
16
+ * or the file the launcher says it opened for append ({@link GATEWAY_LOG_ENV}). The second is the case that matters: the
17
+ * CLI starts `nx run gateway:serve`, and nx runs the gateway in its `run-executor.js`, whose stdout is a SOCKET back to
18
+ * the nx process — which is the one writing the file. The stdout check alone never matched there, so FoodGuard's log
19
+ * still grew ~70 MB an hour on 1.25.9 (issues/closed/foodguard-77, reopened 2026-10-20). Truncating by path is safe for
20
+ * the same reason as through the descriptor: every writer holds the launcher's append descriptor. A gateway run in a
21
+ * terminal, or under a test, with neither, has nothing to rotate.
17
22
  */
18
23
  export declare const OWN_LOG_MAX_BYTES: number;
19
24
  export declare const OWN_LOG_GENERATIONS = 3;
25
+ /**
26
+ * Set by `lensmcp gateway start` on the daemon it spawns: the absolute path of the log it opened for APPEND and handed as
27
+ * its stdout and stderr. The gateway inherits it through nx.
28
+ */
29
+ export declare const GATEWAY_LOG_ENV = "LENSMCP_GATEWAY_LOG";
20
30
  /** `<file>.1` … `.N-1` move up one; the oldest goes. */
21
31
  export declare function shiftGenerations(file: string, keep: number): void;
22
32
  export interface OwnLogOptions {
23
33
  /** The descriptor to watch and truncate — this process's stdout unless a spec hands another. */
24
34
  fd?: number;
35
+ /** The log the launcher opened for append — {@link GATEWAY_LOG_ENV} unless a spec hands another. */
36
+ launchedLog?: string;
25
37
  maxBytes?: number;
26
38
  keep?: number;
27
39
  /** A line in the new file saying where the old lines went. */
28
40
  note?: (line: string) => void;
29
41
  }
30
- /** One look: rotates when `fd` is `file` and over the cap. Returns whether it rotated. */
42
+ /**
43
+ * One look: rotates when `file` is over the cap and it is this process's stdout (`fd`) or the log the launcher opened for
44
+ * append (`launchedLog`). Returns whether it rotated.
45
+ */
31
46
  export declare function rotateOwnLog(file: string, opts?: OwnLogOptions): boolean;
32
47
  /** Look every 30 s while the gateway runs; the returned function stops it. */
33
48
  export declare function startOwnLogRotation(root: string, opts?: OwnLogOptions & {
@@ -1,3 +1,4 @@
1
- "use strict";var O=Object.defineProperty;var i=(t,e)=>O(t,"name",{value:e,configurable:!0});var _=Object.defineProperty,s=i((t,e)=>_(t,"name",{value:e,configurable:!0}),"s"),u=Object.defineProperty,a=s((t,e)=>u(t,"name",{value:e,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.OWN_LOG_GENERATIONS=exports.OWN_LOG_MAX_BYTES=void 0,exports.shiftGenerations=shiftGenerations,exports.rotateOwnLog=rotateOwnLog,exports.startOwnLogRotation=startOwnLogRotation,exports.linePrefixer=linePrefixer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));exports.OWN_LOG_MAX_BYTES=50*1024*1024,exports.OWN_LOG_GENERATIONS=3;const CHECK_EVERY_MS=3e4;function shiftGenerations(t,e){try{fs.unlinkSync(`${t}.${e}`)}catch{}for(let r=e-1;r>=1;r--)try{fs.renameSync(`${t}.${r}`,`${t}.${r+1}`)}catch{}}i(shiftGenerations,"shiftGenerations"),s(shiftGenerations,"shiftGenerations"),a(shiftGenerations,"shiftGenerations");function rotateOwnLog(t,e={}){const r=e.fd??1,o=e.maxBytes??exports.OWN_LOG_MAX_BYTES,c=e.keep??exports.OWN_LOG_GENERATIONS;let n,f;try{n=fs.fstatSync(r),f=fs.statSync(t)}catch{return!1}if(!n.isFile()||n.dev!==f.dev||n.ino!==f.ino||n.size<=o)return!1;try{shiftGenerations(t,c),fs.copyFileSync(t,`${t}.1`);try{fs.chmodSync(`${t}.1`,384)}catch{}fs.ftruncateSync(r,0)}catch{return!1}return(e.note??(l=>{try{fs.writeSync(r,`${l}
2
- `)}catch{}}))(`[gateway] ${path.basename(t)} passed ${Math.round(o/1048576)} MB \u2014 rotated; the lines before are in ${path.basename(t)}.1 (\u2026${c} kept)`),!0}i(rotateOwnLog,"rotateOwnLog"),s(rotateOwnLog,"rotateOwnLog"),a(rotateOwnLog,"rotateOwnLog");function startOwnLogRotation(t,e={}){const r=path.join(t,".lensmcp","gateway.log"),o=setInterval(()=>rotateOwnLog(r,e),e.everyMs??CHECK_EVERY_MS);return o.unref?.(),()=>clearInterval(o)}i(startOwnLogRotation,"startOwnLogRotation"),s(startOwnLogRotation,"startOwnLogRotation"),a(startOwnLogRotation,"startOwnLogRotation");function linePrefixer(t){let e=!0;const r=`[${t}] `;return o=>{let c="";for(const n of o.split(/(?<=\n)/))n&&(c+=(e?r:"")+n,e=n.endsWith(`
3
- `));return c}}i(linePrefixer,"linePrefixer"),s(linePrefixer,"linePrefixer"),a(linePrefixer,"linePrefixer");
1
+ "use strict";var l=Object.defineProperty;var i=(t,e)=>l(t,"name",{value:e,configurable:!0});var O=Object.defineProperty,a=i((t,e)=>O(t,"name",{value:e,configurable:!0}),"a"),y=Object.defineProperty,o=a((t,e)=>y(t,"name",{value:e,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_LOG_ENV=exports.OWN_LOG_GENERATIONS=exports.OWN_LOG_MAX_BYTES=void 0,exports.shiftGenerations=shiftGenerations,exports.rotateOwnLog=rotateOwnLog,exports.startOwnLogRotation=startOwnLogRotation,exports.linePrefixer=linePrefixer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path"));exports.OWN_LOG_MAX_BYTES=50*1024*1024,exports.OWN_LOG_GENERATIONS=3;const CHECK_EVERY_MS=3e4;exports.GATEWAY_LOG_ENV="LENSMCP_GATEWAY_LOG";const readEnv=o(t=>process.env[t],"readEnv");function shiftGenerations(t,e){try{fs.unlinkSync(`${t}.${e}`)}catch{}for(let r=e-1;r>=1;r--)try{fs.renameSync(`${t}.${r}`,`${t}.${r+1}`)}catch{}}i(shiftGenerations,"shiftGenerations"),a(shiftGenerations,"shiftGenerations"),o(shiftGenerations,"shiftGenerations");const sameFile=o((t,e)=>!!t&&t.isFile()&&t.dev===e.dev&&t.ino===e.ino,"sameFile"),statOr=o(t=>{try{return t()}catch{return}},"statOr");function rotateOwnLog(t,e={}){const r=e.fd??1,s=e.maxBytes??exports.OWN_LOG_MAX_BYTES,c=e.keep??exports.OWN_LOG_GENERATIONS,n=statOr(()=>fs.statSync(t));if(!n||n.size<=s)return!1;const u=sameFile(statOr(()=>fs.fstatSync(r)),n),p=e.launchedLog??readEnv(exports.GATEWAY_LOG_ENV);if(!u&&!(p&&sameFile(statOr(()=>fs.statSync(p)),n)))return!1;try{shiftGenerations(t,c),fs.copyFileSync(t,`${t}.1`);try{fs.chmodSync(`${t}.1`,384)}catch{}u?fs.ftruncateSync(r,0):fs.truncateSync(t,0)}catch{return!1}const f=u?_=>{try{fs.writeSync(r,`${_}
2
+ `)}catch{}}:_=>{try{fs.appendFileSync(t,`${_}
3
+ `)}catch{}};return(e.note??f)(`[gateway] ${path.basename(t)} passed ${Math.round(s/1048576)} MB \u2014 rotated; the lines before are in ${path.basename(t)}.1 (\u2026${c} kept)`),!0}i(rotateOwnLog,"rotateOwnLog"),a(rotateOwnLog,"rotateOwnLog"),o(rotateOwnLog,"rotateOwnLog");function startOwnLogRotation(t,e={}){const r=path.join(t,".lensmcp","gateway.log"),s=setInterval(()=>rotateOwnLog(r,e),e.everyMs??CHECK_EVERY_MS);return s.unref?.(),()=>clearInterval(s)}i(startOwnLogRotation,"startOwnLogRotation"),a(startOwnLogRotation,"startOwnLogRotation"),o(startOwnLogRotation,"startOwnLogRotation");function linePrefixer(t){let e=!0;const r=`[${t}] `;return s=>{let c="";for(const n of s.split(/(?<=\n)/))n&&(c+=(e?r:"")+n,e=n.endsWith(`
4
+ `));return c}}i(linePrefixer,"linePrefixer"),a(linePrefixer,"linePrefixer"),o(linePrefixer,"linePrefixer");
@@ -5,18 +5,63 @@ export declare const sourcesEnabled: () => boolean;
5
5
  export declare const sourcesTimeoutMs: () => number;
6
6
  /** Polls in flight at once, across every service (`LENSMCP_SOURCES_CONCURRENCY`, default 4). */
7
7
  export declare const sourcesConcurrency: () => number;
8
- /** The closed set of kinds, and the ones this release reads. MIRROR of `SOURCE_KINDS` / `SOURCE_KINDS_READ`. */
8
+ /**
9
+ * The closed set of kinds, and the ones this release reads — all of them since phase 3, so a kind outside the set is the
10
+ * only one refused. MIRROR of `SOURCE_KINDS` / `SOURCE_KINDS_READ`.
11
+ */
9
12
  export declare const SOURCE_KINDS: readonly ["queue", "loop", "store", "traffic"];
10
- export declare const SOURCE_KINDS_READ: readonly ["queue"];
13
+ export declare const SOURCE_KINDS_READ: readonly ["queue", "loop", "store", "traffic"];
11
14
  export type SourceKindRead = (typeof SOURCE_KINDS_READ)[number];
12
- /** MIRROR of `QUEUE_WIRE_SCHEMA`. */
15
+ /** MIRROR of `QUEUE_WIRE_SCHEMA` / `LOOP_WIRE_SCHEMA` / `STORE_WIRE_SCHEMA` / `TRAFFIC_WIRE_SCHEMA`. */
13
16
  export declare const QUEUE_WIRE_SCHEMA = "lensmcp.queue/v1";
17
+ export declare const LOOP_WIRE_SCHEMA = "lensmcp.loop/v1";
18
+ export declare const STORE_WIRE_SCHEMA = "lensmcp.store/v1";
19
+ export declare const TRAFFIC_WIRE_SCHEMA = "lensmcp.traffic/v1";
20
+ /** Where each kind's last good snapshot rides on a report — never another kind's field. MIRROR of `SOURCE_SNAPSHOT_FIELD`. */
21
+ export declare const SOURCE_SNAPSHOT_FIELD: {
22
+ readonly queue: "snapshot";
23
+ readonly loop: "loop";
24
+ readonly store: "store";
25
+ readonly traffic: "traffic";
26
+ };
27
+ /** How a loop's run ended: it worked, it failed, or it chose not to (a gate held it). MIRROR of `LOOP_OUTCOMES`. */
28
+ export declare const LOOP_OUTCOMES: readonly ["ok", "failed", "skipped"];
29
+ export type LoopOutcome = (typeof LOOP_OUTCOMES)[number];
30
+ /** Which way a traffic channel's messages go, seen from the service. */
31
+ export declare const TRAFFIC_DIRECTIONS: readonly ["inbound", "outbound"];
32
+ export type TrafficDirection = (typeof TRAFFIC_DIRECTIONS)[number];
14
33
  export declare const SOURCE_DEFAULT_INTERVAL_SEC = 10;
15
34
  export declare const SOURCE_MIN_INTERVAL_SEC = 2;
35
+ /** MIRROR of `QUEUE_DEFAULT_OLDEST_WAIT_MIN` / `QUEUE_DEFAULT_STALLED_MIN` / `LOOP_DEFAULT_IDLE_MIN`. */
16
36
  export declare const QUEUE_DEFAULT_OLDEST_WAIT_MIN = 10;
37
+ export declare const QUEUE_DEFAULT_STALLED_MIN = 15;
38
+ export declare const LOOP_DEFAULT_IDLE_MIN = 10;
39
+ /** MIRROR of `STORE_DEFAULT_QUOTA_PCT` / `STORE_DEFAULT_ERROR_RATE_PCT` / `STORE_DEFAULT_ORPHAN_MIN`. */
40
+ export declare const STORE_DEFAULT_QUOTA_PCT = 90;
41
+ export declare const STORE_DEFAULT_ERROR_RATE_PCT = 5;
42
+ export declare const STORE_DEFAULT_ORPHAN_MIN = 60;
43
+ /** MIRROR of `TRAFFIC_DEFAULT_FAILURE_PCT` / `TRAFFIC_DEFAULT_P95_MS` / `TRAFFIC_DEFAULT_RATE_LIMITED`. */
44
+ export declare const TRAFFIC_DEFAULT_FAILURE_PCT = 5;
45
+ export declare const TRAFFIC_DEFAULT_P95_MS = 2000;
46
+ export declare const TRAFFIC_DEFAULT_RATE_LIMITED = 1;
47
+ /**
48
+ * A report carries the snapshot in full when it changed, and at least this often even when it did not — so a lens
49
+ * that starts later (its replay is a few MB of the bus) holds one within a minute. MIRROR of `SOURCE_FULL_REPORT_MS`.
50
+ */
51
+ export declare const SOURCE_FULL_REPORT_MS = 60000;
52
+ /** A loop's own period may be at most a day: a longer one is not a loop the lens can watch run. */
53
+ export declare const LOOP_MAX_INTERVAL_SEC = 86400;
54
+ /**
55
+ * A loop's, a store's or a traffic channel's `lastError` is cut to this many characters (not refused: the error is the
56
+ * thing worth reading).
57
+ */
58
+ export declare const SOURCE_ERROR_MAX = 500;
17
59
  /** A snapshot larger than this is `malformed` — read no further. */
18
60
  export declare const SOURCE_MAX_BYTES: number;
19
- /** Groups one snapshot may hold; a producer with more caps it and says `truncated: { groups }`. */
61
+ /**
62
+ * Groups (a queue's, a store's) or channels (a traffic source's) one snapshot may hold; a producer with more caps it
63
+ * and says `truncated: { groups }` / `truncated: { channels }`.
64
+ */
20
65
  export declare const SOURCE_MAX_GROUPS = 500;
21
66
  /** Sources one service may declare. */
22
67
  export declare const SOURCE_MAX_PER_SERVICE = 16;
@@ -30,27 +75,60 @@ export declare const SOURCE_BACKOFF_MAX_MS = 60000;
30
75
  export declare const sourceStaleAfterMs: (reportEveryMs: number) => number;
31
76
  /** How long to wait before polling again after `failures` failed polls in a row: the interval, then doubling, capped. */
32
77
  export declare function sourceBackoffMs(intervalMs: number, failures: number): number;
33
- export interface QueueThresholds {
34
- /** Warn once a group's oldest pending item has waited longer than this. */
35
- oldestWaitMin: number;
36
- /** Accepted now; judged once the lens keeps a history of snapshots (phase 2). */
78
+ /**
79
+ * A source's thresholds — each kind's own, every one filled with its default when not given, so the lens judges each.
80
+ * Minutes unless the name says otherwise (`…Pct` a percent, `p95Ms` milliseconds, `rateLimited` a count). MIRROR of
81
+ * `SourceThresholds`.
82
+ */
83
+ export interface SourceThresholds {
84
+ /** A queue: warn once a group's oldest pending item has waited longer than this. */
85
+ oldestWaitMin?: number;
86
+ /** A queue: warn once a group's pending work has not moved (nothing taken, nothing finished) for this long. */
37
87
  stalledMin?: number;
88
+ /** A loop: warn once it has run and taken nothing for this long while the queue it `feeds` has work. */
89
+ idleMin?: number;
90
+ /** A store: warn once its bytes are at or over this percent of its quota. */
91
+ quotaPct?: number;
92
+ /** A store: warn once its window's errors are over this percent of its operations. */
93
+ errorRatePct?: number;
94
+ /** A store: warn once it has grown for this long and deleted nothing — what nothing cleans up. */
95
+ orphanMin?: number;
96
+ /** A traffic channel: warn once its failures are over this percent of its messages. */
97
+ failurePct?: number;
98
+ /** A traffic channel: warn once its p95 is over this many milliseconds. */
99
+ p95Ms?: number;
100
+ /** A traffic channel: warn once its counterpart answered «too many requests» this many times in the window. */
101
+ rateLimited?: number;
38
102
  }
39
- /** One `cluster.sources` entry, as the gateway polls it. */
103
+ /** One `cluster.sources` entry, as the gateway reads it. */
40
104
  export interface SourceDecl {
41
105
  kind: SourceKindRead;
42
106
  /** Names the source in the lens (`product-analysis outbox`). Unique per service. */
43
107
  name: string;
44
- /** A dev route ON THE SERVICE, sent to its pod as-is (the edge's `prependPrefix` never applies). */
45
- path: string;
108
+ /**
109
+ * A dev route ON THE SERVICE, sent to its pod as-is (the edge's `prependPrefix` never applies). Absent only on a
110
+ * PUSH-ONLY loop or traffic source — declared, never polled: the lens reads the events the app pushes. A loop that runs
111
+ * in a worker process (FoodGuard's admission poll) has no route an API pod could answer for it.
112
+ */
113
+ path?: string;
114
+ /**
115
+ * How often the gateway POLLS it, in seconds — for a polled loop, not the loop's own period (that one is on the wire).
116
+ * On a push-only LOOP it IS the loop's own period (the lens judges «overdue» by it); on push-only traffic nothing reads it.
117
+ */
46
118
  intervalSec: number;
47
- thresholds: QueueThresholds;
119
+ thresholds: SourceThresholds;
120
+ /** A loop: the NAME of the queue source on the same service that it drains. Absent when none, or none of that name. */
121
+ feeds?: string;
48
122
  }
49
123
  /**
50
124
  * `cluster.sources` read back, as `parseWorkerSpecs` reads `workers`: what cannot be used is left out and said in
51
- * `problems` — a bad entry never stops the others, and never passes silently. An unknown kind is said by name; a kind
52
- * of the closed set this release does not read yet is said as such; an entry with a key its kind does not know is
53
- * refused whole, so a misspelt `intervalSecs` is said rather than polled at the default.
125
+ * `problems` — a bad entry never stops the others, and never passes silently. A kind outside the closed set is said by
126
+ * name; an entry with a key its kind does not know is refused whole, so a misspelt `intervalSecs` is said rather than
127
+ * polled at the default (and `feeds` on anything but a loop is such a key); a threshold is refused in its own unit. A
128
+ * route is required of every kind but `traffic`, whose entry without one is PUSH-ONLY (declared, never polled) — a
129
+ * queue, a loop or a store without one would have nothing to say. A loop's `feeds` that names no queue source of this
130
+ * service costs only the link: the loop is read without it, and that is said — the loop's own numbers are still worth
131
+ * reading.
54
132
  */
55
133
  export declare function parseSourceDecls(raw: unknown): {
56
134
  sources: SourceDecl[];
@@ -81,6 +159,127 @@ export declare function parseQueueSnapshot(body: unknown): {
81
159
  ok: false;
82
160
  reason: string;
83
161
  };
162
+ /**
163
+ * A loop snapshot as the WIRE says it (`lensmcp.loop/v1`), validated — what a background loop, scheduler or sweep says
164
+ * of itself. Only `at`, `intervalSec` (the LOOP's own period, not the poll's) and `lastRunAt` are required.
165
+ */
166
+ export interface LoopWire {
167
+ at: string;
168
+ intervalSec: number;
169
+ /** When its last run began; `null`: it has not run since the service started. */
170
+ lastRunAt: string | null;
171
+ lastDurationMs?: number;
172
+ lastOutcome?: LoopOutcome;
173
+ /** Items its last run took. */
174
+ lastItems?: number;
175
+ nextRunAt?: string | null;
176
+ /** Over the service's own window of `windowSec`. */
177
+ runsInWindow?: number;
178
+ itemsInWindow?: number;
179
+ failuresInWindow?: number;
180
+ windowSec?: number;
181
+ /** The last failed run's error, cut to {@link SOURCE_ERROR_MAX} characters. */
182
+ lastError?: string;
183
+ }
184
+ /**
185
+ * A loop snapshot, validated as {@link parseQueueSnapshot} validates a queue's: any departure is a REASON and the source
186
+ * is `malformed` — never a loop that «did nothing». `lastRunAt` must be SAID — `null` when the loop has not run since the
187
+ * service started: an absent one cannot be told from a producer that forgot it. `lastError` is the one field CUT rather
188
+ * than refused: a long stack trace is exactly what a failing loop says, and its head is the part worth reading.
189
+ */
190
+ export declare function parseLoopSnapshot(body: unknown): {
191
+ ok: true;
192
+ loop: LoopWire;
193
+ } | {
194
+ ok: false;
195
+ reason: string;
196
+ };
197
+ /** One group of a store snapshot — a bucket, a tenant's folder — as the WIRE names it. */
198
+ export interface StoreWireGroup {
199
+ key: string;
200
+ labels?: Record<string, string>;
201
+ objects: number;
202
+ bytes: number;
203
+ }
204
+ /**
205
+ * A store snapshot as the WIRE says it (`lensmcp.store/v1`), validated — what a storage backend the taps do not model
206
+ * says of itself. Only `at`, `objects` and `bytes` are required.
207
+ */
208
+ export interface StoreWire {
209
+ at: string;
210
+ objects: number;
211
+ bytes: number;
212
+ /** What it may hold; `null`: no quota it knows of. */
213
+ quotaBytes?: number | null;
214
+ /** Operations of every kind over the service's window of `windowSec` — what `errorsInWindow` is a rate of. */
215
+ opsInWindow?: number;
216
+ writesInWindow?: number;
217
+ deletesInWindow?: number;
218
+ errorsInWindow?: number;
219
+ windowSec?: number;
220
+ lastWriteAt?: string | null;
221
+ /** The last failed operation's error, cut to {@link SOURCE_ERROR_MAX} characters. */
222
+ lastError?: string;
223
+ groups?: StoreWireGroup[];
224
+ truncated?: {
225
+ groups: number;
226
+ };
227
+ }
228
+ /**
229
+ * A store snapshot, validated as a queue's is: any departure is a REASON and the source is `malformed` — never an empty
230
+ * store. `objects` and `bytes` must be SAID: a store that cannot count itself has nothing the lens can judge. `groups`
231
+ * is optional — a store with no buckets to tell apart says none — but a list given is held to a queue's group rules.
232
+ * `lastError` is cut, never refused, as a loop's is.
233
+ */
234
+ export declare function parseStoreSnapshot(body: unknown): {
235
+ ok: true;
236
+ store: StoreWire;
237
+ } | {
238
+ ok: false;
239
+ reason: string;
240
+ };
241
+ /** One channel of a traffic snapshot, as the WIRE names it. */
242
+ export interface TrafficWireChannel {
243
+ key: string;
244
+ labels?: Record<string, string>;
245
+ direction?: TrafficDirection;
246
+ /** Who is at the other end — a partner, a host, a queue. */
247
+ counterpart?: string;
248
+ /** Over the service's window of `windowSec`. */
249
+ messagesInWindow?: number;
250
+ failuresInWindow?: number;
251
+ /** Answered «too many requests» (an HTTP 429, a quota) — counted apart from `failuresInWindow`. */
252
+ rateLimitedInWindow?: number;
253
+ p95Ms?: number;
254
+ lastAt?: string | null;
255
+ /** Cut to {@link SOURCE_ERROR_MAX} characters. */
256
+ lastError?: string;
257
+ }
258
+ /**
259
+ * A traffic snapshot as the WIRE says it (`lensmcp.traffic/v1`), validated — messages over channels the taps do not
260
+ * classify. Only `at` and `channels` are required.
261
+ */
262
+ export interface TrafficWire {
263
+ at: string;
264
+ windowSec?: number;
265
+ channels: TrafficWireChannel[];
266
+ truncated?: {
267
+ channels: number;
268
+ };
269
+ }
270
+ /**
271
+ * A traffic snapshot, validated as a queue's is: any departure is a REASON and the source is `malformed` — never «no
272
+ * traffic». `channels` must be SAID; an empty list that parses is a real «nothing went over it». A channel's numbers are
273
+ * all optional (a webhook may know its messages and not their p95), but one given is held to its type: a `p95Ms` may be
274
+ * fractional, a count may not. `lastError` is cut, never refused.
275
+ */
276
+ export declare function parseTrafficSnapshot(body: unknown): {
277
+ ok: true;
278
+ traffic: TrafficWire;
279
+ } | {
280
+ ok: false;
281
+ reason: string;
282
+ };
84
283
  export type SourcePollState = 'current' | 'unreachable' | 'refused' | 'malformed' | 'not-running';
85
284
  /** A group on the bus: the wire's `key` travels as `group` — the bus redacts any field NAMED `key`. */
86
285
  export interface QueueGroupRaw {
@@ -106,14 +305,61 @@ export interface QueueSnapshotRaw {
106
305
  truncated?: {
107
306
  groups: number;
108
307
  };
308
+ /** Names its content (the groups and `truncated`), not its times — {@link sourceDigest}. */
309
+ digest: string;
310
+ }
311
+ /** A loop snapshot on the bus: the wire's fields, when the gateway received it, and what it said ({@link sourceDigest}). */
312
+ export interface LoopSnapshotRaw extends LoopWire {
313
+ receivedAt: number;
314
+ /** Names every field but `at`/`receivedAt`. */
315
+ digest: string;
316
+ }
317
+ /** A store's group on the bus: the wire's `key` travels as `group`, as a queue's does. */
318
+ export interface StoreGroupRaw {
319
+ group: string;
320
+ labels?: Record<string, string>;
321
+ objects: number;
322
+ bytes: number;
323
+ }
324
+ /** A store snapshot on the bus: the wire's fields, its groups the largest first, when it arrived, and its digest. */
325
+ export interface StoreSnapshotRaw extends Omit<StoreWire, 'groups'> {
326
+ receivedAt: number;
327
+ /** Names every field but `at`/`receivedAt`. */
328
+ digest: string;
329
+ groups?: StoreGroupRaw[];
330
+ }
331
+ /** A traffic channel on the bus: the wire's `key` travels as `channel`. */
332
+ export interface TrafficChannelRaw extends Omit<TrafficWireChannel, 'key'> {
333
+ channel: string;
334
+ }
335
+ /** A traffic snapshot on the bus: the wire's fields, its channels the most messages first, when it arrived, its digest. */
336
+ export interface TrafficSnapshotRaw extends Omit<TrafficWire, 'channels'> {
337
+ receivedAt: number;
338
+ /** Names every field but `at`/`receivedAt`. */
339
+ digest: string;
340
+ channels: TrafficChannelRaw[];
341
+ }
342
+ /** A last good snapshot as the poller keeps it — of its source's kind. */
343
+ export type KeptSnapshot = QueueSnapshotRaw | LoopSnapshotRaw | StoreSnapshotRaw | TrafficSnapshotRaw;
344
+ /**
345
+ * In place of the snapshot, on a report whose last good snapshot is the one an earlier report carried in full: its
346
+ * digest, and its own `at`/`receivedAt` — fresh after a `current` poll that read the same again, the held one's after a
347
+ * poll that did not read. MIRROR of `SourceSameRaw`.
348
+ */
349
+ export interface SourceSameRaw {
350
+ digest: string;
351
+ at: string;
352
+ receivedAt: number;
109
353
  }
110
354
  /** A declaration on the bus. `sourceKind`, because `kind` is the event's own. */
111
355
  export interface SourceDeclRaw {
112
356
  sourceKind: SourceKindRead;
113
357
  name: string;
114
- path: string;
358
+ /** Absent only on a push-only loop or traffic source — which is declared, and never polled. */
359
+ path?: string;
115
360
  intervalSec: number;
116
- thresholds: QueueThresholds;
361
+ thresholds: SourceThresholds;
362
+ feeds?: string;
117
363
  }
118
364
  export interface SourcePollRaw extends SourceDeclRaw {
119
365
  kind: 'source-poll';
@@ -126,7 +372,16 @@ export interface SourcePollRaw extends SourceDeclRaw {
126
372
  durationMs?: number;
127
373
  reportEveryMs: number;
128
374
  failures?: number;
375
+ /** A queue's last good snapshot, in full. */
129
376
  snapshot?: QueueSnapshotRaw;
377
+ /** A loop's last good snapshot, in full. */
378
+ loop?: LoopSnapshotRaw;
379
+ /** A store's. */
380
+ store?: StoreSnapshotRaw;
381
+ /** A traffic source's (a push-only one is never polled, so never reported). */
382
+ traffic?: TrafficSnapshotRaw;
383
+ /** In place of the kind's snapshot: the last good snapshot is unchanged since the report that carried it in full. */
384
+ same?: SourceSameRaw;
130
385
  }
131
386
  export interface SourceDeclarationRaw {
132
387
  kind: 'source-declaration';
@@ -134,24 +389,64 @@ export interface SourceDeclarationRaw {
134
389
  sources: SourceDeclRaw[];
135
390
  problems: string[];
136
391
  }
392
+ /** A last good snapshot in full, under its kind's own field ({@link SOURCE_SNAPSHOT_FIELD}). */
393
+ export type SourceFull = {
394
+ snapshot: QueueSnapshotRaw;
395
+ } | {
396
+ loop: LoopSnapshotRaw;
397
+ } | {
398
+ store: StoreSnapshotRaw;
399
+ } | {
400
+ traffic: TrafficSnapshotRaw;
401
+ };
402
+ /** What one report carries of the last good snapshot: in full, or `same` in its place. */
403
+ export type SourceCarried = SourceFull | {
404
+ same: SourceSameRaw;
405
+ };
406
+ /** `kept` under its kind's field — never another kind's: the contract refuses a report that carries one. */
407
+ export declare const sourceFull: (kind: SourceKindRead, kept: KeptSnapshot) => SourceFull;
408
+ /**
409
+ * Names a snapshot's CONTENT — never when it was measured or received: sha1 of its canonical JSON, the first 16 hex
410
+ * characters. Two polls that read the same have the same digest, so the second report can say `same` instead of
411
+ * carrying it all again. Not a secret and not a check against tampering — a name, short enough to ride every report.
412
+ */
413
+ export declare function sourceDigest(content: unknown): string;
137
414
  /** The snapshot as the bus carries it: every group, the oldest-waiting first (then the largest); totals over them. */
138
415
  export declare function keepQueueSnapshot(at: string, groups: readonly QueueWireGroup[], receivedAt: number, truncated?: {
139
416
  groups: number;
140
417
  }): QueueSnapshotRaw;
141
- /** One poll, as the bus carries it. */
418
+ /** A loop snapshot as the bus carries it: the wire's fields, when it arrived, and its digest. */
419
+ export declare function keepLoopSnapshot(loop: LoopWire, receivedAt: number): LoopSnapshotRaw;
420
+ /**
421
+ * A store snapshot as the bus carries it: the wire's fields, its groups the largest (by bytes, then objects) first and
422
+ * keyed `group`, when it arrived, and its digest — of every field but its times, so a write (`lastWriteAt`) is a change,
423
+ * and the same groups answered in another order are not.
424
+ */
425
+ export declare function keepStoreSnapshot(store: StoreWire, receivedAt: number): StoreSnapshotRaw;
426
+ /**
427
+ * A traffic snapshot as the bus carries it: the wire's fields, its channels the most messages first and keyed
428
+ * `channel`, when it arrived, and its digest — of every field but its times.
429
+ */
430
+ export declare function keepTrafficSnapshot(traffic: TrafficWire, receivedAt: number): TrafficSnapshotRaw;
431
+ /** `same` for a kept snapshot (any kind's): its digest, and its own times. */
432
+ export declare const sourceSame: (kept: KeptSnapshot) => SourceSameRaw;
433
+ /** One poll, as the bus carries it — with what it carries of the last good snapshot, if one is held. */
142
434
  export declare function buildSourcePollRaw(project: string, decl: SourceDecl, poll: {
143
435
  state: SourcePollState;
144
436
  polledAt: number;
145
437
  detail?: string;
146
438
  status?: number;
147
439
  durationMs?: number;
148
- }, reportEveryMs: number, failures: number, snapshot: QueueSnapshotRaw | undefined): SourcePollRaw;
440
+ }, reportEveryMs: number, failures: number, carried?: SourceCarried): SourcePollRaw;
149
441
  export declare function buildSourceDeclarationRaw(project: string, parsed: {
150
442
  sources: readonly SourceDecl[];
151
443
  problems: readonly string[];
152
444
  }): SourceDeclarationRaw;
153
- /** The event's title: what a person reads in the event list. */
154
- export declare function sourcePollTitle(raw: SourcePollRaw): string;
445
+ /**
446
+ * The event's title: what a person reads in the event list. `held` is the last good snapshot the poller holds — a
447
+ * report that carries `same` in its place is still titled with what it says.
448
+ */
449
+ export declare function sourcePollTitle(raw: SourcePollRaw, held?: KeptSnapshot): string;
155
450
  export type SourceHttpOutcome = {
156
451
  ok: true;
157
452
  status: number;
@@ -201,8 +496,14 @@ export interface SourceProbeView {
201
496
  failures: number;
202
497
  /** When the next poll is due (backed off while it fails). */
203
498
  nextAt: number;
204
- /** The last good snapshot, kept whatever the state, with its age. */
499
+ /** A queue's last good snapshot, kept whatever the state, with its age. */
205
500
  snapshot?: QueueSnapshotRaw;
501
+ /** A loop's — the same rules. */
502
+ loop?: LoopSnapshotRaw;
503
+ /** A store's — the same rules. */
504
+ store?: StoreSnapshotRaw;
505
+ /** A traffic source's — the same rules. A push-only one has no probe: there is nothing to poll. */
506
+ traffic?: TrafficSnapshotRaw;
206
507
  ageMs?: number;
207
508
  }
208
509
  export interface SourcePoller {
@@ -1 +1 @@
1
- "use strict";var k=Object.defineProperty;var x=(e,s)=>k(e,"name",{value:s,configurable:!0});var U=Object.defineProperty,v=x((e,s)=>U(e,"name",{value:s,configurable:!0}),"v"),b=Object.defineProperty,l=v((e,s)=>b(e,"name",{value:s,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sourceStaleAfterMs=exports.SOURCE_BACKOFF_MAX_MS=exports.SOURCE_DECLARATION_REPORT_MS=exports.SOURCE_NOT_RUNNING_REPORT_MS=exports.SOURCE_MAX_PER_SERVICE=exports.SOURCE_MAX_GROUPS=exports.SOURCE_MAX_BYTES=exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=exports.SOURCE_MIN_INTERVAL_SEC=exports.SOURCE_DEFAULT_INTERVAL_SEC=exports.QUEUE_WIRE_SCHEMA=exports.SOURCE_KINDS_READ=exports.SOURCE_KINDS=exports.sourcesConcurrency=exports.sourcesTimeoutMs=exports.sourcesEnabled=void 0,exports.sourceBackoffMs=sourceBackoffMs,exports.parseSourceDecls=parseSourceDecls,exports.parseQueueSnapshot=parseQueueSnapshot,exports.keepQueueSnapshot=keepQueueSnapshot,exports.buildSourcePollRaw=buildSourcePollRaw,exports.buildSourceDeclarationRaw=buildSourceDeclarationRaw,exports.sourcePollTitle=sourcePollTitle,exports.requestOverSocket=requestOverSocket,exports.createSourcePoller=createSourcePoller;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),discovery_1=require("./discovery"),readEnv=l(e=>process.env[e],"readEnv"),envInt=l((e,s,n)=>{const u=Number(readEnv(e));return Number.isInteger(u)&&u>=n?u:s},"envInt"),sourcesEnabled=l(()=>readEnv("LENSMCP_SOURCES")!=="0","sourcesEnabled");exports.sourcesEnabled=sourcesEnabled;const sourcesTimeoutMs=l(()=>envInt("LENSMCP_SOURCES_TIMEOUT_MS",3e3,100),"sourcesTimeoutMs");exports.sourcesTimeoutMs=sourcesTimeoutMs;const sourcesConcurrency=l(()=>envInt("LENSMCP_SOURCES_CONCURRENCY",4,1),"sourcesConcurrency");exports.sourcesConcurrency=sourcesConcurrency,exports.SOURCE_KINDS=["queue","loop","store","traffic"],exports.SOURCE_KINDS_READ=["queue"],exports.QUEUE_WIRE_SCHEMA="lensmcp.queue/v1",exports.SOURCE_DEFAULT_INTERVAL_SEC=10,exports.SOURCE_MIN_INTERVAL_SEC=2,exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=10,exports.SOURCE_MAX_BYTES=256*1024,exports.SOURCE_MAX_GROUPS=500,exports.SOURCE_MAX_PER_SERVICE=16,exports.SOURCE_NOT_RUNNING_REPORT_MS=6e4,exports.SOURCE_DECLARATION_REPORT_MS=5*6e4,exports.SOURCE_BACKOFF_MAX_MS=6e4;const sourceStaleAfterMs=l(e=>Math.max(3e4,3*Math.max(0,e)),"sourceStaleAfterMs");exports.sourceStaleAfterMs=sourceStaleAfterMs;function sourceBackoffMs(e,s){return s<=1?e:Math.min(e*2**Math.min(s-1,10),Math.max(e,exports.SOURCE_BACKOFF_MAX_MS))}x(sourceBackoffMs,"sourceBackoffMs"),v(sourceBackoffMs,"sourceBackoffMs"),l(sourceBackoffMs,"sourceBackoffMs");const ENTRY_KEYS=["kind","name","path","intervalSec","thresholds"],THRESHOLD_KEYS={queue:["oldestWaitMin","stalledMin"]},PATH_RE=/^\/(?!\/)[^\s#\u0000-\u001f\u007f]{0,511}$/,NAME_RE=/^[^\u0000-\u001f\u007f]{1,100}$/,isObj=l(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isObj"),minutes=l(e=>typeof e=="number"&&Number.isFinite(e)&&e>0&&e<=10080,"minutes");function parseSourceDecls(e){const s=[],n=[];return e===void 0?{sources:s,problems:n}:Array.isArray(e)?(e.forEach((u,p)=>{const f=`sources[${p}]`;if(!isObj(u)){n.push(`${f} is not an object \u2014 not read`);return}const E=typeof u.name=="string"&&u.name.trim()?`${f} "${u.name}"`:f,i=u.kind;if(typeof i!="string"||!exports.SOURCE_KINDS.includes(i)){n.push(`${E}: kind ${JSON.stringify(i??null)} is not a source kind (${exports.SOURCE_KINDS.join(", ")}) \u2014 not read`);return}if(!exports.SOURCE_KINDS_READ.includes(i)){n.push(`${E}: kind "${i}" is not read by this release (it reads ${exports.SOURCE_KINDS_READ.join(", ")}) \u2014 not read`);return}const r=i,h=Object.keys(u).filter(o=>!ENTRY_KEYS.includes(o));if(h.length){n.push(`${E}: unknown key${h.length===1?"":"s"} ${h.map(o=>JSON.stringify(o)).join(", ")} for a ${r} source (known: ${ENTRY_KEYS.join(", ")}) \u2014 not read`);return}const a=u.name;if(typeof a!="string"||!a.trim()||!NAME_RE.test(a)){n.push(`${f}: name ${JSON.stringify(a??null)} must be 1\u2013100 characters, not blank \u2014 not read`);return}if(s.some(o=>o.name===a)){n.push(`${E}: the name is declared twice on this service \u2014 read once`);return}const _=u.path;if(typeof _!="string"||!PATH_RE.test(_)||_.split("?")[0].split("/").includes("..")){n.push(`${E}: path ${JSON.stringify(_??null)} must be a route on the service, starting with "/" (e.g. "/api/dev/lens/backlog") \u2014 not read`);return}const m=u.intervalSec;if(m!==void 0&&!(typeof m=="number"&&Number.isInteger(m)&&m>=exports.SOURCE_MIN_INTERVAL_SEC&&m<=3600)){n.push(`${E}: intervalSec ${JSON.stringify(m)} must be a whole number of seconds from ${exports.SOURCE_MIN_INTERVAL_SEC} to 3600 \u2014 not read`);return}const S=u.thresholds;if(S!==void 0&&!isObj(S)){n.push(`${E}: thresholds must be an object \u2014 not read`);return}const O=isObj(S)?S:{},y=Object.keys(O).filter(o=>!THRESHOLD_KEYS[r].includes(o));if(y.length){n.push(`${E}: unknown threshold${y.length===1?"":"s"} ${y.map(o=>JSON.stringify(o)).join(", ")} for a ${r} source (known: ${THRESHOLD_KEYS[r].join(", ")}) \u2014 not read`);return}const t=THRESHOLD_KEYS[r].find(o=>O[o]!==void 0&&!minutes(O[o]));if(t){n.push(`${E}: thresholds.${t} ${JSON.stringify(O[t])} must be minutes, more than 0 and at most 10080 \u2014 not read`);return}if(s.length>=exports.SOURCE_MAX_PER_SERVICE){n.push(`${E}: more than ${exports.SOURCE_MAX_PER_SERVICE} sources on one service \u2014 not read`);return}s.push({kind:r,name:a,path:_,intervalSec:m??exports.SOURCE_DEFAULT_INTERVAL_SEC,thresholds:{oldestWaitMin:O.oldestWaitMin??exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN,...O.stalledMin!==void 0?{stalledMin:O.stalledMin}:{}}})}),{sources:s,problems:n}):{sources:s,problems:["`sources` is not a list of { kind, name, path } entries \u2014 no source is read"]}}x(parseSourceDecls,"parseSourceDecls"),v(parseSourceDecls,"parseSourceDecls"),l(parseSourceDecls,"parseSourceDecls");const ISO_RE=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})$/,isIso=l(e=>typeof e=="string"&&ISO_RE.test(e)&&!Number.isNaN(Date.parse(e)),"isIso"),isCount=l(e=>typeof e=="number"&&Number.isSafeInteger(e)&&e>=0,"isCount");function parseQueueSnapshot(e){const s=l(i=>({ok:!1,reason:i}),"fail");if(!isObj(e))return s("the snapshot is not a JSON object");if(e.schema!==exports.QUEUE_WIRE_SCHEMA)return s(`schema is ${e.schema===void 0?"absent":JSON.stringify(e.schema).slice(0,60)}, not "${exports.QUEUE_WIRE_SCHEMA}"`);const n=e.at;if(!isIso(n))return s("`at` is not an ISO time");const u=e.truncated;if(u!==void 0&&!(isObj(u)&&isCount(u.groups)))return s("`truncated` must be { groups: <how many were dropped> }, or absent");const p=e.groups;if(!Array.isArray(p))return s("`groups` is not a list");if(p.length>exports.SOURCE_MAX_GROUPS)return s(`${p.length} groups \u2014 at most ${exports.SOURCE_MAX_GROUPS}; cap them and say \`truncated: { groups: <dropped> }\``);const f=new Set,E=[];for(let i=0;i<p.length;i++){const r=p[i],h=`groups[${i}]`;if(!isObj(r))return s(`${h} is not an object`);const a=r.key;if(typeof a!="string"||a.length===0||a.length>200)return s(`${h}.key must be a string of 1\u2013200 characters`);if(f.has(a))return s(`${h}.key ${JSON.stringify(a)} appears twice`);if(f.add(a),!isCount(r.pending))return s(`${h}.pending must be a whole number \u2265 0`);for(const S of["inFlight","failed","completedInWindow"])if(r[S]!==void 0&&!isCount(r[S]))return s(`${h}.${S} must be a whole number \u2265 0, or absent`);const _=r.oldestPendingAt;if(_!=null&&!isIso(_))return s(`${h}.oldestPendingAt must be an ISO time, null, or absent`);const m=r.labels;if(m!==void 0){if(!isObj(m))return s(`${h}.labels must be an object of strings`);const S=Object.entries(m);if(S.length>16)return s(`${h}.labels has more than 16 entries`);for(const[O,y]of S)if(O.length===0||O.length>64||typeof y!="string"||y.length>200)return s(`${h}.labels.${O.slice(0,64)} must be a string of at most 200 characters`)}E.push({key:a,...m!==void 0?{labels:m}:{},pending:r.pending,...r.inFlight!==void 0?{inFlight:r.inFlight}:{},...r.failed!==void 0?{failed:r.failed}:{},..._!==void 0?{oldestPendingAt:_}:{},...r.completedInWindow!==void 0?{completedInWindow:r.completedInWindow}:{}})}return{ok:!0,at:n,groups:E,...u!==void 0?{truncated:{groups:u.groups}}:{}}}x(parseQueueSnapshot,"parseQueueSnapshot"),v(parseQueueSnapshot,"parseQueueSnapshot"),l(parseQueueSnapshot,"parseQueueSnapshot");const declRaw=l(e=>({sourceKind:e.kind,name:e.name,path:e.path,intervalSec:e.intervalSec,thresholds:{...e.thresholds}}),"declRaw"),oldestOf=l(e=>e.pending>0&&e.oldestPendingAt?Date.parse(e.oldestPendingAt):Number.POSITIVE_INFINITY,"oldestOf");function keepQueueSnapshot(e,s,n,u){const p={groups:s.length,pending:0,inFlight:0,failed:0};let f;for(const i of s)p.pending+=i.pending,p.inFlight+=i.inFlight??0,p.failed+=i.failed??0,i.completedInWindow!==void 0&&(f=(f??0)+i.completedInWindow);f!==void 0&&(p.completedInWindow=f);const E=[...s].sort((i,r)=>oldestOf(i)-oldestOf(r)||r.pending-i.pending||(i.key<r.key?-1:i.key>r.key?1:0));return{at:e,receivedAt:n,totals:p,groups:E.map(({key:i,...r})=>({group:i,...r})),...u?{truncated:{groups:u.groups}}:{}}}x(keepQueueSnapshot,"keepQueueSnapshot"),v(keepQueueSnapshot,"keepQueueSnapshot"),l(keepQueueSnapshot,"keepQueueSnapshot");function buildSourcePollRaw(e,s,n,u,p,f){return{kind:"source-poll",provenance:"reported",project:e,...declRaw(s),state:n.state,...n.detail!==void 0?{detail:n.detail}:{},...n.status!==void 0?{status:n.status}:{},polledAt:n.polledAt,...n.durationMs!==void 0?{durationMs:n.durationMs}:{},reportEveryMs:u,...p>0?{failures:p}:{},...f?{snapshot:f}:{}}}x(buildSourcePollRaw,"buildSourcePollRaw"),v(buildSourcePollRaw,"buildSourcePollRaw"),l(buildSourcePollRaw,"buildSourcePollRaw");function buildSourceDeclarationRaw(e,s){return{kind:"source-declaration",project:e,sources:s.sources.map(declRaw),problems:[...s.problems]}}x(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),v(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),l(buildSourceDeclarationRaw,"buildSourceDeclarationRaw");const span=l(e=>{const s=Math.round(e/6e4);return e<6e4?`${Math.max(0,Math.round(e/1e3))}s`:s<120?`${s}m`:`${Math.floor(s/60)}h${s%60?` ${s%60}m`:""}`},"span");function sourcePollTitle(e){const s=`${e.sourceKind} ${e.project}/${e.name} (reported)`;if(e.state!=="current"||!e.snapshot)return`${s}: ${e.state}${e.detail?` \u2014 ${e.detail}`:""}`;const n=e.snapshot,u=n.groups.find(f=>f.pending>0&&f.oldestPendingAt),p=u?.oldestPendingAt?Date.parse(n.at)-Date.parse(u.oldestPendingAt):void 0;return`${s}: ${n.totals.pending} pending in ${n.totals.groups} group${n.totals.groups===1?"":"s"}${p!==void 0?`, oldest waiting ${span(Math.max(0,p))}`:""}`}x(sourcePollTitle,"sourcePollTitle"),v(sourcePollTitle,"sourcePollTitle"),l(sourcePollTitle,"sourcePollTitle");function requestOverSocket(e,s,n,u,p=exports.SOURCE_MAX_BYTES){return new Promise(f=>{let E=!1,i;const r=l(a=>{E||(E=!0,clearTimeout(h),f(a))},"finish"),h=setTimeout(()=>{r({ok:!1,code:"ETIMEDOUT",message:`no answer within ${u} ms`}),i?.destroy()},u);h.unref?.();try{i=http.request({socketPath:e,path:s,method:"GET",agent:!1,headers:n},a=>{const _=[];let m=0;a.on("data",S=>{if(!E){if(m+=S.length,m>p){r({ok:!0,status:a.statusCode??0,body:"",truncated:!0}),a.destroy();return}_.push(S)}}),a.on("end",()=>r({ok:!0,status:a.statusCode??0,body:Buffer.concat(_).toString("utf8"),truncated:!1})),a.on("error",S=>r({ok:!1,code:S.code??"ERESPONSE",message:S.message}))}),i.on("error",a=>r({ok:!1,code:a.code??"EREQUEST",message:a.message})),i.end()}catch(a){r({ok:!1,code:a.code??"EREQUEST",message:a.message})}})}x(requestOverSocket,"requestOverSocket"),v(requestOverSocket,"requestOverSocket"),l(requestOverSocket,"requestOverSocket");const snippet=l(e=>e.replace(/\s+/g," ").trim().slice(0,160),"snippet");function createSourcePoller(e){const s=e.now??Date.now,n=e.timeoutMs??(0,exports.sourcesTimeoutMs)(),u=Math.max(1,e.maxConcurrent??(0,exports.sourcesConcurrency)()),p=e.socketOf??(t=>(0,discovery_1.pickSock)(t.pool,e.scanTtlMs??1500)),f=e.request??requestOverSocket,E=e.log??(t=>console.warn(t)),i=new WeakMap,r=new Map;let h=0,a;const _=l(t=>{const o=t.decl;let d=i.get(o);if(!d){d=parseSourceDecls(t.decl.sources),i.set(o,d);for(const c of d.problems)E(`[gateway] ${t.project}: cluster.sources \u2014 ${c}`)}const M=s();return(d.reportedAt===void 0||M-d.reportedAt>=exports.SOURCE_DECLARATION_REPORT_MS)&&(d.reportedAt=M,e.emit(t,d.problems.length?"warning":"info",d.problems.length?`sources ${t.project}: ${d.problems.length} cluster.sources entr${d.problems.length===1?"y":"ies"} not read \u2014 ${d.problems[0]}`:`sources ${t.project}: reads ${d.sources.map(c=>`${c.kind} "${c.name}"`).join(", ")}`,`source-declaration:${t.wsKey}:${t.project}`,buildSourceDeclarationRaw(t.project,d))),d},"declarationOf"),m=l(t=>{if(t.state==="starting")return{notRunning:"it is starting \u2014 read once it is up"};const o=p(t);return o?{socket:o}:{notRunning:t.state==="up"?"it has no live pod socket \u2014 no pod runs it now":"it is scaled to zero \u2014 the lens does not start a service to read a source"}},"reach"),S=l((t,o)=>{const d=t.last;if(!d)return;const M=s();if(!o&&t.reportedAt!==void 0&&M-t.reportedAt<t.reportEveryMs)return;t.reportedAt=M;const c=buildSourcePollRaw(t.svc.project,t.decl,d,t.reportEveryMs,t.failures,t.good),R=d.state==="unreachable"||d.state==="refused"||d.state==="malformed";e.emit(t.svc,R?"warning":"info",sourcePollTitle(c),`source:${t.svc.wsKey}:${t.svc.project}:${t.decl.name}`,c)},"report"),O=l(async t=>{const o=s(),d=t.decl.intervalSec*1e3;t.inFlight=!0,h+=1;const M=t.last?.state;try{const c=m(t.svc);if("notRunning"in c){t.last={state:"not-running",detail:c.notRunning,polledAt:o},t.failures=0,t.nextAt=o+d,t.reportEveryMs=exports.SOURCE_NOT_RUNNING_REPORT_MS,S(t,M!=="not-running");return}const R=e.serviceKey(t.svc),g=await f(c.socket,t.decl.path,{accept:"application/json","x-lensmcp-probe":`source:${t.decl.kind}`,...R?{"x-internal-token":R}:{}},n,exports.SOURCE_MAX_BYTES),$=Math.max(0,s()-o);if(g.ok===!1){const A=g.code==="ECONNREFUSED"||g.code==="ENOENT";t.last=A&&t.svc.state!=="up"?{state:"not-running",detail:`nothing listens on its pod socket \u2014 the service is ${t.svc.state}`,polledAt:o,durationMs:$}:{state:"unreachable",detail:g.code==="ETIMEDOUT"?g.message:`${g.code}: ${snippet(g.message)}`,polledAt:o,durationMs:$}}else if(g.status<200||g.status>299){const A=snippet(g.body);t.last={state:"refused",status:g.status,detail:`HTTP ${g.status}${A?`: ${A}`:""}`,polledAt:o,durationMs:$}}else if(g.truncated)t.last={state:"malformed",detail:`the snapshot is larger than ${exports.SOURCE_MAX_BYTES/1024} KiB`,polledAt:o,durationMs:$};else{let A;try{A=parseQueueSnapshot(JSON.parse(g.body))}catch{A={ok:!1,reason:`not JSON: ${snippet(g.body)||"(an empty body)"}`}}A.ok===!0?(t.good=keepQueueSnapshot(A.at,A.groups,s(),A.truncated),t.last={state:"current",polledAt:o,durationMs:$}):t.last={state:"malformed",detail:A.reason,polledAt:o,durationMs:$}}const C=t.last.state==="unreachable"||t.last.state==="refused"||t.last.state==="malformed";t.failures=C?t.failures+1:0;const N=C?sourceBackoffMs(d,t.failures):d;t.nextAt=o+N,t.reportEveryMs=t.last.state==="not-running"?exports.SOURCE_NOT_RUNNING_REPORT_MS:N,S(t,t.last.state!=="not-running"||M!=="not-running")}catch(c){t.last={state:"unreachable",detail:`the poll failed: ${snippet(c?.message??String(c))}`,polledAt:o},t.failures+=1,t.nextAt=o+sourceBackoffMs(d,t.failures),t.reportEveryMs=t.nextAt-o,S(t,!0)}finally{t.inFlight=!1,h-=1}},"pollOne"),y=l(async()=>{if(e.stopped?.())return;const t=new Set;for(const c of e.services())if(c.decl.sources!==void 0)for(const R of _(c).sources){const g=`${c.wsKey} ${c.project} ${R.name}`;t.add(g);const $=r.get(g);$?($.svc!==c||$.decl!==R)&&($.svc=c,$.decl=R):r.set(g,{svc:c,decl:R,nextAt:0,inFlight:!1,failures:0,reportEveryMs:R.intervalSec*1e3})}for(const[c,R]of[...r.entries()])!t.has(c)&&!R.inFlight&&r.delete(c);const o=s(),d=[...r.values()].filter(c=>!c.inFlight&&o>=c.nextAt).sort((c,R)=>c.nextAt-R.nextAt),M=[];for(const c of d){if(h>=u)break;M.push(O(c))}await Promise.all(M)},"tick");return{tick:y,start(t=1e3){return a&&clearInterval(a),a=setInterval(()=>{y().catch(o=>console.error("[gateway] source poll error:",o))},t),a.unref(),a},stop(){a&&clearInterval(a),a=void 0},snapshot(){const t=s();return[...r.values()].map(o=>{const d=o.last!==void 0&&t-o.last.polledAt>(0,exports.sourceStaleAfterMs)(o.reportEveryMs);return{wsKey:o.svc.wsKey,project:o.svc.project,name:o.decl.name,kind:o.decl.kind,path:o.decl.path,state:o.last?d?"stale":o.last.state:"never-polled",...o.last?.detail!==void 0?{detail:o.last.detail}:{},...o.last?.status!==void 0?{status:o.last.status}:{},...o.last?{polledAt:o.last.polledAt}:{},failures:o.failures,nextAt:o.nextAt,...o.good?{snapshot:o.good,ageMs:Math.max(0,t-o.good.receivedAt)}:{}}})}}}x(createSourcePoller,"createSourcePoller"),v(createSourcePoller,"createSourcePoller"),l(createSourcePoller,"createSourcePoller");
1
+ "use strict";var k=Object.defineProperty;var b=(e,t)=>k(e,"name",{value:t,configurable:!0});var C=Object.defineProperty,g=b((e,t)=>C(e,"name",{value:t,configurable:!0}),"g"),U=Object.defineProperty,u=g((e,t)=>U(e,"name",{value:t,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sourceSame=exports.sourceFull=exports.sourceStaleAfterMs=exports.SOURCE_BACKOFF_MAX_MS=exports.SOURCE_DECLARATION_REPORT_MS=exports.SOURCE_NOT_RUNNING_REPORT_MS=exports.SOURCE_MAX_PER_SERVICE=exports.SOURCE_MAX_GROUPS=exports.SOURCE_MAX_BYTES=exports.SOURCE_ERROR_MAX=exports.LOOP_MAX_INTERVAL_SEC=exports.SOURCE_FULL_REPORT_MS=exports.TRAFFIC_DEFAULT_RATE_LIMITED=exports.TRAFFIC_DEFAULT_P95_MS=exports.TRAFFIC_DEFAULT_FAILURE_PCT=exports.STORE_DEFAULT_ORPHAN_MIN=exports.STORE_DEFAULT_ERROR_RATE_PCT=exports.STORE_DEFAULT_QUOTA_PCT=exports.LOOP_DEFAULT_IDLE_MIN=exports.QUEUE_DEFAULT_STALLED_MIN=exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=exports.SOURCE_MIN_INTERVAL_SEC=exports.SOURCE_DEFAULT_INTERVAL_SEC=exports.TRAFFIC_DIRECTIONS=exports.LOOP_OUTCOMES=exports.SOURCE_SNAPSHOT_FIELD=exports.TRAFFIC_WIRE_SCHEMA=exports.STORE_WIRE_SCHEMA=exports.LOOP_WIRE_SCHEMA=exports.QUEUE_WIRE_SCHEMA=exports.SOURCE_KINDS_READ=exports.SOURCE_KINDS=exports.sourcesConcurrency=exports.sourcesTimeoutMs=exports.sourcesEnabled=void 0,exports.sourceBackoffMs=sourceBackoffMs,exports.parseSourceDecls=parseSourceDecls,exports.parseQueueSnapshot=parseQueueSnapshot,exports.parseLoopSnapshot=parseLoopSnapshot,exports.parseStoreSnapshot=parseStoreSnapshot,exports.parseTrafficSnapshot=parseTrafficSnapshot,exports.sourceDigest=sourceDigest,exports.keepQueueSnapshot=keepQueueSnapshot,exports.keepLoopSnapshot=keepLoopSnapshot,exports.keepStoreSnapshot=keepStoreSnapshot,exports.keepTrafficSnapshot=keepTrafficSnapshot,exports.buildSourcePollRaw=buildSourcePollRaw,exports.buildSourceDeclarationRaw=buildSourceDeclarationRaw,exports.sourcePollTitle=sourcePollTitle,exports.requestOverSocket=requestOverSocket,exports.createSourcePoller=createSourcePoller;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),http=tslib_1.__importStar(require("node:http")),discovery_1=require("./discovery"),readEnv=u(e=>process.env[e],"readEnv"),envInt=u((e,t,s)=>{const n=Number(readEnv(e));return Number.isInteger(n)&&n>=s?n:t},"envInt"),sourcesEnabled=u(()=>readEnv("LENSMCP_SOURCES")!=="0","sourcesEnabled");exports.sourcesEnabled=sourcesEnabled;const sourcesTimeoutMs=u(()=>envInt("LENSMCP_SOURCES_TIMEOUT_MS",3e3,100),"sourcesTimeoutMs");exports.sourcesTimeoutMs=sourcesTimeoutMs;const sourcesConcurrency=u(()=>envInt("LENSMCP_SOURCES_CONCURRENCY",4,1),"sourcesConcurrency");exports.sourcesConcurrency=sourcesConcurrency,exports.SOURCE_KINDS=["queue","loop","store","traffic"],exports.SOURCE_KINDS_READ=["queue","loop","store","traffic"],exports.QUEUE_WIRE_SCHEMA="lensmcp.queue/v1",exports.LOOP_WIRE_SCHEMA="lensmcp.loop/v1",exports.STORE_WIRE_SCHEMA="lensmcp.store/v1",exports.TRAFFIC_WIRE_SCHEMA="lensmcp.traffic/v1",exports.SOURCE_SNAPSHOT_FIELD={queue:"snapshot",loop:"loop",store:"store",traffic:"traffic"},exports.LOOP_OUTCOMES=["ok","failed","skipped"],exports.TRAFFIC_DIRECTIONS=["inbound","outbound"],exports.SOURCE_DEFAULT_INTERVAL_SEC=10,exports.SOURCE_MIN_INTERVAL_SEC=2,exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=10,exports.QUEUE_DEFAULT_STALLED_MIN=15,exports.LOOP_DEFAULT_IDLE_MIN=10,exports.STORE_DEFAULT_QUOTA_PCT=90,exports.STORE_DEFAULT_ERROR_RATE_PCT=5,exports.STORE_DEFAULT_ORPHAN_MIN=60,exports.TRAFFIC_DEFAULT_FAILURE_PCT=5,exports.TRAFFIC_DEFAULT_P95_MS=2e3,exports.TRAFFIC_DEFAULT_RATE_LIMITED=1,exports.SOURCE_FULL_REPORT_MS=6e4,exports.LOOP_MAX_INTERVAL_SEC=86400,exports.SOURCE_ERROR_MAX=500,exports.SOURCE_MAX_BYTES=256*1024,exports.SOURCE_MAX_GROUPS=500,exports.SOURCE_MAX_PER_SERVICE=16,exports.SOURCE_NOT_RUNNING_REPORT_MS=6e4,exports.SOURCE_DECLARATION_REPORT_MS=5*6e4,exports.SOURCE_BACKOFF_MAX_MS=6e4;const sourceStaleAfterMs=u(e=>Math.max(3e4,3*Math.max(0,e)),"sourceStaleAfterMs");exports.sourceStaleAfterMs=sourceStaleAfterMs;function sourceBackoffMs(e,t){return t<=1?e:Math.min(e*2**Math.min(t-1,10),Math.max(e,exports.SOURCE_BACKOFF_MAX_MS))}b(sourceBackoffMs,"sourceBackoffMs"),g(sourceBackoffMs,"sourceBackoffMs"),u(sourceBackoffMs,"sourceBackoffMs");const isPolled=u(e=>e.path!==void 0,"isPolled"),ENTRY_KEYS={queue:["kind","name","path","intervalSec","thresholds"],loop:["kind","name","path","intervalSec","thresholds","feeds"],store:["kind","name","path","intervalSec","thresholds"],traffic:["kind","name","path","intervalSec","thresholds"]},finite=u(e=>typeof e=="number"&&Number.isFinite(e),"finite"),MINUTES={ok:u(e=>finite(e)&&e>0&&e<=10080,"ok"),must:"must be minutes, more than 0 and at most 10080"},PERCENT={ok:u(e=>finite(e)&&e>0&&e<=100,"ok"),must:"must be a percent, more than 0 and at most 100"},MILLISECONDS={ok:u(e=>finite(e)&&e>0&&e<=6e5,"ok"),must:"must be milliseconds, more than 0 and at most 600000"},COUNT={ok:u(e=>Number.isInteger(e)&&e>=1&&e<=1e6,"ok"),must:"must be a count, a whole number from 1 to 1000000"},THRESHOLDS={queue:{oldestWaitMin:{def:exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN,...MINUTES},stalledMin:{def:exports.QUEUE_DEFAULT_STALLED_MIN,...MINUTES}},loop:{idleMin:{def:exports.LOOP_DEFAULT_IDLE_MIN,...MINUTES}},store:{quotaPct:{def:exports.STORE_DEFAULT_QUOTA_PCT,...PERCENT},errorRatePct:{def:exports.STORE_DEFAULT_ERROR_RATE_PCT,...PERCENT},orphanMin:{def:exports.STORE_DEFAULT_ORPHAN_MIN,...MINUTES}},traffic:{failurePct:{def:exports.TRAFFIC_DEFAULT_FAILURE_PCT,...PERCENT},p95Ms:{def:exports.TRAFFIC_DEFAULT_P95_MS,...MILLISECONDS},rateLimited:{def:exports.TRAFFIC_DEFAULT_RATE_LIMITED,...COUNT}}},PATH_RE=/^\/(?!\/)[^\s#\u0000-\u001f\u007f]{0,511}$/,NAME_RE=/^[^\u0000-\u001f\u007f]{1,100}$/,isObj=u(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isObj");function parseSourceDecls(e){const t=[],s=[],n=[];if(e===void 0)return{sources:t,problems:s};if(!Array.isArray(e))return{sources:t,problems:["`sources` is not a list of { kind, name, path } entries \u2014 no source is read"]};e.forEach((i,l)=>{const S=`sources[${l}]`;if(!isObj(i)){s.push(`${S} is not an object \u2014 not read`);return}const d=typeof i.name=="string"&&i.name.trim()?`${S} "${i.name}"`:S,r=i.kind;if(typeof r!="string"||!exports.SOURCE_KINDS_READ.includes(r)){s.push(`${d}: kind ${JSON.stringify(r??null)} is not a source kind (${exports.SOURCE_KINDS.join(", ")}) \u2014 not read`);return}const a=r,p=Object.keys(i).filter(_=>!ENTRY_KEYS[a].includes(_));if(p.length){s.push(`${d}: unknown key${p.length===1?"":"s"} ${p.map(_=>JSON.stringify(_)).join(", ")} for a ${a} source (known: ${ENTRY_KEYS[a].join(", ")}) \u2014 not read`);return}const R=i.name;if(typeof R!="string"||!R.trim()||!NAME_RE.test(R)){s.push(`${S}: name ${JSON.stringify(R??null)} must be 1\u2013100 characters, not blank \u2014 not read`);return}if(t.some(_=>_.name===R)){s.push(`${d}: the name is declared twice on this service \u2014 read once`);return}const h=i.path;if(h===void 0&&a!=="traffic"&&a!=="loop"){s.push(`${d}: a ${a} source needs a path \u2014 the route on the service that answers "lensmcp.${a}/v1" (only a loop or a traffic source may go without one, as push-only) \u2014 not read`);return}if(h!==void 0&&(typeof h!="string"||!PATH_RE.test(h)||h.split("?")[0].split("/").includes(".."))){s.push(`${d}: path ${JSON.stringify(h)} must be a route on the service, starting with "/" (e.g. "/api/dev/lens/backlog") \u2014 not read`);return}const m=i.intervalSec;if(m!==void 0&&!(typeof m=="number"&&Number.isInteger(m)&&m>=exports.SOURCE_MIN_INTERVAL_SEC&&m<=3600)){s.push(`${d}: intervalSec ${JSON.stringify(m)} must be a whole number of seconds from ${exports.SOURCE_MIN_INTERVAL_SEC} to 3600 \u2014 not read`);return}const A=i.thresholds;if(A!==void 0&&!isObj(A)){s.push(`${d}: thresholds must be an object \u2014 not read`);return}const I=isObj(A)?A:{},x=THRESHOLDS[a],o=Object.keys(x),c=Object.keys(I).filter(_=>!o.includes(_));if(c.length){s.push(`${d}: unknown threshold${c.length===1?"":"s"} ${c.map(_=>JSON.stringify(_)).join(", ")} for a ${a} source (known: ${o.join(", ")}) \u2014 not read`);return}const f=o.find(_=>I[_]!==void 0&&!x[_].ok(I[_]));if(f){s.push(`${d}: thresholds.${f} ${JSON.stringify(I[f])} ${x[f].must} \u2014 not read`);return}if(t.length>=exports.SOURCE_MAX_PER_SERVICE){s.push(`${d}: more than ${exports.SOURCE_MAX_PER_SERVICE} sources on one service \u2014 not read`);return}const O={};for(const _ of o)O[_]=I[_]??x[_].def;const E={kind:a,name:R,...h!==void 0?{path:h}:{},intervalSec:m??exports.SOURCE_DEFAULT_INTERVAL_SEC,thresholds:O};t.push(E),a==="loop"&&i.feeds!==void 0&&n.push({decl:E,label:d,feeds:i.feeds})});for(const{decl:i,label:l,feeds:S}of n)typeof S!="string"||!S.trim()?s.push(`${l}: feeds ${JSON.stringify(S)} must be the name of a queue source on this service \u2014 read without it`):t.some(d=>d.kind==="queue"&&d.name===S)?i.feeds=S:s.push(`${l}: feeds ${JSON.stringify(S)} names no queue source on this service \u2014 read without it`);return{sources:t,problems:s}}b(parseSourceDecls,"parseSourceDecls"),g(parseSourceDecls,"parseSourceDecls"),u(parseSourceDecls,"parseSourceDecls");const ISO_RE=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})$/,isIso=u(e=>typeof e=="string"&&ISO_RE.test(e)&&!Number.isNaN(Date.parse(e)),"isIso"),isCount=u(e=>typeof e=="number"&&Number.isSafeInteger(e)&&e>=0,"isCount"),notSchema=u((e,t)=>`schema is ${e===void 0?"absent":JSON.stringify(e).slice(0,60)}, not "${t}"`,"notSchema"),keyWrong=u((e,t,s)=>{if(typeof e!="string"||e.length===0||e.length>200)return`${t}.key must be a string of 1\u2013200 characters`;if(s.has(e))return`${t}.key ${JSON.stringify(e)} appears twice`;s.add(e)},"keyWrong"),labelsWrong=u((e,t)=>{if(e===void 0)return;if(!isObj(e))return`${t}.labels must be an object of strings`;const s=Object.entries(e);if(s.length>16)return`${t}.labels has more than 16 entries`;for(const[n,i]of s)if(n.length===0||n.length>64||typeof i!="string"||i.length>200)return`${t}.labels.${n.slice(0,64)} must be a string of at most 200 characters`},"labelsWrong"),overCap=u((e,t)=>`${e} ${t} \u2014 at most ${exports.SOURCE_MAX_GROUPS}; cap them and say \`truncated: { ${t}: <dropped> }\``,"overCap");function parseQueueSnapshot(e){const t=u(d=>({ok:!1,reason:d}),"fail");if(!isObj(e))return t("the snapshot is not a JSON object");if(e.schema!==exports.QUEUE_WIRE_SCHEMA)return t(notSchema(e.schema,exports.QUEUE_WIRE_SCHEMA));const s=e.at;if(!isIso(s))return t("`at` is not an ISO time");const n=e.truncated;if(n!==void 0&&!(isObj(n)&&isCount(n.groups)))return t("`truncated` must be { groups: <how many were dropped> }, or absent");const i=e.groups;if(!Array.isArray(i))return t("`groups` is not a list");if(i.length>exports.SOURCE_MAX_GROUPS)return t(overCap(i.length,"groups"));const l=new Set,S=[];for(let d=0;d<i.length;d++){const r=i[d],a=`groups[${d}]`;if(!isObj(r))return t(`${a} is not an object`);const p=keyWrong(r.key,a,l);if(p)return t(p);if(!isCount(r.pending))return t(`${a}.pending must be a whole number \u2265 0`);for(const A of["inFlight","failed","completedInWindow"])if(r[A]!==void 0&&!isCount(r[A]))return t(`${a}.${A} must be a whole number \u2265 0, or absent`);const R=r.oldestPendingAt;if(R!=null&&!isIso(R))return t(`${a}.oldestPendingAt must be an ISO time, null, or absent`);const h=r.labels,m=labelsWrong(h,a);if(m)return t(m);S.push({key:r.key,...h!==void 0?{labels:h}:{},pending:r.pending,...r.inFlight!==void 0?{inFlight:r.inFlight}:{},...r.failed!==void 0?{failed:r.failed}:{},...R!==void 0?{oldestPendingAt:R}:{},...r.completedInWindow!==void 0?{completedInWindow:r.completedInWindow}:{}})}return{ok:!0,at:s,groups:S,...n!==void 0?{truncated:{groups:n.groups}}:{}}}b(parseQueueSnapshot,"parseQueueSnapshot"),g(parseQueueSnapshot,"parseQueueSnapshot"),u(parseQueueSnapshot,"parseQueueSnapshot");const LOOP_COUNTS=["lastDurationMs","lastItems","runsInWindow","itemsInWindow","failuresInWindow","windowSec"],cutText=u((e,t)=>{if(e.length<=t)return e;const s=t-1,n=e.charCodeAt(s-1);return`${e.slice(0,n>=55296&&n<=56319?s-1:s)}\u2026`},"cutText");function parseLoopSnapshot(e){const t=u(a=>({ok:!1,reason:a}),"fail");if(!isObj(e))return t("the snapshot is not a JSON object");if(e.schema!==exports.LOOP_WIRE_SCHEMA)return t(notSchema(e.schema,exports.LOOP_WIRE_SCHEMA));const s=e.at;if(!isIso(s))return t("`at` is not an ISO time");const n=e.intervalSec;if(!(typeof n=="number"&&Number.isFinite(n)&&n>0&&n<=exports.LOOP_MAX_INTERVAL_SEC))return t(`\`intervalSec\` must be the loop's own period in seconds, more than 0 and at most ${exports.LOOP_MAX_INTERVAL_SEC}`);const i=e.lastRunAt;if(i===void 0)return t("`lastRunAt` is absent \u2014 say null if the loop has not run since the service started");if(i!==null&&!isIso(i))return t("`lastRunAt` must be an ISO time, or null if the loop has not run since the service started");for(const a of LOOP_COUNTS)if(e[a]!==void 0&&!isCount(e[a]))return t(`\`${a}\` must be a whole number \u2265 0, or absent`);const l=e.lastOutcome;if(l!==void 0&&!exports.LOOP_OUTCOMES.includes(l))return t(`\`lastOutcome\` must be ${exports.LOOP_OUTCOMES.join(", ")}, or absent`);const S=e.nextRunAt;if(S!=null&&!isIso(S))return t("`nextRunAt` must be an ISO time, null, or absent");const d=e.lastError;if(d!==void 0&&typeof d!="string")return t("`lastError` must be a string, or absent");const r=u(a=>e[a]!==void 0?{[a]:e[a]}:{},"count");return{ok:!0,loop:{at:s,intervalSec:n,lastRunAt:i,...r("lastDurationMs"),...l!==void 0?{lastOutcome:l}:{},...r("lastItems"),...S!==void 0?{nextRunAt:S}:{},...r("runsInWindow"),...r("itemsInWindow"),...r("failuresInWindow"),...r("windowSec"),...d!==void 0?{lastError:cutText(d,exports.SOURCE_ERROR_MAX)}:{}}}}b(parseLoopSnapshot,"parseLoopSnapshot"),g(parseLoopSnapshot,"parseLoopSnapshot"),u(parseLoopSnapshot,"parseLoopSnapshot");const STORE_COUNTS=["opsInWindow","writesInWindow","deletesInWindow","errorsInWindow","windowSec"];function parseStoreSnapshot(e){const t=u(p=>({ok:!1,reason:p}),"fail");if(!isObj(e))return t("the snapshot is not a JSON object");if(e.schema!==exports.STORE_WIRE_SCHEMA)return t(notSchema(e.schema,exports.STORE_WIRE_SCHEMA));const s=e.at;if(!isIso(s))return t("`at` is not an ISO time");for(const p of["objects","bytes"])if(!isCount(e[p]))return t(`\`${p}\` must be a whole number \u2265 0`);const n=e.quotaBytes;if(n!=null&&!isCount(n))return t("`quotaBytes` must be a whole number \u2265 0, null when it has no quota, or absent");for(const p of STORE_COUNTS)if(e[p]!==void 0&&!isCount(e[p]))return t(`\`${p}\` must be a whole number \u2265 0, or absent`);const i=e.lastWriteAt;if(i!=null&&!isIso(i))return t("`lastWriteAt` must be an ISO time, null, or absent");const l=e.lastError;if(l!==void 0&&typeof l!="string")return t("`lastError` must be a string, or absent");const S=e.truncated;if(S!==void 0&&!(isObj(S)&&isCount(S.groups)))return t("`truncated` must be { groups: <how many were dropped> }, or absent");const d=e.groups;let r;if(d!==void 0){if(!Array.isArray(d))return t("`groups` must be a list, or absent");if(d.length>exports.SOURCE_MAX_GROUPS)return t(overCap(d.length,"groups"));const p=new Set;r=[];for(let R=0;R<d.length;R++){const h=d[R],m=`groups[${R}]`;if(!isObj(h))return t(`${m} is not an object`);const A=keyWrong(h.key,m,p);if(A)return t(A);for(const o of["objects","bytes"])if(!isCount(h[o]))return t(`${m}.${o} must be a whole number \u2265 0`);const I=h.labels,x=labelsWrong(I,m);if(x)return t(x);r.push({key:h.key,...I!==void 0?{labels:I}:{},objects:h.objects,bytes:h.bytes})}}const a=u(p=>e[p]!==void 0?{[p]:e[p]}:{},"count");return{ok:!0,store:{at:s,objects:e.objects,bytes:e.bytes,...n!==void 0?{quotaBytes:n}:{},...a("opsInWindow"),...a("writesInWindow"),...a("deletesInWindow"),...a("errorsInWindow"),...a("windowSec"),...i!==void 0?{lastWriteAt:i}:{},...l!==void 0?{lastError:cutText(l,exports.SOURCE_ERROR_MAX)}:{},...r?{groups:r}:{},...S!==void 0?{truncated:{groups:S.groups}}:{}}}}b(parseStoreSnapshot,"parseStoreSnapshot"),g(parseStoreSnapshot,"parseStoreSnapshot"),u(parseStoreSnapshot,"parseStoreSnapshot");const TRAFFIC_COUNTS=["messagesInWindow","failuresInWindow","rateLimitedInWindow"];function parseTrafficSnapshot(e){const t=u(r=>({ok:!1,reason:r}),"fail");if(!isObj(e))return t("the snapshot is not a JSON object");if(e.schema!==exports.TRAFFIC_WIRE_SCHEMA)return t(notSchema(e.schema,exports.TRAFFIC_WIRE_SCHEMA));const s=e.at;if(!isIso(s))return t("`at` is not an ISO time");const n=e.windowSec;if(n!==void 0&&!isCount(n))return t("`windowSec` must be a whole number \u2265 0, or absent");const i=e.truncated;if(i!==void 0&&!(isObj(i)&&isCount(i.channels)))return t("`truncated` must be { channels: <how many were dropped> }, or absent");const l=e.channels;if(!Array.isArray(l))return t("`channels` is not a list");if(l.length>exports.SOURCE_MAX_GROUPS)return t(overCap(l.length,"channels"));const S=new Set,d=[];for(let r=0;r<l.length;r++){const a=l[r],p=`channels[${r}]`;if(!isObj(a))return t(`${p} is not an object`);const R=keyWrong(a.key,p,S);if(R)return t(R);const h=a.direction;if(h!==void 0&&!exports.TRAFFIC_DIRECTIONS.includes(h))return t(`${p}.direction must be ${exports.TRAFFIC_DIRECTIONS.join(", ")}, or absent`);const m=a.counterpart;if(m!==void 0&&!(typeof m=="string"&&m.length>0&&m.length<=200))return t(`${p}.counterpart must be a string of 1\u2013200 characters, or absent`);for(const O of TRAFFIC_COUNTS)if(a[O]!==void 0&&!isCount(a[O]))return t(`${p}.${O} must be a whole number \u2265 0, or absent`);const A=a.p95Ms;if(A!==void 0&&!(finite(A)&&A>=0))return t(`${p}.p95Ms must be milliseconds \u2265 0, or absent`);const I=a.lastAt;if(I!=null&&!isIso(I))return t(`${p}.lastAt must be an ISO time, null, or absent`);const x=a.lastError;if(x!==void 0&&typeof x!="string")return t(`${p}.lastError must be a string, or absent`);const o=a.labels,c=labelsWrong(o,p);if(c)return t(c);const f=u(O=>a[O]!==void 0?{[O]:a[O]}:{},"count");d.push({key:a.key,...o!==void 0?{labels:o}:{},...h!==void 0?{direction:h}:{},...m!==void 0?{counterpart:m}:{},...f("messagesInWindow"),...f("failuresInWindow"),...f("rateLimitedInWindow"),...A!==void 0?{p95Ms:A}:{},...I!==void 0?{lastAt:I}:{},...x!==void 0?{lastError:cutText(x,exports.SOURCE_ERROR_MAX)}:{}})}return{ok:!0,traffic:{at:s,...n!==void 0?{windowSec:n}:{},channels:d,...i!==void 0?{truncated:{channels:i.channels}}:{}}}}b(parseTrafficSnapshot,"parseTrafficSnapshot"),g(parseTrafficSnapshot,"parseTrafficSnapshot"),u(parseTrafficSnapshot,"parseTrafficSnapshot");const sourceFull=u((e,t)=>({[exports.SOURCE_SNAPSHOT_FIELD[e]]:t}),"sourceFull");exports.sourceFull=sourceFull;const declRaw=u(e=>({sourceKind:e.kind,name:e.name,...e.path!==void 0?{path:e.path}:{},intervalSec:e.intervalSec,thresholds:{...e.thresholds},...e.feeds!==void 0?{feeds:e.feeds}:{}}),"declRaw"),canonical=u(e=>{if(Array.isArray(e))return e.map(canonical);if(!isObj(e))return e;const t={};for(const s of Object.keys(e).sort())t[s]=canonical(e[s]);return t},"canonical");function sourceDigest(e){return(0,node_crypto_1.createHash)("sha1").update(JSON.stringify(canonical(e))??"null").digest("hex").slice(0,16)}b(sourceDigest,"sourceDigest"),g(sourceDigest,"sourceDigest"),u(sourceDigest,"sourceDigest");const oldestOf=u(e=>e.pending>0&&e.oldestPendingAt?Date.parse(e.oldestPendingAt):Number.POSITIVE_INFINITY,"oldestOf");function keepQueueSnapshot(e,t,s,n){const i={groups:t.length,pending:0,inFlight:0,failed:0};let l;for(const r of t)i.pending+=r.pending,i.inFlight+=r.inFlight??0,i.failed+=r.failed??0,r.completedInWindow!==void 0&&(l=(l??0)+r.completedInWindow);l!==void 0&&(i.completedInWindow=l);const S=[...t].sort((r,a)=>oldestOf(r)-oldestOf(a)||a.pending-r.pending||(r.key<a.key?-1:r.key>a.key?1:0)).map(({key:r,...a})=>({group:r,...a})),d=n?{groups:n.groups}:void 0;return{at:e,receivedAt:s,totals:i,groups:S,...d?{truncated:d}:{},digest:sourceDigest({groups:S,truncated:d??null})}}b(keepQueueSnapshot,"keepQueueSnapshot"),g(keepQueueSnapshot,"keepQueueSnapshot"),u(keepQueueSnapshot,"keepQueueSnapshot");function keepLoopSnapshot(e,t){const{at:s,...n}=e;return{at:s,receivedAt:t,digest:sourceDigest(n),...n}}b(keepLoopSnapshot,"keepLoopSnapshot"),g(keepLoopSnapshot,"keepLoopSnapshot"),u(keepLoopSnapshot,"keepLoopSnapshot");const byKey=u((e,t)=>e<t?-1:e>t?1:0,"byKey");function keepStoreSnapshot(e,t){const{at:s,groups:n,truncated:i,...l}=e,S=n?[...n].sort((r,a)=>a.bytes-r.bytes||a.objects-r.objects||byKey(r.key,a.key)).map(({key:r,...a})=>({group:r,...a})):void 0,d={...l,...S?{groups:S}:{},...i?{truncated:{groups:i.groups}}:{}};return{at:s,receivedAt:t,digest:sourceDigest(d),...d}}b(keepStoreSnapshot,"keepStoreSnapshot"),g(keepStoreSnapshot,"keepStoreSnapshot"),u(keepStoreSnapshot,"keepStoreSnapshot");const messagesOf=u(e=>e.messagesInWindow??-1,"messagesOf");function keepTrafficSnapshot(e,t){const{at:s,channels:n,truncated:i,...l}=e,S=[...n].sort((r,a)=>messagesOf(a)-messagesOf(r)||byKey(r.key,a.key)).map(({key:r,...a})=>({channel:r,...a})),d={...l,channels:S,...i?{truncated:{channels:i.channels}}:{}};return{at:s,receivedAt:t,digest:sourceDigest(d),...d}}b(keepTrafficSnapshot,"keepTrafficSnapshot"),g(keepTrafficSnapshot,"keepTrafficSnapshot"),u(keepTrafficSnapshot,"keepTrafficSnapshot");const sourceSame=u(e=>({digest:e.digest,at:e.at,receivedAt:e.receivedAt}),"sourceSame");exports.sourceSame=sourceSame;function buildSourcePollRaw(e,t,s,n,i,l){return{kind:"source-poll",provenance:"reported",project:e,...declRaw(t),state:s.state,...s.detail!==void 0?{detail:s.detail}:{},...s.status!==void 0?{status:s.status}:{},polledAt:s.polledAt,...s.durationMs!==void 0?{durationMs:s.durationMs}:{},reportEveryMs:n,...i>0?{failures:i}:{},...l??{}}}b(buildSourcePollRaw,"buildSourcePollRaw"),g(buildSourcePollRaw,"buildSourcePollRaw"),u(buildSourcePollRaw,"buildSourcePollRaw");function buildSourceDeclarationRaw(e,t){return{kind:"source-declaration",project:e,sources:t.sources.map(declRaw),problems:[...t.problems]}}b(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),g(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),u(buildSourceDeclarationRaw,"buildSourceDeclarationRaw");const span=u(e=>{const t=Math.round(e/6e4);return e<6e4?`${Math.max(0,Math.round(e/1e3))}s`:t<120?`${t}m`:`${Math.floor(t/60)}h${t%60?` ${t%60}m`:""}`},"span"),loopWords=u(e=>`${e.lastRunAt===null?"has not run since the service started":`last run ${span(Math.max(0,Date.parse(e.at)-Date.parse(e.lastRunAt)))} before it was measured`}${e.lastOutcome?`, ${e.lastOutcome}`:""}${e.lastItems!==void 0?`, ${e.lastItems} item${e.lastItems===1?"":"s"}`:""}`,"loopWords"),plural=u((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),byteSize=u(e=>{const t=["B","kB","MB","GB","TB","PB"];let s=e,n=0;for(;s>=999.5&&n<t.length-1;)s/=1e3,n+=1;return n===0?`${e} B`:`${s<9.95?s.toFixed(1):Math.round(s)} ${t[n]}`},"byteSize"),storeWords=u(e=>{const t=typeof e.quotaBytes=="number"&&e.quotaBytes>0?` of ${byteSize(e.quotaBytes)} (${Math.round(e.bytes/e.quotaBytes*1e3)/10}%)`:"",s=e.errorsInWindow?`, ${plural(e.errorsInWindow,"error")}${e.windowSec?` in ${span(e.windowSec*1e3)}`:""}`:"";return`${plural(e.objects,"object")}, ${byteSize(e.bytes)}${t}${s}`},"storeWords"),trafficWords=u(e=>{let t,s=0,n=0;for(const l of e.channels)l.messagesInWindow!==void 0&&(t=(t??0)+l.messagesInWindow),s+=l.failuresInWindow??0,n+=l.rateLimitedInWindow??0;const i=t!==void 0?`, ${plural(t,"message")}${e.windowSec?` in ${span(e.windowSec*1e3)}`:""}`:"";return`${plural(e.channels.length,"channel")}${i}${s?`, ${s} failed`:""}${n?`, ${n} rate-limited`:""}`},"trafficWords");function sourcePollTitle(e,t){const s=`${e.sourceKind} ${e.project}/${e.name} (reported)`,n=t??e[exports.SOURCE_SNAPSHOT_FIELD[e.sourceKind]];if(e.state!=="current"||!n)return`${s}: ${e.state}${e.detail?` \u2014 ${e.detail}`:""}`;if(e.sourceKind==="loop")return`${s}: ${loopWords(n)}`;if(e.sourceKind==="store")return`${s}: ${storeWords(n)}`;if(e.sourceKind==="traffic")return`${s}: ${trafficWords(n)}`;const i=n,l=i.groups.find(d=>d.pending>0&&d.oldestPendingAt),S=l?.oldestPendingAt?Date.parse(i.at)-Date.parse(l.oldestPendingAt):void 0;return`${s}: ${i.totals.pending} pending in ${i.totals.groups} group${i.totals.groups===1?"":"s"}${S!==void 0?`, oldest waiting ${span(Math.max(0,S))}`:""}`}b(sourcePollTitle,"sourcePollTitle"),g(sourcePollTitle,"sourcePollTitle"),u(sourcePollTitle,"sourcePollTitle");function requestOverSocket(e,t,s,n,i=exports.SOURCE_MAX_BYTES){return new Promise(l=>{let S=!1,d;const r=u(p=>{S||(S=!0,clearTimeout(a),l(p))},"finish"),a=setTimeout(()=>{r({ok:!1,code:"ETIMEDOUT",message:`no answer within ${n} ms`}),d?.destroy()},n);a.unref?.();try{d=http.request({socketPath:e,path:t,method:"GET",agent:!1,headers:s},p=>{const R=[];let h=0;p.on("data",m=>{if(!S){if(h+=m.length,h>i){r({ok:!0,status:p.statusCode??0,body:"",truncated:!0}),p.destroy();return}R.push(m)}}),p.on("end",()=>r({ok:!0,status:p.statusCode??0,body:Buffer.concat(R).toString("utf8"),truncated:!1})),p.on("error",m=>r({ok:!1,code:m.code??"ERESPONSE",message:m.message}))}),d.on("error",p=>r({ok:!1,code:p.code??"EREQUEST",message:p.message})),d.end()}catch(p){r({ok:!1,code:p.code??"EREQUEST",message:p.message})}})}b(requestOverSocket,"requestOverSocket"),g(requestOverSocket,"requestOverSocket"),u(requestOverSocket,"requestOverSocket");const snippet=u(e=>e.replace(/\s+/g," ").trim().slice(0,160),"snippet");function readSnapshot(e,t,s){let n;try{n=JSON.parse(t)}catch{return{ok:!1,reason:`not JSON: ${snippet(t)||"(an empty body)"}`}}if(e==="loop"){const l=parseLoopSnapshot(n);return l.ok===!0?{ok:!0,kept:keepLoopSnapshot(l.loop,s)}:l}if(e==="store"){const l=parseStoreSnapshot(n);return l.ok===!0?{ok:!0,kept:keepStoreSnapshot(l.store,s)}:l}if(e==="traffic"){const l=parseTrafficSnapshot(n);return l.ok===!0?{ok:!0,kept:keepTrafficSnapshot(l.traffic,s)}:l}const i=parseQueueSnapshot(n);return i.ok===!0?{ok:!0,kept:keepQueueSnapshot(i.at,i.groups,s,i.truncated)}:i}b(readSnapshot,"readSnapshot"),g(readSnapshot,"readSnapshot"),u(readSnapshot,"readSnapshot");function createSourcePoller(e){const t=e.now??Date.now,s=e.timeoutMs??(0,exports.sourcesTimeoutMs)(),n=Math.max(1,e.maxConcurrent??(0,exports.sourcesConcurrency)()),i=e.socketOf??(o=>(0,discovery_1.pickSock)(o.pool,e.scanTtlMs??1500)),l=e.request??requestOverSocket,S=e.log??(o=>console.warn(o)),d=new WeakMap,r=new Map;let a=0,p;const R=u(o=>{const c=o.decl;let f=d.get(c);if(!f){f=parseSourceDecls(o.decl.sources),d.set(c,f);for(const E of f.problems)S(`[gateway] ${o.project}: cluster.sources \u2014 ${E}`)}const O=t();return(f.reportedAt===void 0||O-f.reportedAt>=exports.SOURCE_DECLARATION_REPORT_MS)&&(f.reportedAt=O,e.emit(o,f.problems.length?"warning":"info",f.problems.length?`sources ${o.project}: ${f.problems.length} cluster.sources entr${f.problems.length===1?"y":"ies"} not read \u2014 ${f.problems[0]}`:`sources ${o.project}: reads ${f.sources.map(E=>`${E.kind} "${E.name}"${isPolled(E)?"":" (push-only)"}`).join(", ")}`,`source-declaration:${o.wsKey}:${o.project}`,buildSourceDeclarationRaw(o.project,f))),f},"declarationOf"),h=u(o=>{if(o.state==="starting")return{notRunning:"it is starting \u2014 read once it is up"};const c=i(o);return c?{socket:c}:{notRunning:o.state==="up"?"it has no live pod socket \u2014 no pod runs it now":"it is scaled to zero \u2014 the lens does not start a service to read a source"}},"reach"),m=u((o,c)=>{const f=o.good;if(f)return o.full&&o.full.digest===f.digest&&c-o.full.at<exports.SOURCE_FULL_REPORT_MS?{same:(0,exports.sourceSame)(f)}:(o.full={digest:f.digest,at:c},(0,exports.sourceFull)(o.decl.kind,f))},"carried"),A=u((o,c)=>{const f=o.last;if(!f)return;const O=t();if(!c&&o.reportedAt!==void 0&&O-o.reportedAt<o.reportEveryMs)return;o.reportedAt=O;const E=buildSourcePollRaw(o.svc.project,o.decl,f,o.reportEveryMs,o.failures,m(o,O)),_=f.state==="unreachable"||f.state==="refused"||f.state==="malformed";e.emit(o.svc,_?"warning":"info",sourcePollTitle(E,o.good),`source:${o.svc.wsKey}:${o.svc.project}:${o.decl.name}`,E)},"report"),I=u(async o=>{const c=t(),f=o.decl.intervalSec*1e3;o.inFlight=!0,a+=1;const O=o.last?.state;try{const E=h(o.svc);if("notRunning"in E){o.last={state:"not-running",detail:E.notRunning,polledAt:c},o.failures=0,o.nextAt=c+f,o.reportEveryMs=exports.SOURCE_NOT_RUNNING_REPORT_MS,A(o,O!=="not-running");return}const _=e.serviceKey(o.svc),T=await l(E.socket,o.decl.path,{accept:"application/json","x-lensmcp-probe":`source:${o.decl.kind}`,..._?{"x-internal-token":_}:{}},s,exports.SOURCE_MAX_BYTES),$=Math.max(0,t()-c);if(T.ok===!1){const v=T.code==="ECONNREFUSED"||T.code==="ENOENT";o.last=v&&o.svc.state!=="up"?{state:"not-running",detail:`nothing listens on its pod socket \u2014 the service is ${o.svc.state}`,polledAt:c,durationMs:$}:{state:"unreachable",detail:T.code==="ETIMEDOUT"?T.message:`${T.code}: ${snippet(T.message)}`,polledAt:c,durationMs:$}}else if(T.status<200||T.status>299){const v=snippet(T.body);o.last={state:"refused",status:T.status,detail:`HTTP ${T.status}${v?`: ${v}`:""}`,polledAt:c,durationMs:$}}else if(T.truncated)o.last={state:"malformed",detail:`the snapshot is larger than ${exports.SOURCE_MAX_BYTES/1024} KiB`,polledAt:c,durationMs:$};else{const v=readSnapshot(o.decl.kind,T.body,t());v.ok===!0?(o.good=v.kept,o.last={state:"current",polledAt:c,durationMs:$}):o.last={state:"malformed",detail:v.reason,polledAt:c,durationMs:$}}const M=o.last.state==="unreachable"||o.last.state==="refused"||o.last.state==="malformed";o.failures=M?o.failures+1:0;const y=M?sourceBackoffMs(f,o.failures):f;o.nextAt=c+y,o.reportEveryMs=o.last.state==="not-running"?exports.SOURCE_NOT_RUNNING_REPORT_MS:y,A(o,o.last.state!=="not-running"||O!=="not-running")}catch(E){o.last={state:"unreachable",detail:`the poll failed: ${snippet(E?.message??String(E))}`,polledAt:c},o.failures+=1,o.nextAt=c+sourceBackoffMs(f,o.failures),o.reportEveryMs=o.nextAt-c,A(o,!0)}finally{o.inFlight=!1,a-=1}},"pollOne"),x=u(async()=>{if(e.stopped?.())return;const o=new Set;for(const E of e.services())if(E.decl.sources!==void 0)for(const _ of R(E).sources){if(!isPolled(_))continue;const T=`${E.wsKey} ${E.project} ${_.kind} ${_.name}`;o.add(T);const $=r.get(T);$?($.svc!==E||$.decl!==_)&&($.svc=E,$.decl=_):r.set(T,{svc:E,decl:_,nextAt:0,inFlight:!1,failures:0,reportEveryMs:_.intervalSec*1e3})}for(const[E,_]of[...r.entries()])!o.has(E)&&!_.inFlight&&r.delete(E);const c=t(),f=[...r.values()].filter(E=>!E.inFlight&&c>=E.nextAt).sort((E,_)=>E.nextAt-_.nextAt),O=[];for(const E of f){if(a>=n)break;O.push(I(E))}await Promise.all(O)},"tick");return{tick:x,start(o=1e3){return p&&clearInterval(p),p=setInterval(()=>{x().catch(c=>console.error("[gateway] source poll error:",c))},o),p.unref(),p},stop(){p&&clearInterval(p),p=void 0},snapshot(){const o=t();return[...r.values()].map(c=>{const f=c.last!==void 0&&o-c.last.polledAt>(0,exports.sourceStaleAfterMs)(c.reportEveryMs);return{wsKey:c.svc.wsKey,project:c.svc.project,name:c.decl.name,kind:c.decl.kind,path:c.decl.path,state:c.last?f?"stale":c.last.state:"never-polled",...c.last?.detail!==void 0?{detail:c.last.detail}:{},...c.last?.status!==void 0?{status:c.last.status}:{},...c.last?{polledAt:c.last.polledAt}:{},failures:c.failures,nextAt:c.nextAt,...c.good?{...(0,exports.sourceFull)(c.decl.kind,c.good),ageMs:Math.max(0,o-c.good.receivedAt)}:{}}})}}}b(createSourcePoller,"createSourcePoller"),g(createSourcePoller,"createSourcePoller"),u(createSourcePoller,"createSourcePoller");
@@ -91,26 +91,47 @@ export interface ClusterProjectDecl {
91
91
  */
92
92
  firstByteMs?: number;
93
93
  /**
94
- * CUSTOM SOURCES (issues/open/foodguard-81): what this service REPORTS that the lens cannot observe — work it keeps
95
- * waiting in its OWN storage (an outbox table), which no event can show. Each entry is a typed kind (`queue` · `loop` ·
96
- * `store` · `traffic`; this release reads `queue`) with a dev route ON THE SERVICE that answers the kind's snapshot
97
- * (`lensmcp.queue/v1`: `{ schema, at, truncated?, groups: [{ key, labels?, pending, inFlight?, failed?,
98
- * oldestPendingAt?, completedInWindow? }] }`). The dev gateway reads it from a RUNNING pod every `intervalSec`
94
+ * CUSTOM SOURCES (issues/closed/foodguard-81): what this service REPORTS that the lens cannot observe — work it keeps
95
+ * waiting in its OWN storage (an outbox table), which no event can show, the background loop that drains it, a
96
+ * storage backend the taps do not model, and messages over a channel they do not classify. Each entry is a typed kind
97
+ * (`queue` · `loop` · `store` · `traffic`; this release reads all four) with a dev route ON THE SERVICE that answers
98
+ * the kind's snapshot — `lensmcp.queue/v1`: `{ schema, at, truncated?, groups: [{ key, labels?, pending, inFlight?,
99
+ * failed?, oldestPendingAt?, completedInWindow? }] }`; `lensmcp.loop/v1`: `{ schema, at, intervalSec, lastRunAt,
100
+ * lastDurationMs?, lastOutcome?, lastItems?, nextRunAt?, runsInWindow?, itemsInWindow?, failuresInWindow?, windowSec?,
101
+ * lastError? }`; `lensmcp.store/v1`: `{ schema, at, objects, bytes, quotaBytes?, opsInWindow?, writesInWindow?,
102
+ * deletesInWindow?, errorsInWindow?, windowSec?, lastWriteAt?, lastError?, truncated?, groups?: [{ key, labels?,
103
+ * objects, bytes }] }`; `lensmcp.traffic/v1`: `{ schema, at, windowSec?, truncated?, channels: [{ key, labels?,
104
+ * direction?, counterpart?, messagesInWindow?, failuresInWindow?, rateLimitedInWindow?, p95Ms?, lastAt?, lastError?
105
+ * }] }`. A `traffic` entry WITHOUT a `path` is push-only: declared, never polled — the lens counts the `inbound` /
106
+ * `outbound` events the app pushes. The dev gateway reads every other entry from a RUNNING pod every `intervalSec`
99
107
  * (default 10, at least 2) over the pod's socket — never waking a service scaled to zero — and the lens shows it,
100
- * labelled REPORTED (`observe.sources`, `source.get`, `process://sources`, Data › Queues), warning once a group's
101
- * oldest pending item has waited longer than `thresholds.oldestWaitMin` (default 10). The lens never connects to the
102
- * app's database; the prod gateway ignores the key. Pod (`service`) declarations only. Parsed by `source-poller.ts`
103
- * `parseSourceDecls`: an unknown kind or a malformed entry is left out and said, never silently.
108
+ * labelled REPORTED (`observe.sources`, `source.get`, `process://sources`, the dashboard's Data views), judged against
109
+ * each kind's `thresholds` (every one filled with its default): a queue's `oldestWaitMin` (10) and `stalledMin` (15)
110
+ * and a loop's `idleMin` (10), in minutes; a store's `quotaPct` (90) and `errorRatePct` (5), percents, and
111
+ * `orphanMin` (60), minutes; a traffic source's `failurePct` (5), a percent, `p95Ms` (2000), milliseconds, and
112
+ * `rateLimited` (1), a count of «too many requests» answers. A loop's `feeds` names the queue source of this service
113
+ * it drains. The lens never connects to the app's database or store; the prod gateway ignores the key. Pod
114
+ * (`service`) declarations only. Parsed by `source-poller.ts` `parseSourceDecls`: an unknown kind or a malformed
115
+ * entry is left out and said, never silently; a `feeds` that names no queue source is dropped, and said.
104
116
  */
105
117
  sources?: ReadonlyArray<{
106
118
  kind: string;
107
119
  name: string;
108
- path: string;
120
+ /** Required of every kind but `traffic`, whose entry without one is push-only. */
121
+ path?: string;
109
122
  intervalSec?: number;
110
123
  thresholds?: {
111
124
  oldestWaitMin?: number;
112
125
  stalledMin?: number;
126
+ idleMin?: number;
127
+ quotaPct?: number;
128
+ errorRatePct?: number;
129
+ orphanMin?: number;
130
+ failurePct?: number;
131
+ p95Ms?: number;
132
+ rateLimited?: number;
113
133
  };
134
+ feeds?: string;
114
135
  }>;
115
136
  /**
116
137
  * FRONTEND apps only: run this app UNDER the lens. When true, the gateway
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.25.9",
3
+ "version": "1.25.11",
4
4
  "description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -58,9 +58,9 @@
58
58
  }
59
59
  },
60
60
  "dependencies": {
61
- "@lensmcp/core": "1.25.9",
62
- "@lensmcp/node-instrumentation": "1.25.9",
63
- "@lensmcp/nx-plugin": "1.25.9",
61
+ "@lensmcp/core": "1.25.11",
62
+ "@lensmcp/node-instrumentation": "1.25.11",
63
+ "@lensmcp/nx-plugin": "1.25.11",
64
64
  "fastify": "^5.8.5",
65
65
  "@fastify/reply-from": "^12.6.2",
66
66
  "@typescript/native-preview": "^7.0.0-dev.20260707.2",
@@ -132,7 +132,7 @@ export declare const CRASH_LINES = 12;
132
132
  /**
133
133
  * The end of what a worker said before it died, for a reader: a stack's frames past the first few folded into one line
134
134
  * — an uncaught error's message and what the app logged before it are what say why; twenty frames of module loader do
135
- * not, and kept whole they pushed both out of the window.
135
+ * not, and kept whole they pushed both out of the window. Plain text: its colour codes are dropped.
136
136
  */
137
137
  export declare function crashLines(tail: readonly string[], max?: number): string[];
138
138
  export declare function buildWorkerStartedEvent(who: WorkerIdentity, s: WorkerStarted, at?: number, id?: () => string): WorkerBusEvent;
@@ -1,2 +1,2 @@
1
- "use strict";var w=Object.defineProperty;var p=(r,e)=>w(r,"name",{value:e,configurable:!0});var k=Object.defineProperty,a=p((r,e)=>k(r,"name",{value:e,configurable:!0}),"a"),f=Object.defineProperty,i=a((r,e)=>f(r,"name",{value:e,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.CRASH_LINES=exports.MIN_RESPAWN_MS=exports.CRASH_LOOP_AFTER=void 0,exports.serializeWorkerSpecs=serializeWorkerSpecs,exports.parseWorkerSpecs=parseWorkerSpecs,exports.workerEnv=workerEnv,exports.workerBackoff=workerBackoff,exports.respawnDelay=respawnDelay,exports.workerExitVerdict=workerExitVerdict,exports.spanOf=spanOf,exports.crashLines=crashLines,exports.buildWorkerStartedEvent=buildWorkerStartedEvent,exports.buildWorkerUpEvent=buildWorkerUpEvent,exports.buildWorkerCrashedEvent=buildWorkerCrashedEvent,exports.buildWorkerExitedEvent=buildWorkerExitedEvent,exports.buildWorkerStoppedEvent=buildWorkerStoppedEvent;const exit_reason_1=require("./exit-reason"),NAME_RE=/^[\w.-]+$/,ENV_KEY_RE=/^[A-Za-z_][A-Za-z0-9_]*$/;function serializeWorkerSpecs(r){return JSON.stringify(r.map(e=>({name:e.name,...e.env&&typeof e.env=="object"?{env:e.env}:{},...e.port!==void 0?{port:e.port}:{}})))}p(serializeWorkerSpecs,"serializeWorkerSpecs"),a(serializeWorkerSpecs,"serializeWorkerSpecs"),i(serializeWorkerSpecs,"serializeWorkerSpecs");function parseWorkerSpecs(r){const e=[];let t;try{t=JSON.parse(r||"[]")}catch{return{specs:[],problems:[`WORKERS is not JSON: ${String(r).slice(0,80)}`]}}if(!Array.isArray(t))return{specs:[],problems:["WORKERS is not a list"]};const n=[];for(const s of t){const o=typeof s=="string"?{name:s}:s,c=typeof o?.name=="string"?o.name:"";if(!NAME_RE.test(c)){e.push(`worker ${JSON.stringify(o?.name??s)} is not a plain file name \u2014 not started`);continue}if(n.some(l=>l.name===c)){e.push(`worker "${c}" is declared twice \u2014 started once`);continue}const d={};for(const[l,v]of Object.entries(o?.env&&typeof o.env=="object"?o.env:{})){if(!ENV_KEY_RE.test(l)){e.push(`worker "${c}": env ${JSON.stringify(l)} is not a variable name \u2014 left out`);continue}typeof v=="string"||typeof v=="number"||typeof v=="boolean"?d[l]=String(v):e.push(`worker "${c}": env ${l} is not a string, number or boolean \u2014 left out`)}let u=0;o?.port!==void 0&&(Number.isInteger(o.port)&&o.port>=0&&o.port<=65535?u=o.port:e.push(`worker "${c}": port ${JSON.stringify(o.port)} is not a port \u2014 it gets 0 (a free one)`)),n.push({name:c,env:d,port:u})}return{specs:n,problems:e}}p(parseWorkerSpecs,"parseWorkerSpecs"),a(parseWorkerSpecs,"parseWorkerSpecs"),i(parseWorkerSpecs,"parseWorkerSpecs");function workerEnv(r,e){const t={...r,PORT:String(e.port),...e.env,LENSMCP_WORKER:e.name};return delete t.WORKERS,t}p(workerEnv,"workerEnv"),a(workerEnv,"workerEnv"),i(workerEnv,"workerEnv"),exports.CRASH_LOOP_AFTER=3;function envMs(r,e,t){const n=Number(r[e]);return r[e]!==void 0&&r[e]!==""&&Number.isFinite(n)&&n>=0?Math.floor(n):t}p(envMs,"envMs"),a(envMs,"envMs"),i(envMs,"envMs"),exports.MIN_RESPAWN_MS=100;function workerBackoff(r=process.env){const e=Math.max(exports.MIN_RESPAWN_MS,envMs(r,"LENSMCP_WORKER_BACKOFF_MS",1e3));return{baseMs:e,maxMs:Math.max(e,envMs(r,"LENSMCP_WORKER_BACKOFF_MAX_MS",6e4)),stableMs:envMs(r,"LENSMCP_WORKER_STABLE_MS",3e4)}}p(workerBackoff,"workerBackoff"),a(workerBackoff,"workerBackoff"),i(workerBackoff,"workerBackoff");function respawnDelay(r,e){const t=Math.max(1,Math.floor(r));return Math.min(e.maxMs,e.baseMs*2**Math.min(t-1,30))}p(respawnDelay,"respawnDelay"),a(respawnDelay,"respawnDelay"),i(respawnDelay,"respawnDelay");function workerExitVerdict(r,e,t){return t?{kind:"stopped"}:r===0&&!e?{kind:"exited"}:{kind:"crashed",external:e==="SIGTERM"||e==="SIGINT"||e==="SIGHUP"}}p(workerExitVerdict,"workerExitVerdict"),a(workerExitVerdict,"workerExitVerdict"),i(workerExitVerdict,"workerExitVerdict");function spanOf(r){return r<1e4?`${Math.round(r/100)/10} s`:r<12e4?`${Math.round(r/1e3)} s`:`${Math.round(r/6e4)} min`}p(spanOf,"spanOf"),a(spanOf,"spanOf"),i(spanOf,"spanOf"),exports.CRASH_LINES=12;const STACK_FRAMES=3;function crashLines(r,e=exports.CRASH_LINES){const t=[];let n=0,s=0;const o=i(()=>{s&&t.push(` \u2026 ${s} more frame${s===1?"":"s"}`),s=0},"fold");for(const c of r){if(/^\s+at\s/.test(c)){if(n+=1,n>STACK_FRAMES){s+=1;continue}}else o(),n=0;t.push(c)}return o(),t.slice(-e)}p(crashLines,"crashLines"),a(crashLines,"crashLines"),i(crashLines,"crashLines");let idCounter=0;function makeId(){return idCounter=(idCounter+1)%65535,`${Date.now().toString(36)}${idCounter.toString(36)}${Math.random().toString(36).slice(2,8)}`}p(makeId,"makeId"),a(makeId,"makeId"),i(makeId,"makeId");function envelope(r,e,t,n,s,o,c,d){const u="pending";return{id:c(),sessionId:u,timestamp:o,source:"gateway",category:"cluster",severity:t,context:{sessionId:u,projectName:r.project},fingerprint:`cluster-${e}:${r.service}`,title:n,...d?{message:d}:{},raw:{kind:e,project:r.project,service:r.service,worker:r.worker,pid:r.pid,podGeneration:r.podGeneration,...s}}}p(envelope,"envelope"),a(envelope,"envelope"),i(envelope,"envelope");function buildWorkerStartedEvent(r,e,t=Date.now(),n=makeId){const s=e.why==="respawn"?`started again (restart ${e.restarts})`:e.why==="reload"?"started on the new build":"started";return envelope(r,"worker-started","info",`${r.service} worker ${r.worker} ${s} \u2014 pid ${r.pid}`,{why:e.why,port:e.port,restarts:e.restarts},t,n)}p(buildWorkerStartedEvent,"buildWorkerStartedEvent"),a(buildWorkerStartedEvent,"buildWorkerStartedEvent"),i(buildWorkerStartedEvent,"buildWorkerStartedEvent");function buildWorkerUpEvent(r,e,t=Date.now(),n=makeId){return envelope(r,"worker-up","info",`${r.service} worker ${r.worker} is up again \u2014 ${spanOf(e.ranMs)} without a crash, after ${e.afterCrashes} in a row`,{ranMs:e.ranMs,afterCrashes:e.afterCrashes},t,n)}p(buildWorkerUpEvent,"buildWorkerUpEvent"),a(buildWorkerUpEvent,"buildWorkerUpEvent"),i(buildWorkerUpEvent,"buildWorkerUpEvent");function buildWorkerCrashedEvent(r,e,t=Date.now(),n=makeId){const s=e.crashes>=exports.CRASH_LOOP_AFTER,o=e.signal?e.signal:`exit ${e.code}`,c=s?`${r.service} worker ${r.worker} is crash-looping \u2014 ${e.crashes} crashes in a row (${o}); next start in ${spanOf(e.nextInMs)}`:`${r.service} worker ${r.worker} crashed (${o}${e.external?", a signal from outside its devserver":""}) after ${spanOf(e.ranMs)} \u2014 starting it again in ${spanOf(e.nextInMs)}`,d=e.external?`stopped by ${e.signal} from outside its devserver (\`kill\`, Activity Monitor)`:(0,exit_reason_1.describeExit)(e.code,e.signal),u=crashLines(e.lines);return envelope(r,"worker-crashed","error",c,{exitCode:e.code,signal:e.signal,ranMs:e.ranMs,crashes:e.crashes,restarts:e.restarts,nextInMs:e.nextInMs,crashLooping:s,external:e.external,...d?{reading:d}:{},lines:u},t,n,u.length?u.join(`
1
+ "use strict";var w=Object.defineProperty;var p=(r,e)=>w(r,"name",{value:e,configurable:!0});var k=Object.defineProperty,a=p((r,e)=>k(r,"name",{value:e,configurable:!0}),"a"),f=Object.defineProperty,i=a((r,e)=>f(r,"name",{value:e,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.CRASH_LINES=exports.MIN_RESPAWN_MS=exports.CRASH_LOOP_AFTER=void 0,exports.serializeWorkerSpecs=serializeWorkerSpecs,exports.parseWorkerSpecs=parseWorkerSpecs,exports.workerEnv=workerEnv,exports.workerBackoff=workerBackoff,exports.respawnDelay=respawnDelay,exports.workerExitVerdict=workerExitVerdict,exports.spanOf=spanOf,exports.crashLines=crashLines,exports.buildWorkerStartedEvent=buildWorkerStartedEvent,exports.buildWorkerUpEvent=buildWorkerUpEvent,exports.buildWorkerCrashedEvent=buildWorkerCrashedEvent,exports.buildWorkerExitedEvent=buildWorkerExitedEvent,exports.buildWorkerStoppedEvent=buildWorkerStoppedEvent;const exit_reason_1=require("./exit-reason"),NAME_RE=/^[\w.-]+$/,ENV_KEY_RE=/^[A-Za-z_][A-Za-z0-9_]*$/;function serializeWorkerSpecs(r){return JSON.stringify(r.map(e=>({name:e.name,...e.env&&typeof e.env=="object"?{env:e.env}:{},...e.port!==void 0?{port:e.port}:{}})))}p(serializeWorkerSpecs,"serializeWorkerSpecs"),a(serializeWorkerSpecs,"serializeWorkerSpecs"),i(serializeWorkerSpecs,"serializeWorkerSpecs");function parseWorkerSpecs(r){const e=[];let t;try{t=JSON.parse(r||"[]")}catch{return{specs:[],problems:[`WORKERS is not JSON: ${String(r).slice(0,80)}`]}}if(!Array.isArray(t))return{specs:[],problems:["WORKERS is not a list"]};const n=[];for(const s of t){const o=typeof s=="string"?{name:s}:s,c=typeof o?.name=="string"?o.name:"";if(!NAME_RE.test(c)){e.push(`worker ${JSON.stringify(o?.name??s)} is not a plain file name \u2014 not started`);continue}if(n.some(l=>l.name===c)){e.push(`worker "${c}" is declared twice \u2014 started once`);continue}const u={};for(const[l,v]of Object.entries(o?.env&&typeof o.env=="object"?o.env:{})){if(!ENV_KEY_RE.test(l)){e.push(`worker "${c}": env ${JSON.stringify(l)} is not a variable name \u2014 left out`);continue}typeof v=="string"||typeof v=="number"||typeof v=="boolean"?u[l]=String(v):e.push(`worker "${c}": env ${l} is not a string, number or boolean \u2014 left out`)}let d=0;o?.port!==void 0&&(Number.isInteger(o.port)&&o.port>=0&&o.port<=65535?d=o.port:e.push(`worker "${c}": port ${JSON.stringify(o.port)} is not a port \u2014 it gets 0 (a free one)`)),n.push({name:c,env:u,port:d})}return{specs:n,problems:e}}p(parseWorkerSpecs,"parseWorkerSpecs"),a(parseWorkerSpecs,"parseWorkerSpecs"),i(parseWorkerSpecs,"parseWorkerSpecs");function workerEnv(r,e){const t={...r,PORT:String(e.port),...e.env,LENSMCP_WORKER:e.name};return delete t.WORKERS,t}p(workerEnv,"workerEnv"),a(workerEnv,"workerEnv"),i(workerEnv,"workerEnv"),exports.CRASH_LOOP_AFTER=3;function envMs(r,e,t){const n=Number(r[e]);return r[e]!==void 0&&r[e]!==""&&Number.isFinite(n)&&n>=0?Math.floor(n):t}p(envMs,"envMs"),a(envMs,"envMs"),i(envMs,"envMs"),exports.MIN_RESPAWN_MS=100;function workerBackoff(r=process.env){const e=Math.max(exports.MIN_RESPAWN_MS,envMs(r,"LENSMCP_WORKER_BACKOFF_MS",1e3));return{baseMs:e,maxMs:Math.max(e,envMs(r,"LENSMCP_WORKER_BACKOFF_MAX_MS",6e4)),stableMs:envMs(r,"LENSMCP_WORKER_STABLE_MS",3e4)}}p(workerBackoff,"workerBackoff"),a(workerBackoff,"workerBackoff"),i(workerBackoff,"workerBackoff");function respawnDelay(r,e){const t=Math.max(1,Math.floor(r));return Math.min(e.maxMs,e.baseMs*2**Math.min(t-1,30))}p(respawnDelay,"respawnDelay"),a(respawnDelay,"respawnDelay"),i(respawnDelay,"respawnDelay");function workerExitVerdict(r,e,t){return t?{kind:"stopped"}:r===0&&!e?{kind:"exited"}:{kind:"crashed",external:e==="SIGTERM"||e==="SIGINT"||e==="SIGHUP"}}p(workerExitVerdict,"workerExitVerdict"),a(workerExitVerdict,"workerExitVerdict"),i(workerExitVerdict,"workerExitVerdict");function spanOf(r){return r<1e4?`${Math.round(r/100)/10} s`:r<12e4?`${Math.round(r/1e3)} s`:`${Math.round(r/6e4)} min`}p(spanOf,"spanOf"),a(spanOf,"spanOf"),i(spanOf,"spanOf"),exports.CRASH_LINES=12;const STACK_FRAMES=3,ANSI_SGR=/\x1b\[[0-9;]*m/g;function crashLines(r,e=exports.CRASH_LINES){const t=[];let n=0,s=0;const o=i(()=>{s&&t.push(` \u2026 ${s} more frame${s===1?"":"s"}`),s=0},"fold");for(const c of r){const u=c.replace(ANSI_SGR,"");if(/^\s+at\s/.test(u)){if(n+=1,n>STACK_FRAMES){s+=1;continue}}else o(),n=0;t.push(u)}return o(),t.slice(-e)}p(crashLines,"crashLines"),a(crashLines,"crashLines"),i(crashLines,"crashLines");let idCounter=0;function makeId(){return idCounter=(idCounter+1)%65535,`${Date.now().toString(36)}${idCounter.toString(36)}${Math.random().toString(36).slice(2,8)}`}p(makeId,"makeId"),a(makeId,"makeId"),i(makeId,"makeId");function envelope(r,e,t,n,s,o,c,u){const d="pending";return{id:c(),sessionId:d,timestamp:o,source:"gateway",category:"cluster",severity:t,context:{sessionId:d,projectName:r.project},fingerprint:`cluster-${e}:${r.service}`,title:n,...u?{message:u}:{},raw:{kind:e,project:r.project,service:r.service,worker:r.worker,pid:r.pid,podGeneration:r.podGeneration,...s}}}p(envelope,"envelope"),a(envelope,"envelope"),i(envelope,"envelope");function buildWorkerStartedEvent(r,e,t=Date.now(),n=makeId){const s=e.why==="respawn"?`started again (restart ${e.restarts})`:e.why==="reload"?"started on the new build":"started";return envelope(r,"worker-started","info",`${r.service} worker ${r.worker} ${s} \u2014 pid ${r.pid}`,{why:e.why,port:e.port,restarts:e.restarts},t,n)}p(buildWorkerStartedEvent,"buildWorkerStartedEvent"),a(buildWorkerStartedEvent,"buildWorkerStartedEvent"),i(buildWorkerStartedEvent,"buildWorkerStartedEvent");function buildWorkerUpEvent(r,e,t=Date.now(),n=makeId){return envelope(r,"worker-up","info",`${r.service} worker ${r.worker} is up again \u2014 ${spanOf(e.ranMs)} without a crash, after ${e.afterCrashes} in a row`,{ranMs:e.ranMs,afterCrashes:e.afterCrashes},t,n)}p(buildWorkerUpEvent,"buildWorkerUpEvent"),a(buildWorkerUpEvent,"buildWorkerUpEvent"),i(buildWorkerUpEvent,"buildWorkerUpEvent");function buildWorkerCrashedEvent(r,e,t=Date.now(),n=makeId){const s=e.crashes>=exports.CRASH_LOOP_AFTER,o=e.signal?e.signal:`exit ${e.code}`,c=s?`${r.service} worker ${r.worker} is crash-looping \u2014 ${e.crashes} crashes in a row (${o}); next start in ${spanOf(e.nextInMs)}`:`${r.service} worker ${r.worker} crashed (${o}${e.external?", a signal from outside its devserver":""}) after ${spanOf(e.ranMs)} \u2014 starting it again in ${spanOf(e.nextInMs)}`,u=e.external?`stopped by ${e.signal} from outside its devserver (\`kill\`, Activity Monitor)`:(0,exit_reason_1.describeExit)(e.code,e.signal),d=crashLines(e.lines);return envelope(r,"worker-crashed","error",c,{exitCode:e.code,signal:e.signal,ranMs:e.ranMs,crashes:e.crashes,restarts:e.restarts,nextInMs:e.nextInMs,crashLooping:s,external:e.external,...u?{reading:u}:{},lines:d},t,n,d.length?d.join(`
2
2
  `):void 0)}p(buildWorkerCrashedEvent,"buildWorkerCrashedEvent"),a(buildWorkerCrashedEvent,"buildWorkerCrashedEvent"),i(buildWorkerCrashedEvent,"buildWorkerCrashedEvent");function buildWorkerExitedEvent(r,e,t=Date.now(),n=makeId){const s=e.reason??`it ended on its own (code 0) after ${spanOf(e.ranMs)}`;return envelope(r,"worker-exited","warning",`${r.service} worker ${r.worker} is not running \u2014 ${s}; not started again until the next build`,{ranMs:e.ranMs,...e.reason?{reason:e.reason}:{}},t,n)}p(buildWorkerExitedEvent,"buildWorkerExitedEvent"),a(buildWorkerExitedEvent,"buildWorkerExitedEvent"),i(buildWorkerExitedEvent,"buildWorkerExitedEvent");function buildWorkerStoppedEvent(r,e,t=Date.now(),n=makeId){return envelope(r,"worker-stopped","info",`${r.service} worker ${r.worker} stopped \u2014 ${e.reason}`,{reason:e.reason},t,n)}p(buildWorkerStoppedEvent,"buildWorkerStoppedEvent"),a(buildWorkerStoppedEvent,"buildWorkerStoppedEvent"),i(buildWorkerStoppedEvent,"buildWorkerStoppedEvent");