@sylphx/sdk 0.7.0 → 0.8.0-rc.1
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 +25 -0
- package/dist/health/index.js +521 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/index.mjs +485 -0
- package/dist/health/index.mjs.map +1 -0
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.js +7 -7
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +7 -7
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +2 -2
- package/dist/server/index.mjs.map +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -393,6 +393,31 @@ const plans = await getPlans(config)
|
|
|
393
393
|
| `@sylphx/sdk/react` | React hooks, components, `SylphxProvider` |
|
|
394
394
|
| `@sylphx/sdk/server` | JWT verification, webhook verification, server client |
|
|
395
395
|
| `@sylphx/sdk/nextjs` | `createSylphxMiddleware`, `auth()`, `currentUser()` |
|
|
396
|
+
| `@sylphx/sdk/web-analytics` | Standalone web-analytics tracker (rrweb + web-vitals) |
|
|
397
|
+
| `@sylphx/sdk/health` | Multi-signal health score for the `sylphx-health-agent` sidecar (ADR-111 Phase B) |
|
|
398
|
+
|
|
399
|
+
### `@sylphx/sdk/health` — Phase B health score (ADR-111)
|
|
400
|
+
|
|
401
|
+
Apps register signals (event-loop lag, queue depth, error rate, memory pressure)
|
|
402
|
+
and the SDK folds them into a continuous score in `[0, 1]`. The
|
|
403
|
+
`sylphx-health-agent` sidecar polls the score and decides liveness /
|
|
404
|
+
readiness / drain via the three-tier gate. See
|
|
405
|
+
[`src/health/README.md`](./src/health/README.md) for the full guide.
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import { sylphxHealth, eventLoopLagSignal, queueDepthSignal } from '@sylphx/sdk/health'
|
|
409
|
+
|
|
410
|
+
const health = sylphxHealth({
|
|
411
|
+
signals: [
|
|
412
|
+
eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 }),
|
|
413
|
+
queueDepthSignal({ getter: () => queue.size, fullThreshold: 1000 }),
|
|
414
|
+
],
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
app.get('/healthz', health.handler())
|
|
418
|
+
// Or — Unix-socket transport for the sidecar:
|
|
419
|
+
health.serveUnixSocket() // → /var/run/sylphx/health.sock
|
|
420
|
+
```
|
|
396
421
|
|
|
397
422
|
---
|
|
398
423
|
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/health/index.ts
|
|
21
|
+
var health_exports = {};
|
|
22
|
+
__export(health_exports, {
|
|
23
|
+
HealthError: () => HealthError,
|
|
24
|
+
createNodeHandler: () => createNodeHandler,
|
|
25
|
+
createWebHandler: () => createWebHandler,
|
|
26
|
+
defaultScoringStrategy: () => defaultScoringStrategy,
|
|
27
|
+
errorRateSignal: () => errorRateSignal,
|
|
28
|
+
eventLoopLagSignal: () => eventLoopLagSignal,
|
|
29
|
+
memoryPressureSignal: () => memoryPressureSignal,
|
|
30
|
+
queueDepthSignal: () => queueDepthSignal,
|
|
31
|
+
sylphxHealth: () => sylphxHealth,
|
|
32
|
+
weightedProduct: () => weightedProduct
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(health_exports);
|
|
35
|
+
|
|
36
|
+
// src/health/effects.ts
|
|
37
|
+
var import_effect = require("effect");
|
|
38
|
+
|
|
39
|
+
// src/health/types.ts
|
|
40
|
+
var HealthError = class extends Error {
|
|
41
|
+
_tag = "HealthError";
|
|
42
|
+
cause;
|
|
43
|
+
constructor(message, cause) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "HealthError";
|
|
46
|
+
this.cause = cause;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/health/effects.ts
|
|
51
|
+
function evaluateEffect(evaluator) {
|
|
52
|
+
return import_effect.Effect.tryPromise({
|
|
53
|
+
try: () => evaluator(),
|
|
54
|
+
catch: (err) => new HealthError("health evaluation failed", err)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/health/handler.ts
|
|
59
|
+
function createWebHandler(source) {
|
|
60
|
+
return async (_req) => {
|
|
61
|
+
try {
|
|
62
|
+
const score = await source.evaluate();
|
|
63
|
+
return new Response(JSON.stringify(score), {
|
|
64
|
+
status: 200,
|
|
65
|
+
headers: {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
// Cache-Control: never cache. Even 1-second cache on a
|
|
68
|
+
// CDN edge would mask a fast-moving health collapse.
|
|
69
|
+
"Cache-Control": "no-store, no-cache, must-revalidate"
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
74
|
+
return new Response(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
error: "health_evaluator_failed",
|
|
77
|
+
message
|
|
78
|
+
}),
|
|
79
|
+
{
|
|
80
|
+
status: 500,
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
"Cache-Control": "no-store, no-cache, must-revalidate"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function createNodeHandler(source) {
|
|
91
|
+
return async (_req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const score = await source.evaluate();
|
|
94
|
+
res.statusCode = 200;
|
|
95
|
+
res.setHeader("Content-Type", "application/json");
|
|
96
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
97
|
+
res.end(JSON.stringify(score));
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
res.statusCode = 500;
|
|
101
|
+
res.setHeader("Content-Type", "application/json");
|
|
102
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
103
|
+
res.end(JSON.stringify({ error: "health_evaluator_failed", message }));
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/health/scoring.ts
|
|
109
|
+
function clamp01(x) {
|
|
110
|
+
if (!Number.isFinite(x)) return 0;
|
|
111
|
+
if (x < 0) return 0;
|
|
112
|
+
if (x > 1) return 1;
|
|
113
|
+
return x;
|
|
114
|
+
}
|
|
115
|
+
var weightedProduct = (readings) => {
|
|
116
|
+
if (readings.length === 0) return 1;
|
|
117
|
+
const active = [];
|
|
118
|
+
for (const { signal, reading } of readings) {
|
|
119
|
+
const w = signal.weight;
|
|
120
|
+
if (!Number.isFinite(w) || w <= 0) continue;
|
|
121
|
+
if (reading.unknown === true) continue;
|
|
122
|
+
active.push({
|
|
123
|
+
factor: clamp01(reading.healthFactor),
|
|
124
|
+
weight: w
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (active.length === 0) return 1;
|
|
128
|
+
const totalWeight = active.reduce((sum, a) => sum + a.weight, 0);
|
|
129
|
+
if (totalWeight <= 0 || !Number.isFinite(totalWeight)) return 1;
|
|
130
|
+
let logSum = 0;
|
|
131
|
+
for (const { factor, weight } of active) {
|
|
132
|
+
const normalisedWeight = weight / totalWeight;
|
|
133
|
+
if (factor <= 0) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
logSum += normalisedWeight * Math.log(factor);
|
|
137
|
+
}
|
|
138
|
+
const score = Math.exp(logSum);
|
|
139
|
+
return clamp01(score);
|
|
140
|
+
};
|
|
141
|
+
function defaultScoringStrategy() {
|
|
142
|
+
return weightedProduct;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/health/signals/event-loop-lag.ts
|
|
146
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
147
|
+
function eventLoopLagSignal(opts = {}) {
|
|
148
|
+
const degradedMs = opts.degradedMs ?? 5e3;
|
|
149
|
+
const deadMs = opts.deadMs ?? 3e4;
|
|
150
|
+
const resolutionMs = opts.resolutionMs ?? 10;
|
|
151
|
+
const weight = opts.weight ?? 0.4;
|
|
152
|
+
if (!Number.isFinite(degradedMs) || degradedMs < 0) {
|
|
153
|
+
throw new Error(`eventLoopLagSignal: degradedMs must be >= 0, got ${degradedMs}`);
|
|
154
|
+
}
|
|
155
|
+
if (!Number.isFinite(deadMs) || deadMs <= degradedMs) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`eventLoopLagSignal: deadMs must be > degradedMs (${degradedMs}), got ${deadMs}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (!Number.isFinite(resolutionMs) || resolutionMs < 1) {
|
|
161
|
+
throw new Error(`eventLoopLagSignal: resolutionMs must be >= 1, got ${resolutionMs}`);
|
|
162
|
+
}
|
|
163
|
+
const monitor = opts.monitor ?? (0, import_node_perf_hooks.monitorEventLoopDelay)({ resolution: resolutionMs });
|
|
164
|
+
monitor.enable();
|
|
165
|
+
return {
|
|
166
|
+
name: "eventLoopLagMs",
|
|
167
|
+
weight,
|
|
168
|
+
read() {
|
|
169
|
+
const maxNs = monitor.max;
|
|
170
|
+
if (!Number.isFinite(maxNs) || maxNs <= 0 || maxNs >= Number.MAX_SAFE_INTEGER) {
|
|
171
|
+
monitor.reset();
|
|
172
|
+
return { value: 0, healthFactor: 1, unknown: true };
|
|
173
|
+
}
|
|
174
|
+
const observedMs = maxNs / 1e6;
|
|
175
|
+
monitor.reset();
|
|
176
|
+
let factor;
|
|
177
|
+
if (observedMs <= degradedMs) factor = 1;
|
|
178
|
+
else if (observedMs >= deadMs) factor = 0;
|
|
179
|
+
else {
|
|
180
|
+
const span = deadMs - degradedMs;
|
|
181
|
+
factor = 1 - (observedMs - degradedMs) / span;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
value: Math.round(observedMs),
|
|
185
|
+
healthFactor: factor
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
dispose() {
|
|
189
|
+
monitor.disable();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/health/unix-socket-server.ts
|
|
195
|
+
var import_node_fs = require("fs");
|
|
196
|
+
function startUnixSocketServer(source, opts = {}) {
|
|
197
|
+
const bun = opts.bunRuntime !== void 0 ? opts.bunRuntime : globalThis.Bun;
|
|
198
|
+
if (bun === null || bun === void 0 || typeof bun.serve !== "function") {
|
|
199
|
+
throw new Error(
|
|
200
|
+
"startUnixSocketServer: Bun.serve is unavailable \u2014 Unix-socket transport requires a Bun runtime"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const path = opts.path ?? "/var/run/sylphx/health.sock";
|
|
204
|
+
const unlinkOnShutdown = opts.unlinkOnShutdown ?? true;
|
|
205
|
+
try {
|
|
206
|
+
(0, import_node_fs.unlinkSync)(path);
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
const handler = createWebHandler(source);
|
|
210
|
+
const server = bun.serve({
|
|
211
|
+
unix: path,
|
|
212
|
+
fetch: async (req) => handler(req)
|
|
213
|
+
});
|
|
214
|
+
const handle = {
|
|
215
|
+
path,
|
|
216
|
+
server,
|
|
217
|
+
async shutdown() {
|
|
218
|
+
try {
|
|
219
|
+
server.stop(true);
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
if (unlinkOnShutdown) {
|
|
223
|
+
try {
|
|
224
|
+
(0, import_node_fs.unlinkSync)(path);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
if (opts.installSignalHandlers === true) {
|
|
231
|
+
const onSignal = (sig) => {
|
|
232
|
+
void handle.shutdown().then(() => {
|
|
233
|
+
if (typeof process !== "undefined") process.exit(0);
|
|
234
|
+
});
|
|
235
|
+
void sig;
|
|
236
|
+
};
|
|
237
|
+
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
238
|
+
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
239
|
+
}
|
|
240
|
+
return handle;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/health/signals/error-rate.ts
|
|
244
|
+
function parseWindow(w) {
|
|
245
|
+
if (typeof w === "number") {
|
|
246
|
+
if (!Number.isFinite(w) || w <= 0) {
|
|
247
|
+
throw new Error(`errorRateSignal: window must be > 0, got ${w}`);
|
|
248
|
+
}
|
|
249
|
+
return w;
|
|
250
|
+
}
|
|
251
|
+
const m = /^(\d+)(s|m)$/.exec(w);
|
|
252
|
+
if (m === null) {
|
|
253
|
+
throw new Error(`errorRateSignal: invalid window '${w}', expected '5s' / '1m' / number`);
|
|
254
|
+
}
|
|
255
|
+
const n = Number.parseInt(m[1], 10);
|
|
256
|
+
const unit = m[2];
|
|
257
|
+
const ms = unit === "s" ? n * 1e3 : n * 6e4;
|
|
258
|
+
if (ms <= 0) {
|
|
259
|
+
throw new Error(`errorRateSignal: parsed window ${ms}ms must be > 0`);
|
|
260
|
+
}
|
|
261
|
+
return ms;
|
|
262
|
+
}
|
|
263
|
+
function errorRateSignal(opts) {
|
|
264
|
+
const windowMs = parseWindow(opts.window);
|
|
265
|
+
const degradedRate = opts.degradedRate ?? 0.05;
|
|
266
|
+
const deadRate = opts.deadRate ?? 0.5;
|
|
267
|
+
const minSamples = opts.minSamples ?? 10;
|
|
268
|
+
const weight = opts.weight ?? 0.2;
|
|
269
|
+
const now = opts.now ?? Date.now;
|
|
270
|
+
if (degradedRate < 0 || degradedRate > 1) {
|
|
271
|
+
throw new Error(`errorRateSignal: degradedRate must be in [0, 1], got ${degradedRate}`);
|
|
272
|
+
}
|
|
273
|
+
if (deadRate <= degradedRate || deadRate > 1) {
|
|
274
|
+
throw new Error(`errorRateSignal: deadRate must be in (degradedRate, 1], got ${deadRate}`);
|
|
275
|
+
}
|
|
276
|
+
if (!Number.isFinite(minSamples) || minSamples < 1) {
|
|
277
|
+
throw new Error(`errorRateSignal: minSamples must be >= 1, got ${minSamples}`);
|
|
278
|
+
}
|
|
279
|
+
const samples = [];
|
|
280
|
+
function pruneExpired(t) {
|
|
281
|
+
const cutoff = t - windowMs;
|
|
282
|
+
while (samples.length > 0 && samples[0].t < cutoff) {
|
|
283
|
+
samples.shift();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
name: `recent${Math.round(windowMs / 1e3)}sErrorRate`,
|
|
288
|
+
weight,
|
|
289
|
+
recordSuccess() {
|
|
290
|
+
samples.push({ t: now(), e: false });
|
|
291
|
+
},
|
|
292
|
+
recordError() {
|
|
293
|
+
samples.push({ t: now(), e: true });
|
|
294
|
+
},
|
|
295
|
+
reset() {
|
|
296
|
+
samples.length = 0;
|
|
297
|
+
},
|
|
298
|
+
read() {
|
|
299
|
+
const t = now();
|
|
300
|
+
pruneExpired(t);
|
|
301
|
+
if (samples.length === 0 || samples.length < minSamples) {
|
|
302
|
+
return { value: 0, healthFactor: 1 };
|
|
303
|
+
}
|
|
304
|
+
let errors = 0;
|
|
305
|
+
for (const s of samples) {
|
|
306
|
+
if (s.e) errors++;
|
|
307
|
+
}
|
|
308
|
+
const rate = errors / samples.length;
|
|
309
|
+
let factor;
|
|
310
|
+
if (rate <= degradedRate) factor = 1;
|
|
311
|
+
else if (rate >= deadRate) factor = 0;
|
|
312
|
+
else {
|
|
313
|
+
const span = deadRate - degradedRate;
|
|
314
|
+
factor = 1 - (rate - degradedRate) / span;
|
|
315
|
+
}
|
|
316
|
+
const valueRounded = Math.round(rate * 1e4) / 1e4;
|
|
317
|
+
return {
|
|
318
|
+
value: valueRounded,
|
|
319
|
+
healthFactor: factor
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/health/signals/memory-pressure.ts
|
|
326
|
+
var import_node_fs2 = require("fs");
|
|
327
|
+
var CGROUP_V1_UNLIMITED_FLOOR = 1e18;
|
|
328
|
+
function tryRead(reader, path) {
|
|
329
|
+
try {
|
|
330
|
+
return reader(path);
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function readContainerMemoryLimit(reader, v2Path, v1Path) {
|
|
336
|
+
const v2 = tryRead(reader, v2Path);
|
|
337
|
+
if (v2 !== null) {
|
|
338
|
+
const trimmed2 = v2.trim();
|
|
339
|
+
if (trimmed2 === "max" || trimmed2 === "") return null;
|
|
340
|
+
const n2 = Number.parseInt(trimmed2, 10);
|
|
341
|
+
if (!Number.isFinite(n2) || n2 <= 0) return null;
|
|
342
|
+
if (n2 > CGROUP_V1_UNLIMITED_FLOOR) return null;
|
|
343
|
+
return n2;
|
|
344
|
+
}
|
|
345
|
+
const v1 = tryRead(reader, v1Path);
|
|
346
|
+
if (v1 === null) return null;
|
|
347
|
+
const trimmed = v1.trim();
|
|
348
|
+
const n = Number.parseInt(trimmed, 10);
|
|
349
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
350
|
+
if (n > CGROUP_V1_UNLIMITED_FLOOR) return null;
|
|
351
|
+
return n;
|
|
352
|
+
}
|
|
353
|
+
function memoryPressureSignal(opts = {}) {
|
|
354
|
+
const degradedRatio = opts.degradedRatio ?? 0.85;
|
|
355
|
+
const deadRatio = opts.deadRatio ?? 0.95;
|
|
356
|
+
const v2Path = opts.cgroupV2Path ?? "/sys/fs/cgroup/memory.max";
|
|
357
|
+
const v1Path = opts.cgroupV1Path ?? "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
358
|
+
const weight = opts.weight ?? 0.2;
|
|
359
|
+
const memoryUsage = opts.memoryUsage ?? (() => {
|
|
360
|
+
const m = process.memoryUsage();
|
|
361
|
+
return { rss: m.rss };
|
|
362
|
+
});
|
|
363
|
+
const readFile = opts.readFile ?? ((p) => (0, import_node_fs2.readFileSync)(p, "utf8"));
|
|
364
|
+
if (degradedRatio <= 0 || degradedRatio >= 1) {
|
|
365
|
+
throw new Error(`memoryPressureSignal: degradedRatio must be in (0, 1), got ${degradedRatio}`);
|
|
366
|
+
}
|
|
367
|
+
if (deadRatio <= degradedRatio || deadRatio > 1) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`memoryPressureSignal: deadRatio must be in (degradedRatio, 1], got ${deadRatio}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
let cachedLimit;
|
|
373
|
+
function getLimit() {
|
|
374
|
+
if (cachedLimit === void 0) {
|
|
375
|
+
cachedLimit = readContainerMemoryLimit(readFile, v2Path, v1Path);
|
|
376
|
+
}
|
|
377
|
+
return cachedLimit;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
name: "memoryPressure",
|
|
381
|
+
weight,
|
|
382
|
+
read() {
|
|
383
|
+
const limit = getLimit();
|
|
384
|
+
if (limit === null) {
|
|
385
|
+
return { value: 0, healthFactor: 1, unknown: true };
|
|
386
|
+
}
|
|
387
|
+
let rss;
|
|
388
|
+
try {
|
|
389
|
+
rss = memoryUsage().rss;
|
|
390
|
+
} catch {
|
|
391
|
+
return { value: 0, healthFactor: 1, unknown: true };
|
|
392
|
+
}
|
|
393
|
+
if (!Number.isFinite(rss) || rss < 0) {
|
|
394
|
+
return { value: 0, healthFactor: 1, unknown: true };
|
|
395
|
+
}
|
|
396
|
+
const ratio = rss / limit;
|
|
397
|
+
let factor;
|
|
398
|
+
if (ratio <= degradedRatio) factor = 1;
|
|
399
|
+
else if (ratio >= deadRatio) factor = 0;
|
|
400
|
+
else {
|
|
401
|
+
const span = deadRatio - degradedRatio;
|
|
402
|
+
factor = 1 - (ratio - degradedRatio) / span;
|
|
403
|
+
}
|
|
404
|
+
const valueRounded = Math.round(ratio * 1e3) / 1e3;
|
|
405
|
+
return { value: valueRounded, healthFactor: factor };
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/health/signals/queue-depth.ts
|
|
411
|
+
function queueDepthSignal(opts) {
|
|
412
|
+
if (typeof opts.getter !== "function") {
|
|
413
|
+
throw new Error("queueDepthSignal: getter must be a function");
|
|
414
|
+
}
|
|
415
|
+
if (!Number.isFinite(opts.fullThreshold) || opts.fullThreshold <= 0) {
|
|
416
|
+
throw new Error(`queueDepthSignal: fullThreshold must be > 0, got ${opts.fullThreshold}`);
|
|
417
|
+
}
|
|
418
|
+
const healthyBelow = opts.healthyBelow ?? 0;
|
|
419
|
+
if (!Number.isFinite(healthyBelow) || healthyBelow < 0 || healthyBelow >= opts.fullThreshold) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`queueDepthSignal: healthyBelow must be in [0, fullThreshold), got ${healthyBelow}`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const weight = opts.weight ?? 0.2;
|
|
425
|
+
return {
|
|
426
|
+
name: opts.name ?? "queueDepth",
|
|
427
|
+
weight,
|
|
428
|
+
async read() {
|
|
429
|
+
let raw;
|
|
430
|
+
try {
|
|
431
|
+
raw = await opts.getter();
|
|
432
|
+
} catch {
|
|
433
|
+
return { value: 0, healthFactor: 1, unknown: true };
|
|
434
|
+
}
|
|
435
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) {
|
|
436
|
+
return { value: 0, healthFactor: 1, unknown: true };
|
|
437
|
+
}
|
|
438
|
+
const depth = raw;
|
|
439
|
+
let factor;
|
|
440
|
+
if (depth <= healthyBelow) factor = 1;
|
|
441
|
+
else if (depth >= opts.fullThreshold) factor = 0;
|
|
442
|
+
else {
|
|
443
|
+
const span = opts.fullThreshold - healthyBelow;
|
|
444
|
+
factor = 1 - (depth - healthyBelow) / span;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
value: depth,
|
|
448
|
+
healthFactor: factor
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/health/index.ts
|
|
455
|
+
function sylphxHealth(opts = {}) {
|
|
456
|
+
const signals = opts.signals && opts.signals.length > 0 ? [...opts.signals] : [eventLoopLagSignal({ degradedMs: 5e3, deadMs: 3e4, weight: 1 })];
|
|
457
|
+
const scoringStrategy = opts.scoringStrategy ?? defaultScoringStrategy();
|
|
458
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
459
|
+
let disposed = false;
|
|
460
|
+
const evaluate = async () => {
|
|
461
|
+
if (disposed) {
|
|
462
|
+
throw new Error("sylphxHealth: instance disposed");
|
|
463
|
+
}
|
|
464
|
+
const readings = await Promise.all(
|
|
465
|
+
signals.map(async (signal) => ({
|
|
466
|
+
signal,
|
|
467
|
+
reading: await signal.read()
|
|
468
|
+
}))
|
|
469
|
+
);
|
|
470
|
+
const score = scoringStrategy(readings);
|
|
471
|
+
const signalsMap = {};
|
|
472
|
+
for (const { signal, reading } of readings) {
|
|
473
|
+
signalsMap[signal.name] = reading.value;
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
score,
|
|
477
|
+
signals: signalsMap,
|
|
478
|
+
lastTickAt: now().toISOString()
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
const evaluator = { evaluate };
|
|
482
|
+
const evaluateEffect2 = evaluateEffect(evaluate);
|
|
483
|
+
return {
|
|
484
|
+
signals,
|
|
485
|
+
evaluate,
|
|
486
|
+
evaluateEffect: evaluateEffect2,
|
|
487
|
+
handler() {
|
|
488
|
+
return createWebHandler(evaluator);
|
|
489
|
+
},
|
|
490
|
+
nodeHandler() {
|
|
491
|
+
return createNodeHandler(evaluator);
|
|
492
|
+
},
|
|
493
|
+
serveUnixSocket(unixOpts) {
|
|
494
|
+
return startUnixSocketServer(evaluator, unixOpts);
|
|
495
|
+
},
|
|
496
|
+
dispose() {
|
|
497
|
+
if (disposed) return;
|
|
498
|
+
disposed = true;
|
|
499
|
+
for (const s of signals) {
|
|
500
|
+
try {
|
|
501
|
+
s.dispose?.();
|
|
502
|
+
} catch {
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
509
|
+
0 && (module.exports = {
|
|
510
|
+
HealthError,
|
|
511
|
+
createNodeHandler,
|
|
512
|
+
createWebHandler,
|
|
513
|
+
defaultScoringStrategy,
|
|
514
|
+
errorRateSignal,
|
|
515
|
+
eventLoopLagSignal,
|
|
516
|
+
memoryPressureSignal,
|
|
517
|
+
queueDepthSignal,
|
|
518
|
+
sylphxHealth,
|
|
519
|
+
weightedProduct
|
|
520
|
+
});
|
|
521
|
+
//# sourceMappingURL=index.js.map
|