@lensmcp/bridge 1.16.30 → 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.
@@ -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;AAa1C,wBAAgB,cAAc,IAAI,OAAO,GAAG,IAAI,CAsC/C;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAgDhD"}
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
- appendFileSync(filePath, JSON.stringify(event) + '\n');
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.30",
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.30",
23
- "@lensmcp/protocol-types": "1.16.30",
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
  },