@lensmcp/bridge 1.16.29 → 1.16.31
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/lib/env-sink.d.ts.map +1 -1
- package/lib/env-sink.js +93 -2
- package/package.json +3 -3
package/lib/env-sink.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-sink.d.ts","sourceRoot":"","sources":["../../src/lib/env-sink.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"env-sink.d.ts","sourceRoot":"","sources":["../../src/lib/env-sink.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAwF1C,wBAAgB,cAAc,IAAI,OAAO,GAAG,IAAI,CA6C/C;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAgDhD"}
|
package/lib/env-sink.js
CHANGED
|
@@ -1,6 +1,92 @@
|
|
|
1
1
|
import { connect } from 'node:net';
|
|
2
2
|
import { createSocket } from 'node:dgram';
|
|
3
|
-
import { appendFileSync } from 'node:fs';
|
|
3
|
+
import { appendFileSync, renameSync, statSync, unlinkSync } from 'node:fs';
|
|
4
|
+
// ---- file-sink hygiene — mirrors @lensmcp/node-instrumentation's emit.ts contract ------------------
|
|
5
|
+
// (bridge deliberately has no dep on node-instrumentation, so the ~40 lines are mirrored, same env
|
|
6
|
+
// vars.) Browser producers are the highest-volume writers on the bus — a valtio store churning per
|
|
7
|
+
// frame or a fetch retry loop must not grow events.jsonl unboundedly (2026-07-28: ~680 ev/s, 254 MB
|
|
8
|
+
// in 2h, every ingesting consumer OOM-crash-looped). Per-fingerprint token bucket + round-robin
|
|
9
|
+
// rotation: live file caps at MAX_EVENT_BYTES, history kept as `.1` … `.N`, oldest dropped.
|
|
10
|
+
/** Same cap contract as emit.ts: `LENSMCP_EVENT_MAX_BYTES` (default 50 MB) per generation. */
|
|
11
|
+
const MAX_EVENT_BYTES = (() => {
|
|
12
|
+
const n = Number(process.env['LENSMCP_EVENT_MAX_BYTES']);
|
|
13
|
+
return Number.isFinite(n) && n > 0 ? n : 50 * 1024 * 1024;
|
|
14
|
+
})();
|
|
15
|
+
/** Same history contract as emit.ts: `LENSMCP_EVENT_MAX_FILES` rotated generations (default 5). */
|
|
16
|
+
const MAX_EVENT_FILES = (() => {
|
|
17
|
+
const n = Number(process.env['LENSMCP_EVENT_MAX_FILES']);
|
|
18
|
+
return Number.isFinite(n) && n >= 1 && n <= 20 ? Math.floor(n) : 5;
|
|
19
|
+
})();
|
|
20
|
+
const ROTATE_CHECK_BYTES = 1024 * 1024; // stat at most ~once per MB appended
|
|
21
|
+
let bytesSinceCheck = 0;
|
|
22
|
+
function rotateIfLarge(file, addedBytes) {
|
|
23
|
+
bytesSinceCheck += addedBytes;
|
|
24
|
+
if (bytesSinceCheck < ROTATE_CHECK_BYTES)
|
|
25
|
+
return;
|
|
26
|
+
bytesSinceCheck = 0;
|
|
27
|
+
try {
|
|
28
|
+
if (statSync(file).size <= MAX_EVENT_BYTES)
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return; /* missing / already rotated by a concurrent writer — fine */
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
unlinkSync(`${file}.${MAX_EVENT_FILES}`); // drop the oldest generation
|
|
36
|
+
}
|
|
37
|
+
catch { /* none to drop */ }
|
|
38
|
+
for (let gen = MAX_EVENT_FILES - 1; gen >= 1; gen--) {
|
|
39
|
+
try {
|
|
40
|
+
renameSync(`${file}.${gen}`, `${file}.${gen + 1}`);
|
|
41
|
+
}
|
|
42
|
+
catch { /* gap in history */ }
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
renameSync(file, `${file}.1`);
|
|
46
|
+
}
|
|
47
|
+
catch { /* lost the rotate race — a concurrent cascade ran */ }
|
|
48
|
+
}
|
|
49
|
+
/** Per-fingerprint token bucket (burst 30, then 5/s; `LENSMCP_EVENT_MAX_EPS` overrides, 0/'off' disables). */
|
|
50
|
+
const FLOOD_PER_SEC = (() => {
|
|
51
|
+
const raw = process.env['LENSMCP_EVENT_MAX_EPS'];
|
|
52
|
+
if (raw === 'off')
|
|
53
|
+
return Infinity;
|
|
54
|
+
const n = Number(raw);
|
|
55
|
+
if (raw !== undefined && raw !== '' && Number.isFinite(n))
|
|
56
|
+
return n <= 0 ? Infinity : n;
|
|
57
|
+
return 5;
|
|
58
|
+
})();
|
|
59
|
+
const FLOOD_BURST = Math.max(30, FLOOD_PER_SEC === Infinity ? 0 : FLOOD_PER_SEC * 4);
|
|
60
|
+
const floodBuckets = new Map();
|
|
61
|
+
/** Returns `-1` to drop, else the suppressed-since-last-admit count to annotate (0 normally). */
|
|
62
|
+
function floodAdmit(fingerprint) {
|
|
63
|
+
if (FLOOD_PER_SEC === Infinity)
|
|
64
|
+
return 0;
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
let b = floodBuckets.get(fingerprint);
|
|
67
|
+
if (!b) {
|
|
68
|
+
if (floodBuckets.size >= 2048) {
|
|
69
|
+
for (const [k, v] of floodBuckets)
|
|
70
|
+
if (now - v.lastSeenAt > 60_000)
|
|
71
|
+
floodBuckets.delete(k);
|
|
72
|
+
if (floodBuckets.size >= 2048)
|
|
73
|
+
floodBuckets.clear();
|
|
74
|
+
}
|
|
75
|
+
b = { tokens: FLOOD_BURST, lastRefillAt: now, suppressed: 0, lastSeenAt: now };
|
|
76
|
+
floodBuckets.set(fingerprint, b);
|
|
77
|
+
}
|
|
78
|
+
b.lastSeenAt = now;
|
|
79
|
+
b.tokens = Math.min(FLOOD_BURST, b.tokens + ((now - b.lastRefillAt) / 1000) * FLOOD_PER_SEC);
|
|
80
|
+
b.lastRefillAt = now;
|
|
81
|
+
if (b.tokens >= 1) {
|
|
82
|
+
b.tokens -= 1;
|
|
83
|
+
const suppressed = b.suppressed;
|
|
84
|
+
b.suppressed = 0;
|
|
85
|
+
return suppressed;
|
|
86
|
+
}
|
|
87
|
+
b.suppressed += 1;
|
|
88
|
+
return -1;
|
|
89
|
+
}
|
|
4
90
|
/**
|
|
5
91
|
* Resolve a cross-process event sink from env, mirroring
|
|
6
92
|
* `@lensmcp/nest-instrumentation`'s `defaultEventSink` so every producer
|
|
@@ -18,7 +104,12 @@ export function resolveEnvSink() {
|
|
|
18
104
|
if (filePath) {
|
|
19
105
|
cachedEnvSink = (event) => {
|
|
20
106
|
try {
|
|
21
|
-
|
|
107
|
+
const suppressed = floodAdmit(event.fingerprint);
|
|
108
|
+
if (suppressed < 0)
|
|
109
|
+
return; // flood-guarded — next admitted event carries the count
|
|
110
|
+
const line = JSON.stringify(suppressed > 0 ? { ...event, raw: { ...(event.raw ?? {}), floodSuppressed: suppressed } } : event) + '\n';
|
|
111
|
+
rotateIfLarge(filePath, line.length);
|
|
112
|
+
appendFileSync(filePath, line);
|
|
22
113
|
}
|
|
23
114
|
catch {
|
|
24
115
|
/* swallow */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/bridge",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@lensmcp/core": "1.16.
|
|
23
|
-
"@lensmcp/protocol-types": "1.16.
|
|
22
|
+
"@lensmcp/core": "1.16.31",
|
|
23
|
+
"@lensmcp/protocol-types": "1.16.31",
|
|
24
24
|
"tslib": "^2.3.0",
|
|
25
25
|
"ws": "^8.21.0"
|
|
26
26
|
},
|