@lensmcp/protocol-types 1.21.5 → 1.22.0

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/index.d.ts CHANGED
@@ -9,3 +9,6 @@ export * from './lib/producer-health.js';
9
9
  export * from './lib/resources.js';
10
10
  export * from './lib/tokens.js';
11
11
  export * from './lib/capture-demand.js';
12
+ export * from './lib/bounded-clone.js';
13
+ export * from './lib/bounded-map.js';
14
+ export * from './lib/flood-guard.js';
package/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";export*from"./lib/schema-version.js";export*from"./lib/source-location.js";export*from"./lib/context.js";export*from"./lib/event.js";export*from"./lib/node.js";export*from"./lib/edge.js";export*from"./lib/attributes.js";export*from"./lib/producer-health.js";export*from"./lib/resources.js";export*from"./lib/tokens.js";export*from"./lib/capture-demand.js";
1
+ "use strict";export*from"./lib/schema-version.js";export*from"./lib/source-location.js";export*from"./lib/context.js";export*from"./lib/event.js";export*from"./lib/node.js";export*from"./lib/edge.js";export*from"./lib/attributes.js";export*from"./lib/producer-health.js";export*from"./lib/resources.js";export*from"./lib/tokens.js";export*from"./lib/capture-demand.js";export*from"./lib/bounded-clone.js";export*from"./lib/bounded-map.js";export*from"./lib/flood-guard.js";
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Budgeted structural clone — the ONE bounded-copy implementation, shared by
3
+ * every producer and consumer that must not let an unbounded payload through.
4
+ *
5
+ * WHY IT LIVES HERE: this walk was written for the CONSUMER side (`@lensmcp/storage`
6
+ * clamps `raw` at ingest so the ring buffer, flow index and 20 reducers cannot all
7
+ * hold the same multi-megabyte object). But the expensive half of that payload is
8
+ * paid long before a consumer sees it — the producer serializes it, ships it over
9
+ * the wire and appends it to the bus file. Bounding it at the PRODUCER needs the
10
+ * same walk, and a second copy of these rules would drift from the first. Both
11
+ * sides already depend on `@lensmcp/protocol-types`, so it is the only home that
12
+ * adds no dependency edge, and it is deliberately free of runtime imports so a
13
+ * browser bundle can carry it.
14
+ *
15
+ * THE PROPERTY THAT MATTERS: nothing here ever serializes the whole value to
16
+ * measure or copy it. `JSON.parse(JSON.stringify(v))` — the idiom this replaces —
17
+ * allocates the entire payload twice just to find out it was 4 MB. The walk sums
18
+ * as it descends and stops at the budget, so a bounded clone of a huge object
19
+ * costs a handful of property reads rather than the object.
20
+ *
21
+ * CORRECTNESS: consumers branch on discriminators that live inside the payload
22
+ * (`raw.kind`, `update.storeId`, `render.componentInstanceId`), so the clamp NEVER
23
+ * drops a key and never swaps a container for a scalar: it keeps every cheap entry
24
+ * unconditionally, preserves object/array shape, and shrinks only the large leaves.
25
+ * Past the budget it degrades to a SKELETON (scalars kept, arrays emptied,
26
+ * containers still containers). The only wholesale replacement is at the recursion
27
+ * ceiling, and it is a marker OBJECT — so a read off it yields `undefined`, never
28
+ * a TypeError.
29
+ */
30
+ /**
31
+ * Per-event `raw` budget. Sits above the measured p99.9 (15.8 KiB) so no
32
+ * normal diagnostic payload is ever touched, and far below the pathological
33
+ * ones (87 KiB visual frames, an 8.27 MB valtio preview).
34
+ */
35
+ export declare const DEFAULT_RAW_BYTE_LIMIT: number;
36
+ /** Longest single string leaf kept intact (stack traces, base64 crops). */
37
+ export declare const DEFAULT_MAX_STRING_BYTES: number;
38
+ /**
39
+ * Longest array kept intact. Bounds the unbounded ones — `raw.report.chunks[].modules[]`,
40
+ * `raw.frame.changedNodes[]`, `raw.render.why[]` — while leaving every array
41
+ * observed in practice (≤ 64 entries) whole.
42
+ */
43
+ export declare const DEFAULT_MAX_ARRAY_ITEMS = 64;
44
+ /**
45
+ * Recursion ceiling. Doubles as the cycle guard: events arriving over the
46
+ * file/UDS/UDP transports are `JSON.parse` output (acyclic), but an
47
+ * in-process producer could hand us a cyclic object, and a `Set`-based guard
48
+ * would allocate on every event. A depth cap is total and allocation-free.
49
+ */
50
+ export declare const DEFAULT_MAX_DEPTH = 12;
51
+ /**
52
+ * An entry estimated at or below this is "cheap" and kept unconditionally.
53
+ * This is the correctness lever: `kind`, `storeId`, `path`, `componentName`,
54
+ * `durationMs`, `floodSuppressed` and every other discriminator/identifier
55
+ * are all far below it, so they survive regardless of how the budget runs out.
56
+ */
57
+ export declare const CHEAP_ENTRY_BYTES = 512;
58
+ /** Cost charged when ACCOUNTING stops at the depth ceiling. */
59
+ export declare const NOMINAL_DEEP_BYTES = 32;
60
+ /** Depth allowed when reporting a stubbed value's true size (slow path only). */
61
+ export declare const DEEP_REPORT_DEPTH = 64;
62
+ /** String prefix kept once the budget is spent (skeleton mode). */
63
+ export declare const SKELETON_STRING_BYTES = 256;
64
+ /** How many truncated paths the marker lists, so the marker stays small. */
65
+ export declare const MAX_RECORDED_PATHS = 20;
66
+ /** Key added to a truncated `raw` root describing what was cut. */
67
+ export declare const TRUNCATION_MARKER_KEY = "__lensmcpTruncation";
68
+ /** Key identifying a value that was replaced wholesale by a marker stub. */
69
+ export declare const TRUNCATED_VALUE_KEY = "__lensmcpTruncated";
70
+ /** Shape of the `raw[TRUNCATION_MARKER_KEY]` marker. */
71
+ export interface TruncationMarker {
72
+ truncated: true;
73
+ /** Approximate serialized size of the ORIGINAL payload. */
74
+ originalBytes: number;
75
+ /** Approximate serialized size of what was kept. */
76
+ keptBytes: number;
77
+ /** Paths that were shrunk, capped at {@link MAX_RECORDED_PATHS}. */
78
+ paths: string[];
79
+ }
80
+ export interface ClampEventOptions {
81
+ /** Per-event `raw` budget in bytes. Default {@link DEFAULT_RAW_BYTE_LIMIT}. */
82
+ rawByteLimit?: number;
83
+ /** Longest string leaf kept intact. Default 4 KiB. */
84
+ maxStringBytes?: number;
85
+ /** Longest array kept intact. Default 64. */
86
+ maxArrayItems?: number;
87
+ /** Recursion ceiling / cycle guard. Default 12. */
88
+ maxDepth?: number;
89
+ }
90
+ /**
91
+ * Approximate the serialized JSON size of a value WITHOUT serializing it.
92
+ *
93
+ * `JSON.stringify(x).length` allocates the entire string just to measure it —
94
+ * an allocation storm when it runs per event at hundreds of events/second,
95
+ * and pathological on the very payloads we are trying to bound (stringifying
96
+ * an 8 MB preview to discover it is 8 MB). This walks the structure and sums
97
+ * instead, and stops early once `budget` is exceeded, so the common case
98
+ * costs a handful of property reads.
99
+ *
100
+ * @param budget Stop as soon as the running total exceeds this. The return
101
+ * value is then a LOWER BOUND, which is all a threshold test needs. Pass
102
+ * `Number.POSITIVE_INFINITY` when the exact figure is wanted (reporting).
103
+ */
104
+ export declare function estimateJsonBytes(value: unknown, budget?: number, maxDepth?: number): number;
105
+ /**
106
+ * Is `value` bigger than `budget` — treating "too deep to measure" as YES?
107
+ *
108
+ * WHY this exists separately from {@link estimateJsonBytes}: the depth ceiling
109
+ * makes a deeply nested payload look SMALL, and a size estimate that
110
+ * under-reports defeats the guard that reads it. Measured while building this:
111
+ * an 80 KB string nested 30 levels deep estimated at 1 300 B, so the clamp
112
+ * never fired and the payload was retained whole — a hole big enough to
113
+ * reintroduce the leak through one deeply nested `preview`. For an admission
114
+ * DECISION the honest answer to "I can't see the bottom" is "assume it is too
115
+ * big"; the clamp then descends and stubs at the ceiling. Accounting keeps the
116
+ * bounded estimate, and is only ever applied to already-clamped events.
117
+ */
118
+ export declare function exceedsJsonByteBudget(value: unknown, budget: number, maxDepth?: number): boolean;
119
+ export interface ClampContext {
120
+ budget: number;
121
+ maxStringBytes: number;
122
+ maxArrayItems: number;
123
+ paths: string[];
124
+ truncatedAnything: boolean;
125
+ }
126
+ export declare function clampValue(value: unknown, ctx: ClampContext, depthLeft: number, path: string): unknown;
127
+ /** What {@link boundedClone} produced, and what it cost. */
128
+ export interface BoundedCloneResult<T = unknown> {
129
+ /** The bounded copy. A plain JSON-safe structure — never the input by reference. */
130
+ value: T;
131
+ /** Did anything get shrunk? */
132
+ truncated: boolean;
133
+ /** Approximate serialized bytes of the ORIGINAL (a full walk, slow path only). */
134
+ originalBytes: number;
135
+ /** Approximate serialized bytes of what was kept. */
136
+ keptBytes: number;
137
+ /** Paths that were shrunk, capped at {@link MAX_RECORDED_PATHS}. */
138
+ paths: string[];
139
+ }
140
+ /**
141
+ * Clone `value` into a plain structure that fits a byte budget.
142
+ *
143
+ * This is the producer-side entry point. Use it wherever the old idiom was
144
+ * `JSON.parse(JSON.stringify(v))` on a value whose size you do not control —
145
+ * that idiom is unbounded in BOTH directions (it allocates the full string and
146
+ * then the full clone), which is how a single state update became a 4.2 MB
147
+ * event on the bus.
148
+ *
149
+ * Returns `{ value: undefined, truncated: false }` for a budget of `0`, so a
150
+ * caller can disable previews entirely with one config value rather than a
151
+ * branch at every call site.
152
+ */
153
+ export declare function boundedClone<T = unknown>(value: unknown, opts?: ClampEventOptions): BoundedCloneResult<T | undefined>;
@@ -0,0 +1 @@
1
+ "use strict";var T=Object.defineProperty;var i=(e,t)=>T(e,"name",{value:t,configurable:!0});var h=Object.defineProperty,l=i((e,t)=>h(e,"name",{value:t,configurable:!0}),"_");export const DEFAULT_RAW_BYTE_LIMIT=32768,DEFAULT_MAX_STRING_BYTES=4096,DEFAULT_MAX_ARRAY_ITEMS=64,DEFAULT_MAX_DEPTH=12,CHEAP_ENTRY_BYTES=512,NOMINAL_DEEP_BYTES=32,DEEP_REPORT_DEPTH=64,SKELETON_STRING_BYTES=256,MAX_RECORDED_PATHS=20,TRUNCATION_MARKER_KEY="__lensmcpTruncation",TRUNCATED_VALUE_KEY="__lensmcpTruncated";export function estimateJsonBytes(e,t=Number.POSITIVE_INFINITY,r=12){return y(e,t,r,32)}i(estimateJsonBytes,"estimateJsonBytes"),l(estimateJsonBytes,"estimateJsonBytes");export function exceedsJsonByteBudget(e,t,r=12){return y(e,t,r,Number.POSITIVE_INFINITY)>t}i(exceedsJsonByteBudget,"exceedsJsonByteBudget"),l(exceedsJsonByteBudget,"exceedsJsonByteBudget");function y(e,t,r,a){switch(typeof e){case"undefined":case"function":case"symbol":return 0;case"boolean":return 5;case"number":return Number.isFinite(e)?8:4;case"bigint":return 20;case"string":return e.length+2;default:break}if(e===null)return 4;if(r<=0)return a;if(e instanceof Date)return 26;if(Array.isArray(e)){let n=2;for(let s=0;s<e.length;s++)if(n+=y(e[s],t-n,r-1,a)+1,n>t)return n;return n}let u=2;const o=e;for(const n in o){if(!Object.prototype.hasOwnProperty.call(o,n))continue;const s=y(o[n],t-u,r-1,a);if(s!==0&&(u+=n.length+4+s,u>t))return u}return u}i(y,"T"),l(y,"walk");function g(e,t){e.truncatedAnything=!0,e.paths.length<20&&e.paths.push(t||"<root>")}i(g,"A"),l(g,"record");function f(e,t,r){return g(e,r),{[TRUNCATED_VALUE_KEY]:!0,originalBytes:estimateJsonBytes(t,Number.POSITIVE_INFINITY,64)}}i(f,"I"),l(f,"stub");function p(e){if(e===null)return!0;const t=typeof e;return t==="number"||t==="boolean"||t==="string"||t==="bigint"}i(p,"y"),l(p,"isScalar");export function clampValue(e,t,r,a){if(typeof e=="string"){const u=t.budget<=0?256:t.maxStringBytes;if(e.length+2<=u)return t.budget-=e.length+2,e;const o=Math.max(0,u-64);return g(t,a),t.budget-=o,`${e.slice(0,o)}\u2026[lensmcp truncated: ${e.length-o} more chars]`}return e===null||typeof e!="object"?(t.budget-=y(e,Number.POSITIVE_INFINITY,1,32),e):r<=0?f(t,e,a):e instanceof Date?(t.budget-=26,e):Array.isArray(e)?m(e,t,r,a):b(e,t,r,a)}i(clampValue,"clampValue"),l(clampValue,"clampValue");function m(e,t,r,a){const u=[],o=Math.min(e.length,t.maxArrayItems);for(let n=0;n<o&&!(t.budget<=0);n++)u.push(clampValue(e[n],t,r-1,`${a}[${n}]`));return u.length<e.length&&g(t,`${a}[] (+${e.length-u.length} items dropped)`),u}i(m,"f"),l(m,"clampArray");function b(e,t,r,a){const u={};let o;const n=t.budget<=0;for(const s in e){if(!Object.prototype.hasOwnProperty.call(e,s))continue;const c=e[s],I=y(c,512,r-1,Number.POSITIVE_INFINITY);if(I<=512&&(!n||p(c))){u[s]=c,t.budget-=s.length+4+I;continue}(o??=[]).push({key:s,bytes:I})}if(!o)return u;o.sort((s,c)=>s.bytes===c.bytes?0:s.bytes<c.bytes?-1:1);for(const{key:s}of o){const c=a?`${a}.${s}`:s;u[s]=clampValue(e[s],t,r-1,c)}return u}i(b,"m"),l(b,"clampObject");export function boundedClone(e,t={}){const r=t.rawByteLimit??32768,a=t.maxDepth??12;if(r<=0)return{value:void 0,truncated:!1,originalBytes:0,keptBytes:0,paths:[]};if(e==null)return{value:e,truncated:!1,originalBytes:0,keptBytes:0,paths:[]};if(!exceedsJsonByteBudget(e,r,a)){const c=estimateJsonBytes(e,r,a),I={budget:r,maxStringBytes:t.maxStringBytes??4096,maxArrayItems:t.maxArrayItems??64,paths:[],truncatedAnything:!1};return{value:clampValue(e,I,a,""),truncated:!1,originalBytes:c,keptBytes:c,paths:[]}}const u=estimateJsonBytes(e,Number.POSITIVE_INFINITY,64),o={budget:r,maxStringBytes:t.maxStringBytes??4096,maxArrayItems:t.maxArrayItems??64,paths:[],truncatedAnything:!1},n=clampValue(e,o,a,""),s=estimateJsonBytes(n,Number.POSITIVE_INFINITY,a);if(o.truncatedAnything&&n!==null&&typeof n=="object"&&!Array.isArray(n)){const c={truncated:!0,originalBytes:u,keptBytes:s,paths:o.paths};n[TRUNCATION_MARKER_KEY]=c}return{value:n,truncated:o.truncatedAnything,originalBytes:u,keptBytes:s,paths:o.paths}}i(boundedClone,"boundedClone"),l(boundedClone,"boundedClone");
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A cap for a `Map` keyed on something a producer chooses.
3
+ *
4
+ * Several reducers kept a session-lifetime `Map` keyed on a value taken straight off an event —
5
+ * `raw.project`, `raw.tool`, a rule id — with no cap and no eviction. Because the bridge accepts a
6
+ * `publish` envelope with an arbitrary `raw` and no origin check, a page the developer visited could
7
+ * grow those maps one entry per message: `db.analyzeRuns` (issues/codex-security/19), the
8
+ * `producers` registries in typecheck/lint/build (21), `visual.rules` (64), and the memory
9
+ * reducer's `ownerNodeId` map (41).
10
+ *
11
+ * The existing bounds did not cover it. The `raw` clamp bounds ONE payload, not the number of
12
+ * distinct keys, and the flood guard is per-fingerprint — so unique fingerprints, which the same
13
+ * envelope chooses, step around it. That is the mechanism this repo already wrote down as
14
+ * *"fingerprint is the flood bucket"*.
15
+ *
16
+ * Eviction is insertion-order: JS `Map` iterates in insertion order, so the first key is the oldest.
17
+ * That matches the cap `db.reducer.ts` already applied to its query shapes, and it is the right
18
+ * policy for these collections — the newest producer is the one a developer is looking at.
19
+ */
20
+ /**
21
+ * Trim `map` to at most `max` entries, dropping the oldest first.
22
+ *
23
+ * Returns the number of entries evicted, so a caller can report pressure rather than hiding it.
24
+ */
25
+ export declare function capMap(map: Map<string, unknown>, max: number): number;
26
+ /**
27
+ * Default ceiling for a producer-keyed registry.
28
+ *
29
+ * Far above what a real workspace produces — a large monorepo has tens of projects and a handful of
30
+ * tools — and low enough that a forged stream cannot exhaust the process before the cap bites.
31
+ */
32
+ export declare const MAX_PRODUCER_KEYS = 256;
@@ -0,0 +1 @@
1
+ "use strict";var c=Object.defineProperty;var t=(e,r)=>c(e,"name",{value:r,configurable:!0});var i=Object.defineProperty,s=t((e,r)=>i(e,"name",{value:r,configurable:!0}),"o");export function capMap(e,r){if(e.size<=r)return 0;let n=0;const o=e.keys();for(;e.size>r;){const a=o.next();if(a.done)break;e.delete(a.value)&&(n+=1)}return n}t(capMap,"capMap"),s(capMap,"capMap");export const MAX_PRODUCER_KEYS=256;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Per-generation cap. Override with `LENSMCP_EVENT_MAX_BYTES` (default 50 MB); 0/invalid ⇒ default.
3
+ */
4
+ export declare function eventMaxBytes(env?: NodeJS.ProcessEnv): number;
5
+ /**
6
+ * Rotated generations kept as history (`.1` … `.N`, newest → oldest). With the live file that bounds
7
+ * a workspace to (N+1) × the cap — ~300 MB at defaults. Override with `LENSMCP_EVENT_MAX_FILES`.
8
+ */
9
+ export declare function eventMaxFiles(env?: NodeJS.ProcessEnv): number;
10
+ /**
11
+ * Rotate `file` if it has outgrown the per-generation cap.
12
+ *
13
+ * Drops the oldest generation, shifts `.1` → `.2` → … → `.N`, and renames the live file to `.1`;
14
+ * the next append recreates it. Best-effort throughout — many processes append to one path, and the
15
+ * final `renameSync` is atomic, so a writer that loses the race throws ENOENT and no-ops. A racing
16
+ * shift can at worst mis-order HISTORY generations; it can never touch the live file.
17
+ *
18
+ * `caps` exists for tests, and passing it also bypasses the throttle so a test need not append a
19
+ * megabyte to observe a rotation.
20
+ */
21
+ export declare function maybeRotateEventFile(file: string, addedBytes: number, caps?: {
22
+ maxBytes?: number;
23
+ maxFiles?: number;
24
+ }): void;
25
+ /** Test seam: forget the throttle state for a path (or all paths). */
26
+ export declare function resetRotationThrottle(file?: string): void;
@@ -0,0 +1 @@
1
+ "use strict";var y=Object.defineProperty;var r=(e,t)=>y(e,"name",{value:t,configurable:!0});var v=Object.defineProperty,o=r((e,t)=>v(e,"name",{value:t,configurable:!0}),"c");import{renameSync as l,statSync as f,unlinkSync as x}from"node:fs";export function eventMaxBytes(e=process.env){const t=Number(e.LENSMCP_EVENT_MAX_BYTES);return Number.isFinite(t)&&t>0?t:50*1024*1024}r(eventMaxBytes,"eventMaxBytes"),o(eventMaxBytes,"eventMaxBytes");export function eventMaxFiles(e=process.env){const t=Number(e.LENSMCP_EVENT_MAX_FILES);return Number.isFinite(t)&&t>=1&&t<=20?Math.floor(t):5}r(eventMaxFiles,"eventMaxFiles"),o(eventMaxFiles,"eventMaxFiles");const E=1024*1024,a=new Map;export function maybeRotateEventFile(e,t,i){const s=(a.get(e)??0)+t,c=i?.maxBytes??eventMaxBytes(),u=i?.maxFiles??eventMaxFiles(),m=Math.min(E,c);if(s<m){a.set(e,s);return}try{const n=f(e).size;if(a.set(e,0),n<=c)return}catch{a.set(e,s);return}try{x(`${e}.${u}`)}catch{}for(let n=u-1;n>=1;n--)try{l(`${e}.${n}`,`${e}.${n+1}`)}catch{}try{l(e,`${e}.1`)}catch{}}r(maybeRotateEventFile,"maybeRotateEventFile"),o(maybeRotateEventFile,"maybeRotateEventFile");export function resetRotationThrottle(e){e===void 0?a.clear():a.delete(e)}r(resetRotationThrottle,"resetRotationThrottle"),o(resetRotationThrottle,"resetRotationThrottle");
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Per-fingerprint event flood guard
3
+ *
4
+ * Lives in `protocol-types` because EVERY producer needs it — node taps, the Nest
5
+ * sink, the gateway, the bridge, and (since the admission decision must happen
6
+ * BEFORE a payload is built) the browser runtime. It is pure logic with no Node
7
+ * APIs, so a browser bundle can carry it. — a token bucket per fingerprint, shared by
8
+ * every bus WRITER (lensEmit, the cluster gateway's observability emit, the
9
+ * bridge env sink mirrors it). One hot fingerprint (an app retrying a fetch in
10
+ * a loop, a valtio store churning per frame) must not be able to write hundreds
11
+ * of lines/sec into events.jsonl: the 2026-07-28 incident sustained ~680 ev/s,
12
+ * grew the bus file to 254 MB in two hours, and OOM-crash-looped every consumer
13
+ * that ingests it. The guard keeps the SIGNAL (the first burst, then a trickle
14
+ * with a `floodSuppressed` count) while dropping the redundant repetition.
15
+ */
16
+ export interface FloodAdmit {
17
+ ok: boolean;
18
+ /** Events dropped for this fingerprint since the last admitted one (0 normally). */
19
+ suppressed: number;
20
+ }
21
+ export interface FloodGuard {
22
+ admit(fingerprint: string): FloodAdmit;
23
+ }
24
+ export interface FloodGuardOptions {
25
+ /** Instantaneous burst allowance per fingerprint (bucket capacity). Default 30. */
26
+ burst?: number;
27
+ /** Sustained events/sec per fingerprint (bucket refill). Default 5. */
28
+ perSec?: number;
29
+ /** Max distinct fingerprints tracked before stale buckets are evicted. Default 2048. */
30
+ maxKeys?: number;
31
+ }
32
+ export declare function createEventFloodGuard(opts?: FloodGuardOptions): FloodGuard;
@@ -0,0 +1 @@
1
+ "use strict";var m=Object.defineProperty;var i=(t,n)=>m(t,"name",{value:n,configurable:!0});var v=Object.defineProperty,u=i((t,n)=>v(t,"name",{value:n,configurable:!0}),"o");function p(){const t=typeof process<"u"?process.env?.LENSMCP_EVENT_MAX_EPS:void 0;if(t===void 0||t==="")return;if(t==="off")return"off";const n=Number(t);if(Number.isFinite(n))return n<=0?"off":n}i(p,"S"),u(p,"envPerSec");export function createEventFloodGuard(t){const n=p();if(n==="off")return{admit:u(()=>({ok:!0,suppressed:0}),"admit")};const l=n??t?.perSec??5,f=Math.max(1,t?.burst??Math.max(30,l*4)),c=t?.maxKeys??2048,r=new Map,d=u(o=>{for(const[s,e]of r)o-e.lastSeenAt>6e4&&r.delete(s);if(r.size>c){const s=[...r.entries()].sort((e,a)=>e[1].lastSeenAt-a[1].lastSeenAt);for(let e=0;e<s.length>>1;e++)r.delete(s[e][0])}},"evictStale");return{admit(o){const s=Date.now();let e=r.get(o);e||(r.size>=c&&d(s),e={tokens:f,lastRefillAt:s,suppressed:0,lastSeenAt:s},r.set(o,e)),e.lastSeenAt=s;const a=s-e.lastRefillAt;if(a>0&&(e.tokens=Math.min(f,e.tokens+a/1e3*l),e.lastRefillAt=s),e.tokens>=1){e.tokens-=1;const S=e.suppressed;return e.suppressed=0,{ok:!0,suppressed:S}}return e.suppressed+=1,{ok:!1,suppressed:0}}}}i(createEventFloodGuard,"createEventFloodGuard"),u(createEventFloodGuard,"createEventFloodGuard");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/protocol-types",
3
- "version": "1.21.5",
3
+ "version": "1.22.0",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -11,6 +11,11 @@
11
11
  "types": "./index.d.ts",
12
12
  "import": "./index.js",
13
13
  "default": "./index.js"
14
+ },
15
+ "./bounded-writer": {
16
+ "types": "./lib/bounded-writer.d.ts",
17
+ "import": "./lib/bounded-writer.js",
18
+ "default": "./lib/bounded-writer.js"
14
19
  }
15
20
  },
16
21
  "dependencies": {