@lensmcp/cluster 1.25.8 → 1.25.10
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 +1 -1
- package/executors/gateway/runtime/own-log.d.ts +18 -3
- package/executors/gateway/runtime/own-log.js +4 -3
- package/executors/gateway/runtime/source-poller.d.ts +323 -22
- package/executors/gateway/runtime/source-poller.js +1 -1
- package/executors/gateway/runtime/types.d.ts +31 -10
- package/package.json +4 -4
- package/worker-supervisor.d.ts +1 -1
- package/worker-supervisor.js +1 -1
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. `
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
2
|
-
`)}catch{}}
|
|
3
|
-
`)
|
|
1
|
+
"use strict";var O=Object.defineProperty;var a=(e,t)=>O(e,"name",{value:t,configurable:!0});var y=Object.defineProperty,o=a((e,t)=>y(e,"name",{value:t,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(e=>process.env[e],"readEnv");function shiftGenerations(e,t){try{fs.unlinkSync(`${e}.${t}`)}catch{}for(let r=t-1;r>=1;r--)try{fs.renameSync(`${e}.${r}`,`${e}.${r+1}`)}catch{}}a(shiftGenerations,"shiftGenerations"),o(shiftGenerations,"shiftGenerations");const sameFile=o((e,t)=>!!e&&e.isFile()&&e.dev===t.dev&&e.ino===t.ino,"sameFile"),statOr=o(e=>{try{return e()}catch{return}},"statOr");function rotateOwnLog(e,t={}){const r=t.fd??1,s=t.maxBytes??exports.OWN_LOG_MAX_BYTES,c=t.keep??exports.OWN_LOG_GENERATIONS,n=statOr(()=>fs.statSync(e));if(!n||n.size<=s)return!1;const i=sameFile(statOr(()=>fs.fstatSync(r)),n),u=t.launchedLog??readEnv(exports.GATEWAY_LOG_ENV);if(!i&&!(u&&sameFile(statOr(()=>fs.statSync(u)),n)))return!1;try{shiftGenerations(e,c),fs.copyFileSync(e,`${e}.1`);try{fs.chmodSync(`${e}.1`,384)}catch{}i?fs.ftruncateSync(r,0):fs.truncateSync(e,0)}catch{return!1}const p=i?_=>{try{fs.writeSync(r,`${_}
|
|
2
|
+
`)}catch{}}:_=>{try{fs.appendFileSync(e,`${_}
|
|
3
|
+
`)}catch{}};return(t.note??p)(`[gateway] ${path.basename(e)} passed ${Math.round(s/1048576)} MB \u2014 rotated; the lines before are in ${path.basename(e)}.1 (\u2026${c} kept)`),!0}a(rotateOwnLog,"rotateOwnLog"),o(rotateOwnLog,"rotateOwnLog");function startOwnLogRotation(e,t={}){const r=path.join(e,".lensmcp","gateway.log"),s=setInterval(()=>rotateOwnLog(r,t),t.everyMs??CHECK_EVERY_MS);return s.unref?.(),()=>clearInterval(s)}a(startOwnLogRotation,"startOwnLogRotation"),o(startOwnLogRotation,"startOwnLogRotation");function linePrefixer(e){let t=!0;const r=`[${e}] `;return s=>{let c="";for(const n of s.split(/(?<=\n)/))n&&(c+=(t?r:"")+n,t=n.endsWith(`
|
|
4
|
+
`));return c}}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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
/**
|
|
45
|
-
|
|
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:
|
|
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.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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,
|
|
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
|
-
/**
|
|
154
|
-
|
|
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
|
-
/**
|
|
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 U=Object.defineProperty;var v=(e,o)=>U(e,"name",{value:o,configurable:!0});var b=Object.defineProperty,l=v((e,o)=>b(e,"name",{value:o,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,o,n)=>{const u=Number(readEnv(e));return Number.isInteger(u)&&u>=n?u:o},"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,o){return o<=1?e:Math.min(e*2**Math.min(o-1,10),Math.max(e,exports.SOURCE_BACKOFF_MAX_MS))}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 o=[],n=[];return e===void 0?{sources:o,problems:n}:Array.isArray(e)?(e.forEach((u,p)=>{const E=`sources[${p}]`;if(!isObj(u)){n.push(`${E} is not an object \u2014 not read`);return}const f=typeof u.name=="string"&&u.name.trim()?`${E} "${u.name}"`:E,i=u.kind;if(typeof i!="string"||!exports.SOURCE_KINDS.includes(i)){n.push(`${f}: 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(`${f}: 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(s=>!ENTRY_KEYS.includes(s));if(h.length){n.push(`${f}: unknown key${h.length===1?"":"s"} ${h.map(s=>JSON.stringify(s)).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(`${E}: name ${JSON.stringify(a??null)} must be 1\u2013100 characters, not blank \u2014 not read`);return}if(o.some(s=>s.name===a)){n.push(`${f}: 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(`${f}: 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(`${f}: 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(`${f}: thresholds must be an object \u2014 not read`);return}const O=isObj(S)?S:{},x=Object.keys(O).filter(s=>!THRESHOLD_KEYS[r].includes(s));if(x.length){n.push(`${f}: unknown threshold${x.length===1?"":"s"} ${x.map(s=>JSON.stringify(s)).join(", ")} for a ${r} source (known: ${THRESHOLD_KEYS[r].join(", ")}) \u2014 not read`);return}const t=THRESHOLD_KEYS[r].find(s=>O[s]!==void 0&&!minutes(O[s]));if(t){n.push(`${f}: thresholds.${t} ${JSON.stringify(O[t])} must be minutes, more than 0 and at most 10080 \u2014 not read`);return}if(o.length>=exports.SOURCE_MAX_PER_SERVICE){n.push(`${f}: more than ${exports.SOURCE_MAX_PER_SERVICE} sources on one service \u2014 not read`);return}o.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:o,problems:n}):{sources:o,problems:["`sources` is not a list of { kind, name, path } entries \u2014 no source is read"]}}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 o=l(i=>({ok:!1,reason:i}),"fail");if(!isObj(e))return o("the snapshot is not a JSON object");if(e.schema!==exports.QUEUE_WIRE_SCHEMA)return o(`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 o("`at` is not an ISO time");const u=e.truncated;if(u!==void 0&&!(isObj(u)&&isCount(u.groups)))return o("`truncated` must be { groups: <how many were dropped> }, or absent");const p=e.groups;if(!Array.isArray(p))return o("`groups` is not a list");if(p.length>exports.SOURCE_MAX_GROUPS)return o(`${p.length} groups \u2014 at most ${exports.SOURCE_MAX_GROUPS}; cap them and say \`truncated: { groups: <dropped> }\``);const E=new Set,f=[];for(let i=0;i<p.length;i++){const r=p[i],h=`groups[${i}]`;if(!isObj(r))return o(`${h} is not an object`);const a=r.key;if(typeof a!="string"||a.length===0||a.length>200)return o(`${h}.key must be a string of 1\u2013200 characters`);if(E.has(a))return o(`${h}.key ${JSON.stringify(a)} appears twice`);if(E.add(a),!isCount(r.pending))return o(`${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 o(`${h}.${S} must be a whole number \u2265 0, or absent`);const _=r.oldestPendingAt;if(_!=null&&!isIso(_))return o(`${h}.oldestPendingAt must be an ISO time, null, or absent`);const m=r.labels;if(m!==void 0){if(!isObj(m))return o(`${h}.labels must be an object of strings`);const S=Object.entries(m);if(S.length>16)return o(`${h}.labels has more than 16 entries`);for(const[O,x]of S)if(O.length===0||O.length>64||typeof x!="string"||x.length>200)return o(`${h}.labels.${O.slice(0,64)} must be a string of at most 200 characters`)}f.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:f,...u!==void 0?{truncated:{groups:u.groups}}:{}}}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,o,n,u){const p={groups:o.length,pending:0,inFlight:0,failed:0};let E;for(const i of o)p.pending+=i.pending,p.inFlight+=i.inFlight??0,p.failed+=i.failed??0,i.completedInWindow!==void 0&&(E=(E??0)+i.completedInWindow);E!==void 0&&(p.completedInWindow=E);const f=[...o].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:f.map(({key:i,...r})=>({group:i,...r})),...u?{truncated:{groups:u.groups}}:{}}}v(keepQueueSnapshot,"keepQueueSnapshot"),l(keepQueueSnapshot,"keepQueueSnapshot");function buildSourcePollRaw(e,o,n,u,p,E){return{kind:"source-poll",provenance:"reported",project:e,...declRaw(o),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}:{},...E?{snapshot:E}:{}}}v(buildSourcePollRaw,"buildSourcePollRaw"),l(buildSourcePollRaw,"buildSourcePollRaw");function buildSourceDeclarationRaw(e,o){return{kind:"source-declaration",project:e,sources:o.sources.map(declRaw),problems:[...o.problems]}}v(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),l(buildSourceDeclarationRaw,"buildSourceDeclarationRaw");const span=l(e=>{const o=Math.round(e/6e4);return e<6e4?`${Math.max(0,Math.round(e/1e3))}s`:o<120?`${o}m`:`${Math.floor(o/60)}h${o%60?` ${o%60}m`:""}`},"span");function sourcePollTitle(e){const o=`${e.sourceKind} ${e.project}/${e.name} (reported)`;if(e.state!=="current"||!e.snapshot)return`${o}: ${e.state}${e.detail?` \u2014 ${e.detail}`:""}`;const n=e.snapshot,u=n.groups.find(E=>E.pending>0&&E.oldestPendingAt),p=u?.oldestPendingAt?Date.parse(n.at)-Date.parse(u.oldestPendingAt):void 0;return`${o}: ${n.totals.pending} pending in ${n.totals.groups} group${n.totals.groups===1?"":"s"}${p!==void 0?`, oldest waiting ${span(Math.max(0,p))}`:""}`}v(sourcePollTitle,"sourcePollTitle"),l(sourcePollTitle,"sourcePollTitle");function requestOverSocket(e,o,n,u,p=exports.SOURCE_MAX_BYTES){return new Promise(E=>{let f=!1,i;const r=l(a=>{f||(f=!0,clearTimeout(h),E(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:o,method:"GET",agent:!1,headers:n},a=>{const _=[];let m=0;a.on("data",S=>{if(!f){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})}})}v(requestOverSocket,"requestOverSocket"),l(requestOverSocket,"requestOverSocket");const snippet=l(e=>e.replace(/\s+/g," ").trim().slice(0,160),"snippet");function createSourcePoller(e){const o=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)),E=e.request??requestOverSocket,f=e.log??(t=>console.warn(t)),i=new WeakMap,r=new Map;let h=0,a;const _=l(t=>{const s=t.decl;let d=i.get(s);if(!d){d=parseSourceDecls(t.decl.sources),i.set(s,d);for(const c of d.problems)f(`[gateway] ${t.project}: cluster.sources \u2014 ${c}`)}const $=o();return(d.reportedAt===void 0||$-d.reportedAt>=exports.SOURCE_DECLARATION_REPORT_MS)&&(d.reportedAt=$,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 s=p(t);return s?{socket:s}:{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,s)=>{const d=t.last;if(!d)return;const $=o();if(!s&&t.reportedAt!==void 0&&$-t.reportedAt<t.reportEveryMs)return;t.reportedAt=$;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 s=o(),d=t.decl.intervalSec*1e3;t.inFlight=!0,h+=1;const $=t.last?.state;try{const c=m(t.svc);if("notRunning"in c){t.last={state:"not-running",detail:c.notRunning,polledAt:s},t.failures=0,t.nextAt=s+d,t.reportEveryMs=exports.SOURCE_NOT_RUNNING_REPORT_MS,S(t,$!=="not-running");return}const R=e.serviceKey(t.svc),g=await E(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),M=Math.max(0,o()-s);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:s,durationMs:M}:{state:"unreachable",detail:g.code==="ETIMEDOUT"?g.message:`${g.code}: ${snippet(g.message)}`,polledAt:s,durationMs:M}}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:s,durationMs:M}}else if(g.truncated)t.last={state:"malformed",detail:`the snapshot is larger than ${exports.SOURCE_MAX_BYTES/1024} KiB`,polledAt:s,durationMs:M};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,o(),A.truncated),t.last={state:"current",polledAt:s,durationMs:M}):t.last={state:"malformed",detail:A.reason,polledAt:s,durationMs:M}}const y=t.last.state==="unreachable"||t.last.state==="refused"||t.last.state==="malformed";t.failures=y?t.failures+1:0;const C=y?sourceBackoffMs(d,t.failures):d;t.nextAt=s+C,t.reportEveryMs=t.last.state==="not-running"?exports.SOURCE_NOT_RUNNING_REPORT_MS:C,S(t,t.last.state!=="not-running"||$!=="not-running")}catch(c){t.last={state:"unreachable",detail:`the poll failed: ${snippet(c?.message??String(c))}`,polledAt:s},t.failures+=1,t.nextAt=s+sourceBackoffMs(d,t.failures),t.reportEveryMs=t.nextAt-s,S(t,!0)}finally{t.inFlight=!1,h-=1}},"pollOne"),x=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 M=r.get(g);M?(M.svc!==c||M.decl!==R)&&(M.svc=c,M.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 s=o(),d=[...r.values()].filter(c=>!c.inFlight&&s>=c.nextAt).sort((c,R)=>c.nextAt-R.nextAt),$=[];for(const c of d){if(h>=u)break;$.push(O(c))}await Promise.all($)},"tick");return{tick:x,start(t=1e3){return a&&clearInterval(a),a=setInterval(()=>{x().catch(s=>console.error("[gateway] source poll error:",s))},t),a.unref(),a},stop(){a&&clearInterval(a),a=void 0},snapshot(){const t=o();return[...r.values()].map(s=>{const d=s.last!==void 0&&t-s.last.polledAt>(0,exports.sourceStaleAfterMs)(s.reportEveryMs);return{wsKey:s.svc.wsKey,project:s.svc.project,name:s.decl.name,kind:s.decl.kind,path:s.decl.path,state:s.last?d?"stale":s.last.state:"never-polled",...s.last?.detail!==void 0?{detail:s.last.detail}:{},...s.last?.status!==void 0?{status:s.last.status}:{},...s.last?{polledAt:s.last.polledAt}:{},failures:s.failures,nextAt:s.nextAt,...s.good?{snapshot:s.good,ageMs:Math.max(0,t-s.good.receivedAt)}:{}}})}}}v(createSourcePoller,"createSourcePoller"),l(createSourcePoller,"createSourcePoller");
|
|
1
|
+
"use strict";var C=Object.defineProperty;var g=(e,t)=>C(e,"name",{value:t,configurable:!0});var 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,o)=>{const n=Number(readEnv(e));return Number.isInteger(n)&&n>=o?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))}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=[],o=[],n=[];if(e===void 0)return{sources:t,problems:o};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 E=`sources[${l}]`;if(!isObj(i)){o.push(`${E} is not an object \u2014 not read`);return}const d=typeof i.name=="string"&&i.name.trim()?`${E} "${i.name}"`:E,r=i.kind;if(typeof r!="string"||!exports.SOURCE_KINDS_READ.includes(r)){o.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){o.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)){o.push(`${E}: name ${JSON.stringify(R??null)} must be 1\u2013100 characters, not blank \u2014 not read`);return}if(t.some(_=>_.name===R)){o.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"){o.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(".."))){o.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)){o.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)){o.push(`${d}: thresholds must be an object \u2014 not read`);return}const b=isObj(A)?A:{},I=THRESHOLDS[a],s=Object.keys(I),c=Object.keys(b).filter(_=>!s.includes(_));if(c.length){o.push(`${d}: unknown threshold${c.length===1?"":"s"} ${c.map(_=>JSON.stringify(_)).join(", ")} for a ${a} source (known: ${s.join(", ")}) \u2014 not read`);return}const f=s.find(_=>b[_]!==void 0&&!I[_].ok(b[_]));if(f){o.push(`${d}: thresholds.${f} ${JSON.stringify(b[f])} ${I[f].must} \u2014 not read`);return}if(t.length>=exports.SOURCE_MAX_PER_SERVICE){o.push(`${d}: more than ${exports.SOURCE_MAX_PER_SERVICE} sources on one service \u2014 not read`);return}const O={};for(const _ of s)O[_]=b[_]??I[_].def;const S={kind:a,name:R,...h!==void 0?{path:h}:{},intervalSec:m??exports.SOURCE_DEFAULT_INTERVAL_SEC,thresholds:O};t.push(S),a==="loop"&&i.feeds!==void 0&&n.push({decl:S,label:d,feeds:i.feeds})});for(const{decl:i,label:l,feeds:E}of n)typeof E!="string"||!E.trim()?o.push(`${l}: feeds ${JSON.stringify(E)} must be the name of a queue source on this service \u2014 read without it`):t.some(d=>d.kind==="queue"&&d.name===E)?i.feeds=E:o.push(`${l}: feeds ${JSON.stringify(E)} names no queue source on this service \u2014 read without it`);return{sources:t,problems:o}}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,o)=>{if(typeof e!="string"||e.length===0||e.length>200)return`${t}.key must be a string of 1\u2013200 characters`;if(o.has(e))return`${t}.key ${JSON.stringify(e)} appears twice`;o.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 o=Object.entries(e);if(o.length>16)return`${t}.labels has more than 16 entries`;for(const[n,i]of o)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 o=e.at;if(!isIso(o))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,E=[];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);E.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:o,groups:E,...n!==void 0?{truncated:{groups:n.groups}}:{}}}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 o=t-1,n=e.charCodeAt(o-1);return`${e.slice(0,n>=55296&&n<=56319?o-1:o)}\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 o=e.at;if(!isIso(o))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 E=e.nextRunAt;if(E!=null&&!isIso(E))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:o,intervalSec:n,lastRunAt:i,...r("lastDurationMs"),...l!==void 0?{lastOutcome:l}:{},...r("lastItems"),...E!==void 0?{nextRunAt:E}:{},...r("runsInWindow"),...r("itemsInWindow"),...r("failuresInWindow"),...r("windowSec"),...d!==void 0?{lastError:cutText(d,exports.SOURCE_ERROR_MAX)}:{}}}}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 o=e.at;if(!isIso(o))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 E=e.truncated;if(E!==void 0&&!(isObj(E)&&isCount(E.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 s of["objects","bytes"])if(!isCount(h[s]))return t(`${m}.${s} must be a whole number \u2265 0`);const b=h.labels,I=labelsWrong(b,m);if(I)return t(I);r.push({key:h.key,...b!==void 0?{labels:b}:{},objects:h.objects,bytes:h.bytes})}}const a=u(p=>e[p]!==void 0?{[p]:e[p]}:{},"count");return{ok:!0,store:{at:o,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}:{},...E!==void 0?{truncated:{groups:E.groups}}:{}}}}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 o=e.at;if(!isIso(o))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 E=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,E);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 b=a.lastAt;if(b!=null&&!isIso(b))return t(`${p}.lastAt must be an ISO time, null, or absent`);const I=a.lastError;if(I!==void 0&&typeof I!="string")return t(`${p}.lastError must be a string, or absent`);const s=a.labels,c=labelsWrong(s,p);if(c)return t(c);const f=u(O=>a[O]!==void 0?{[O]:a[O]}:{},"count");d.push({key:a.key,...s!==void 0?{labels:s}:{},...h!==void 0?{direction:h}:{},...m!==void 0?{counterpart:m}:{},...f("messagesInWindow"),...f("failuresInWindow"),...f("rateLimitedInWindow"),...A!==void 0?{p95Ms:A}:{},...b!==void 0?{lastAt:b}:{},...I!==void 0?{lastError:cutText(I,exports.SOURCE_ERROR_MAX)}:{}})}return{ok:!0,traffic:{at:o,...n!==void 0?{windowSec:n}:{},channels:d,...i!==void 0?{truncated:{channels:i.channels}}:{}}}}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 o of Object.keys(e).sort())t[o]=canonical(e[o]);return t},"canonical");function sourceDigest(e){return(0,node_crypto_1.createHash)("sha1").update(JSON.stringify(canonical(e))??"null").digest("hex").slice(0,16)}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,o,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 E=[...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:o,totals:i,groups:E,...d?{truncated:d}:{},digest:sourceDigest({groups:E,truncated:d??null})}}g(keepQueueSnapshot,"keepQueueSnapshot"),u(keepQueueSnapshot,"keepQueueSnapshot");function keepLoopSnapshot(e,t){const{at:o,...n}=e;return{at:o,receivedAt:t,digest:sourceDigest(n),...n}}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:o,groups:n,truncated:i,...l}=e,E=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,...E?{groups:E}:{},...i?{truncated:{groups:i.groups}}:{}};return{at:o,receivedAt:t,digest:sourceDigest(d),...d}}g(keepStoreSnapshot,"keepStoreSnapshot"),u(keepStoreSnapshot,"keepStoreSnapshot");const messagesOf=u(e=>e.messagesInWindow??-1,"messagesOf");function keepTrafficSnapshot(e,t){const{at:o,channels:n,truncated:i,...l}=e,E=[...n].sort((r,a)=>messagesOf(a)-messagesOf(r)||byKey(r.key,a.key)).map(({key:r,...a})=>({channel:r,...a})),d={...l,channels:E,...i?{truncated:{channels:i.channels}}:{}};return{at:o,receivedAt:t,digest:sourceDigest(d),...d}}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,o,n,i,l){return{kind:"source-poll",provenance:"reported",project:e,...declRaw(t),state:o.state,...o.detail!==void 0?{detail:o.detail}:{},...o.status!==void 0?{status:o.status}:{},polledAt:o.polledAt,...o.durationMs!==void 0?{durationMs:o.durationMs}:{},reportEveryMs:n,...i>0?{failures:i}:{},...l??{}}}g(buildSourcePollRaw,"buildSourcePollRaw"),u(buildSourcePollRaw,"buildSourcePollRaw");function buildSourceDeclarationRaw(e,t){return{kind:"source-declaration",project:e,sources:t.sources.map(declRaw),problems:[...t.problems]}}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 o=e,n=0;for(;o>=999.5&&n<t.length-1;)o/=1e3,n+=1;return n===0?`${e} B`:`${o<9.95?o.toFixed(1):Math.round(o)} ${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}%)`:"",o=e.errorsInWindow?`, ${plural(e.errorsInWindow,"error")}${e.windowSec?` in ${span(e.windowSec*1e3)}`:""}`:"";return`${plural(e.objects,"object")}, ${byteSize(e.bytes)}${t}${o}`},"storeWords"),trafficWords=u(e=>{let t,o=0,n=0;for(const l of e.channels)l.messagesInWindow!==void 0&&(t=(t??0)+l.messagesInWindow),o+=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}${o?`, ${o} failed`:""}${n?`, ${n} rate-limited`:""}`},"trafficWords");function sourcePollTitle(e,t){const o=`${e.sourceKind} ${e.project}/${e.name} (reported)`,n=t??e[exports.SOURCE_SNAPSHOT_FIELD[e.sourceKind]];if(e.state!=="current"||!n)return`${o}: ${e.state}${e.detail?` \u2014 ${e.detail}`:""}`;if(e.sourceKind==="loop")return`${o}: ${loopWords(n)}`;if(e.sourceKind==="store")return`${o}: ${storeWords(n)}`;if(e.sourceKind==="traffic")return`${o}: ${trafficWords(n)}`;const i=n,l=i.groups.find(d=>d.pending>0&&d.oldestPendingAt),E=l?.oldestPendingAt?Date.parse(i.at)-Date.parse(l.oldestPendingAt):void 0;return`${o}: ${i.totals.pending} pending in ${i.totals.groups} group${i.totals.groups===1?"":"s"}${E!==void 0?`, oldest waiting ${span(Math.max(0,E))}`:""}`}g(sourcePollTitle,"sourcePollTitle"),u(sourcePollTitle,"sourcePollTitle");function requestOverSocket(e,t,o,n,i=exports.SOURCE_MAX_BYTES){return new Promise(l=>{let E=!1,d;const r=u(p=>{E||(E=!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:o},p=>{const R=[];let h=0;p.on("data",m=>{if(!E){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})}})}g(requestOverSocket,"requestOverSocket"),u(requestOverSocket,"requestOverSocket");const snippet=u(e=>e.replace(/\s+/g," ").trim().slice(0,160),"snippet");function readSnapshot(e,t,o){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,o)}:l}if(e==="store"){const l=parseStoreSnapshot(n);return l.ok===!0?{ok:!0,kept:keepStoreSnapshot(l.store,o)}:l}if(e==="traffic"){const l=parseTrafficSnapshot(n);return l.ok===!0?{ok:!0,kept:keepTrafficSnapshot(l.traffic,o)}:l}const i=parseQueueSnapshot(n);return i.ok===!0?{ok:!0,kept:keepQueueSnapshot(i.at,i.groups,o,i.truncated)}:i}g(readSnapshot,"readSnapshot"),u(readSnapshot,"readSnapshot");function createSourcePoller(e){const t=e.now??Date.now,o=e.timeoutMs??(0,exports.sourcesTimeoutMs)(),n=Math.max(1,e.maxConcurrent??(0,exports.sourcesConcurrency)()),i=e.socketOf??(s=>(0,discovery_1.pickSock)(s.pool,e.scanTtlMs??1500)),l=e.request??requestOverSocket,E=e.log??(s=>console.warn(s)),d=new WeakMap,r=new Map;let a=0,p;const R=u(s=>{const c=s.decl;let f=d.get(c);if(!f){f=parseSourceDecls(s.decl.sources),d.set(c,f);for(const S of f.problems)E(`[gateway] ${s.project}: cluster.sources \u2014 ${S}`)}const O=t();return(f.reportedAt===void 0||O-f.reportedAt>=exports.SOURCE_DECLARATION_REPORT_MS)&&(f.reportedAt=O,e.emit(s,f.problems.length?"warning":"info",f.problems.length?`sources ${s.project}: ${f.problems.length} cluster.sources entr${f.problems.length===1?"y":"ies"} not read \u2014 ${f.problems[0]}`:`sources ${s.project}: reads ${f.sources.map(S=>`${S.kind} "${S.name}"${isPolled(S)?"":" (push-only)"}`).join(", ")}`,`source-declaration:${s.wsKey}:${s.project}`,buildSourceDeclarationRaw(s.project,f))),f},"declarationOf"),h=u(s=>{if(s.state==="starting")return{notRunning:"it is starting \u2014 read once it is up"};const c=i(s);return c?{socket:c}:{notRunning:s.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((s,c)=>{const f=s.good;if(f)return s.full&&s.full.digest===f.digest&&c-s.full.at<exports.SOURCE_FULL_REPORT_MS?{same:(0,exports.sourceSame)(f)}:(s.full={digest:f.digest,at:c},(0,exports.sourceFull)(s.decl.kind,f))},"carried"),A=u((s,c)=>{const f=s.last;if(!f)return;const O=t();if(!c&&s.reportedAt!==void 0&&O-s.reportedAt<s.reportEveryMs)return;s.reportedAt=O;const S=buildSourcePollRaw(s.svc.project,s.decl,f,s.reportEveryMs,s.failures,m(s,O)),_=f.state==="unreachable"||f.state==="refused"||f.state==="malformed";e.emit(s.svc,_?"warning":"info",sourcePollTitle(S,s.good),`source:${s.svc.wsKey}:${s.svc.project}:${s.decl.name}`,S)},"report"),b=u(async s=>{const c=t(),f=s.decl.intervalSec*1e3;s.inFlight=!0,a+=1;const O=s.last?.state;try{const S=h(s.svc);if("notRunning"in S){s.last={state:"not-running",detail:S.notRunning,polledAt:c},s.failures=0,s.nextAt=c+f,s.reportEveryMs=exports.SOURCE_NOT_RUNNING_REPORT_MS,A(s,O!=="not-running");return}const _=e.serviceKey(s.svc),T=await l(S.socket,s.decl.path,{accept:"application/json","x-lensmcp-probe":`source:${s.decl.kind}`,..._?{"x-internal-token":_}:{}},o,exports.SOURCE_MAX_BYTES),x=Math.max(0,t()-c);if(T.ok===!1){const $=T.code==="ECONNREFUSED"||T.code==="ENOENT";s.last=$&&s.svc.state!=="up"?{state:"not-running",detail:`nothing listens on its pod socket \u2014 the service is ${s.svc.state}`,polledAt:c,durationMs:x}:{state:"unreachable",detail:T.code==="ETIMEDOUT"?T.message:`${T.code}: ${snippet(T.message)}`,polledAt:c,durationMs:x}}else if(T.status<200||T.status>299){const $=snippet(T.body);s.last={state:"refused",status:T.status,detail:`HTTP ${T.status}${$?`: ${$}`:""}`,polledAt:c,durationMs:x}}else if(T.truncated)s.last={state:"malformed",detail:`the snapshot is larger than ${exports.SOURCE_MAX_BYTES/1024} KiB`,polledAt:c,durationMs:x};else{const $=readSnapshot(s.decl.kind,T.body,t());$.ok===!0?(s.good=$.kept,s.last={state:"current",polledAt:c,durationMs:x}):s.last={state:"malformed",detail:$.reason,polledAt:c,durationMs:x}}const v=s.last.state==="unreachable"||s.last.state==="refused"||s.last.state==="malformed";s.failures=v?s.failures+1:0;const M=v?sourceBackoffMs(f,s.failures):f;s.nextAt=c+M,s.reportEveryMs=s.last.state==="not-running"?exports.SOURCE_NOT_RUNNING_REPORT_MS:M,A(s,s.last.state!=="not-running"||O!=="not-running")}catch(S){s.last={state:"unreachable",detail:`the poll failed: ${snippet(S?.message??String(S))}`,polledAt:c},s.failures+=1,s.nextAt=c+sourceBackoffMs(f,s.failures),s.reportEveryMs=s.nextAt-c,A(s,!0)}finally{s.inFlight=!1,a-=1}},"pollOne"),I=u(async()=>{if(e.stopped?.())return;const s=new Set;for(const S of e.services())if(S.decl.sources!==void 0)for(const _ of R(S).sources){if(!isPolled(_))continue;const T=`${S.wsKey} ${S.project} ${_.kind} ${_.name}`;s.add(T);const x=r.get(T);x?(x.svc!==S||x.decl!==_)&&(x.svc=S,x.decl=_):r.set(T,{svc:S,decl:_,nextAt:0,inFlight:!1,failures:0,reportEveryMs:_.intervalSec*1e3})}for(const[S,_]of[...r.entries()])!s.has(S)&&!_.inFlight&&r.delete(S);const c=t(),f=[...r.values()].filter(S=>!S.inFlight&&c>=S.nextAt).sort((S,_)=>S.nextAt-_.nextAt),O=[];for(const S of f){if(a>=n)break;O.push(b(S))}await Promise.all(O)},"tick");return{tick:I,start(s=1e3){return p&&clearInterval(p),p=setInterval(()=>{I().catch(c=>console.error("[gateway] source poll error:",c))},s),p.unref(),p},stop(){p&&clearInterval(p),p=void 0},snapshot(){const s=t();return[...r.values()].map(c=>{const f=c.last!==void 0&&s-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,s-c.good.receivedAt)}:{}}})}}}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/
|
|
95
|
-
* waiting in its OWN storage (an outbox table), which no event can show
|
|
96
|
-
*
|
|
97
|
-
* (`
|
|
98
|
-
*
|
|
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
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* `
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.25.10",
|
|
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.
|
|
62
|
-
"@lensmcp/node-instrumentation": "1.25.
|
|
63
|
-
"@lensmcp/nx-plugin": "1.25.
|
|
61
|
+
"@lensmcp/core": "1.25.10",
|
|
62
|
+
"@lensmcp/node-instrumentation": "1.25.10",
|
|
63
|
+
"@lensmcp/nx-plugin": "1.25.10",
|
|
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",
|
package/worker-supervisor.d.ts
CHANGED
|
@@ -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;
|
package/worker-supervisor.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var k=Object.defineProperty;var a=(r,e)=>k(r,"name",{value:e,configurable:!0});var 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}:{}})))}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,p=typeof o?.name=="string"?o.name:"";if(!NAME_RE.test(p)){e.push(`worker ${JSON.stringify(o?.name??s)} is not a plain file name \u2014 not started`);continue}if(n.some(d=>d.name===p)){e.push(`worker "${p}" is declared twice \u2014 started once`);continue}const
|
|
1
|
+
"use strict";var k=Object.defineProperty;var a=(r,e)=>k(r,"name",{value:e,configurable:!0});var 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}:{}})))}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,p=typeof o?.name=="string"?o.name:"";if(!NAME_RE.test(p)){e.push(`worker ${JSON.stringify(o?.name??s)} is not a plain file name \u2014 not started`);continue}if(n.some(d=>d.name===p)){e.push(`worker "${p}" is declared twice \u2014 started once`);continue}const c={};for(const[d,l]of Object.entries(o?.env&&typeof o.env=="object"?o.env:{})){if(!ENV_KEY_RE.test(d)){e.push(`worker "${p}": env ${JSON.stringify(d)} is not a variable name \u2014 left out`);continue}typeof l=="string"||typeof l=="number"||typeof l=="boolean"?c[d]=String(l):e.push(`worker "${p}": env ${d} 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 "${p}": port ${JSON.stringify(o.port)} is not a port \u2014 it gets 0 (a free one)`)),n.push({name:p,env:c,port:u})}return{specs:n,problems:e}}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}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}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)}}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))}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"}}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`}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 p of r){const c=p.replace(ANSI_SGR,"");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)}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)}`}a(makeId,"makeId"),i(makeId,"makeId");function envelope(r,e,t,n,s,o,p,c){const u="pending";return{id:p(),sessionId:u,timestamp:o,source:"gateway",category:"cluster",severity:t,context:{sessionId:u,projectName:r.project},fingerprint:`cluster-${e}:${r.service}`,title:n,...c?{message:c}:{},raw:{kind:e,project:r.project,service:r.service,worker:r.worker,pid:r.pid,podGeneration:r.podGeneration,...s}}}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)}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)}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}`,p=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)}`,c=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",p,{exitCode:e.code,signal:e.signal,ranMs:e.ranMs,crashes:e.crashes,restarts:e.restarts,nextInMs:e.nextInMs,crashLooping:s,external:e.external,...c?{reading:c}:{},lines:u},t,n,u.length?u.join(`
|
|
2
2
|
`):void 0)}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)}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)}a(buildWorkerStoppedEvent,"buildWorkerStoppedEvent"),i(buildWorkerStoppedEvent,"buildWorkerStoppedEvent");
|