/** * Budgeted structural clone — the ONE bounded-copy implementation, shared by * every producer and consumer that must not let an unbounded payload through. * * WHY IT LIVES HERE: this walk was written for the CONSUMER side (`@lensmcp/storage` * clamps `raw` at ingest so the ring buffer, flow index and 20 reducers cannot all * hold the same multi-megabyte object). But the expensive half of that payload is * paid long before a consumer sees it — the producer serializes it, ships it over * the wire and appends it to the bus file. Bounding it at the PRODUCER needs the * same walk, and a second copy of these rules would drift from the first. Both * sides already depend on `@lensmcp/protocol-types`, so it is the only home that * adds no dependency edge, and it is deliberately free of runtime imports so a * browser bundle can carry it. * * THE PROPERTY THAT MATTERS: nothing here ever serializes the whole value to * measure or copy it. `JSON.parse(JSON.stringify(v))` — the idiom this replaces — * allocates the entire payload twice just to find out it was 4 MB. The walk sums * as it descends and stops at the budget, so a bounded clone of a huge object * costs a handful of property reads rather than the object. * * CORRECTNESS: consumers branch on discriminators that live inside the payload * (`raw.kind`, `update.storeId`, `render.componentInstanceId`), so the clamp NEVER * drops a key and never swaps a container for a scalar: it keeps every cheap entry * unconditionally, preserves object/array shape, and shrinks only the large leaves. * Past the budget it degrades to a SKELETON (scalars kept, arrays emptied, * containers still containers). The only wholesale replacement is at the recursion * ceiling, and it is a marker OBJECT — so a read off it yields `undefined`, never * a TypeError. */ /** * Per-event `raw` budget. Sits above the measured p99.9 (15.8 KiB) so no * normal diagnostic payload is ever touched, and far below the pathological * ones (87 KiB visual frames, an 8.27 MB valtio preview). */ export declare const DEFAULT_RAW_BYTE_LIMIT: number; /** Longest single string leaf kept intact (stack traces, base64 crops). */ export declare const DEFAULT_MAX_STRING_BYTES: number; /** * Longest array kept intact. Bounds the unbounded ones — `raw.report.chunks[].modules[]`, * `raw.frame.changedNodes[]`, `raw.render.why[]` — while leaving every array * observed in practice (≤ 64 entries) whole. */ export declare const DEFAULT_MAX_ARRAY_ITEMS = 64; /** * Recursion ceiling. Doubles as the cycle guard: events arriving over the * file/UDS/UDP transports are `JSON.parse` output (acyclic), but an * in-process producer could hand us a cyclic object, and a `Set`-based guard * would allocate on every event. A depth cap is total and allocation-free. */ export declare const DEFAULT_MAX_DEPTH = 12; /** * An entry estimated at or below this is "cheap" and kept unconditionally. * This is the correctness lever: `kind`, `storeId`, `path`, `componentName`, * `durationMs`, `floodSuppressed` and every other discriminator/identifier * are all far below it, so they survive regardless of how the budget runs out. */ export declare const CHEAP_ENTRY_BYTES = 512; /** Cost charged when ACCOUNTING stops at the depth ceiling. */ export declare const NOMINAL_DEEP_BYTES = 32; /** Depth allowed when reporting a stubbed value's true size (slow path only). */ export declare const DEEP_REPORT_DEPTH = 64; /** String prefix kept once the budget is spent (skeleton mode). */ export declare const SKELETON_STRING_BYTES = 256; /** How many truncated paths the marker lists, so the marker stays small. */ export declare const MAX_RECORDED_PATHS = 20; /** Key added to a truncated `raw` root describing what was cut. */ export declare const TRUNCATION_MARKER_KEY = "__lensmcpTruncation"; /** Key identifying a value that was replaced wholesale by a marker stub. */ export declare const TRUNCATED_VALUE_KEY = "__lensmcpTruncated"; /** Shape of the `raw[TRUNCATION_MARKER_KEY]` marker. */ export interface TruncationMarker { truncated: true; /** Approximate serialized size of the ORIGINAL payload. */ originalBytes: number; /** Approximate serialized size of what was kept. */ keptBytes: number; /** Paths that were shrunk, capped at {@link MAX_RECORDED_PATHS}. */ paths: string[]; } export interface ClampEventOptions { /** Per-event `raw` budget in bytes. Default {@link DEFAULT_RAW_BYTE_LIMIT}. */ rawByteLimit?: number; /** Longest string leaf kept intact. Default 4 KiB. */ maxStringBytes?: number; /** Longest array kept intact. Default 64. */ maxArrayItems?: number; /** Recursion ceiling / cycle guard. Default 12. */ maxDepth?: number; } /** * Approximate the serialized JSON size of a value WITHOUT serializing it. * * `JSON.stringify(x).length` allocates the entire string just to measure it — * an allocation storm when it runs per event at hundreds of events/second, * and pathological on the very payloads we are trying to bound (stringifying * an 8 MB preview to discover it is 8 MB). This walks the structure and sums * instead, and stops early once `budget` is exceeded, so the common case * costs a handful of property reads. * * @param budget Stop as soon as the running total exceeds this. The return * value is then a LOWER BOUND, which is all a threshold test needs. Pass * `Number.POSITIVE_INFINITY` when the exact figure is wanted (reporting). */ export declare function estimateJsonBytes(value: unknown, budget?: number, maxDepth?: number): number; /** * Is `value` bigger than `budget` — treating "too deep to measure" as YES? * * WHY this exists separately from {@link estimateJsonBytes}: the depth ceiling * makes a deeply nested payload look SMALL, and a size estimate that * under-reports defeats the guard that reads it. Measured while building this: * an 80 KB string nested 30 levels deep estimated at 1 300 B, so the clamp * never fired and the payload was retained whole — a hole big enough to * reintroduce the leak through one deeply nested `preview`. For an admission * DECISION the honest answer to "I can't see the bottom" is "assume it is too * big"; the clamp then descends and stubs at the ceiling. Accounting keeps the * bounded estimate, and is only ever applied to already-clamped events. */ export declare function exceedsJsonByteBudget(value: unknown, budget: number, maxDepth?: number): boolean; export interface ClampContext { budget: number; maxStringBytes: number; maxArrayItems: number; paths: string[]; truncatedAnything: boolean; } export declare function clampValue(value: unknown, ctx: ClampContext, depthLeft: number, path: string): unknown; /** What {@link boundedClone} produced, and what it cost. */ export interface BoundedCloneResult { /** The bounded copy. A plain JSON-safe structure — never the input by reference. */ value: T; /** Did anything get shrunk? */ truncated: boolean; /** Approximate serialized bytes of the ORIGINAL (a full walk, slow path only). */ originalBytes: number; /** Approximate serialized bytes of what was kept. */ keptBytes: number; /** Paths that were shrunk, capped at {@link MAX_RECORDED_PATHS}. */ paths: string[]; } /** * Clone `value` into a plain structure that fits a byte budget. * * This is the producer-side entry point. Use it wherever the old idiom was * `JSON.parse(JSON.stringify(v))` on a value whose size you do not control — * that idiom is unbounded in BOTH directions (it allocates the full string and * then the full clone), which is how a single state update became a 4.2 MB * event on the bus. * * Returns `{ value: undefined, truncated: false }` for a budget of `0`, so a * caller can disable previews entirely with one config value rather than a * branch at every call site. */ export declare function boundedClone(value: unknown, opts?: ClampEventOptions): BoundedCloneResult;