@buoy-gg/console 7.0.29 → 7.0.30

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,83 +0,0 @@
1
- /**
2
- * sanitizeArgs — deep-convert captured console arguments into JSON-safe values.
3
- *
4
- * Captured `args` can hold Errors, functions, bigints, symbols, and circular
5
- * references. Both the external-sync snapshot (device → desktop) and the
6
- * preserve-log persistence need plain JSON, so this produces a structural copy
7
- * (objects/arrays preserved so they stay expandable) with cycles broken.
8
- *
9
- * Strings are capped: a single `console.log` of a multi-MB payload (API dump,
10
- * base64, …) used to ride every snapshot uncapped and drop the whole console
11
- * panel at the 2MB emit budget.
12
- */
13
-
14
- const MAX_DEPTH = 6;
15
- const MAX_ARRAY = 100;
16
- const MAX_KEYS = 100;
17
-
18
- /** Max chars of one string on the wire / in the persisted buffer. */
19
- export const SNAPSHOT_STRING_LIMIT = 2 * 1024;
20
-
21
- export function capString(
22
- value: string,
23
- limit: number = SNAPSHOT_STRING_LIMIT,
24
- ): string {
25
- if (value.length <= limit) return value;
26
- return `${value.slice(0, limit)}… [${value.length - limit} more]`;
27
- }
28
-
29
- function sanitizeValue(value: unknown, depth: number, seen: WeakSet<object>): unknown {
30
- if (value === null) return null;
31
- // Checked inline rather than through a `const t = typeof value` alias: the
32
- // alias does not narrow `value`, so the string branch could not be handed to
33
- // a `(s: string)` function without a cast — which is how this file stopped
34
- // compiling.
35
- if (typeof value === "string") return capString(value);
36
- if (typeof value === "boolean") return value;
37
- if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
38
- if (typeof value === "undefined") return "undefined";
39
- if (typeof value === "bigint") return `${String(value)}n`;
40
- if (typeof value === "function") {
41
- return `ƒ ${(value as { name?: string }).name || "(anonymous)"}`;
42
- }
43
- if (typeof value === "symbol") return value.toString();
44
-
45
- if (value instanceof Error) {
46
- return {
47
- name: value.name,
48
- message: capString(value.message),
49
- stack: value.stack ? capString(value.stack, 4 * 1024) : value.stack,
50
- };
51
- }
52
-
53
- if (depth >= MAX_DEPTH) return Array.isArray(value) ? "[Array]" : "{…}";
54
-
55
- const obj = value as object;
56
- if (seen.has(obj)) return "[Circular]";
57
- seen.add(obj);
58
-
59
- try {
60
- if (Array.isArray(value)) {
61
- const out = value.slice(0, MAX_ARRAY).map((v) => sanitizeValue(v, depth + 1, seen));
62
- if (value.length > MAX_ARRAY) out.push(`… ${value.length - MAX_ARRAY} more`);
63
- return out;
64
- }
65
- const out: Record<string, unknown> = {};
66
- for (const key of Object.keys(obj).slice(0, MAX_KEYS)) {
67
- try {
68
- out[key] = sanitizeValue((obj as Record<string, unknown>)[key], depth + 1, seen);
69
- } catch {
70
- out[key] = "[Unserializable]";
71
- }
72
- }
73
- return out;
74
- } finally {
75
- seen.delete(obj);
76
- }
77
- }
78
-
79
- /** Deep-sanitize an array of console arguments into JSON-safe values. */
80
- export function sanitizeArgs(args: unknown[]): unknown[] {
81
- const seen = new WeakSet<object>();
82
- return args.map((arg) => sanitizeValue(arg, 0, seen));
83
- }
@@ -1,115 +0,0 @@
1
- /**
2
- * consoleSyncAdapter — streams captured console entries from the device to the
3
- * desktop dashboard via @buoy-gg/external-sync.
4
- *
5
- * Structurally matches external-sync's `ToolSyncAdapter` WITHOUT importing the
6
- * package (so console has no hard dependency on external-sync, matching how the
7
- * other tools' adapters are written). Auto-discovery in @buoy-gg/core picks it
8
- * up by the export name and announces the "console" capability.
9
- *
10
- * Snapshots MUST be JSON-serializable. Captured `args` can hold Errors,
11
- * functions, bigints, and circular references, so we deep-sanitize them into
12
- * plain JSON values (objects/arrays preserved so they stay expandable on the
13
- * dashboard). Oversized strings and the pre-rendered `message` are capped so
14
- * a single fat `console.log` cannot drop the whole panel at the 2MB emit
15
- * budget; the on-device store keeps the raw args.
16
- */
17
-
18
- import {
19
- subscribe,
20
- getEntries,
21
- clearEntries,
22
- type ConsoleLogEntry,
23
- } from "../store/consoleLogStore";
24
- import { approxJsonSize } from "@buoy-gg/shared-ui";
25
- import { capString, sanitizeArgs } from "../store/sanitize";
26
-
27
- interface ConsoleSnapshot {
28
- entries: ConsoleLogEntry[];
29
- }
30
-
31
- /** Soft cap so a burst of fat logs still fits under the 2MB emit budget. */
32
- const MAX_SNAPSHOT_BYTES = 1.5 * 1024 * 1024;
33
- /** One entry's args/message after string caps; fatter args collapse to a stub. */
34
- const MAX_ENTRY_BYTES = 16 * 1024;
35
-
36
- const ARGS_OMITTED = Object.freeze({
37
- __buoyTruncated: true,
38
- note: "Log payload omitted — inspect it on-device.",
39
- });
40
-
41
- interface WireEntry {
42
- entry: ConsoleLogEntry;
43
- bytes: number;
44
- }
45
-
46
- /**
47
- * Per-entry wire cache. A captured entry is never mutated — the store only
48
- * appends and trims — so keying on identity is correct and self-invalidating.
49
- *
50
- * This is the whole cost of the tool. `sanitizeArgs` deep-copies every arg to
51
- * make it JSON-safe, and without a cache that copy is redone for all 1000
52
- * entries on every snapshot, ~5x/sec: measured at 12.6ms per tick on ordinary
53
- * object logs, and it was that expensive BEFORE any of the size guards
54
- * existed. Converting each entry once removes it from the hot path entirely.
55
- */
56
- const wireCache = new WeakMap<ConsoleLogEntry, WireEntry>();
57
-
58
- function toWireEntry(entry: ConsoleLogEntry): WireEntry {
59
- const cached = wireCache.get(entry);
60
- if (cached) return cached;
61
-
62
- const wired: ConsoleLogEntry = {
63
- ...entry,
64
- args: sanitizeArgs(entry.args),
65
- message: capString(entry.message),
66
- stack: entry.stack ? capString(entry.stack, 4 * 1024) : entry.stack,
67
- };
68
- // One walk decides both "is this entry too fat" and what it costs the
69
- // snapshot budget — the previous code walked every entry twice.
70
- const size = approxJsonSize(wired, MAX_ENTRY_BYTES);
71
- const result: WireEntry =
72
- size.bytes <= MAX_ENTRY_BYTES
73
- ? { entry: wired, bytes: size.bytes }
74
- : (() => {
75
- const stub = { ...wired, args: [ARGS_OMITTED] };
76
- return {
77
- entry: stub,
78
- bytes: approxJsonSize(stub, MAX_ENTRY_BYTES).bytes,
79
- };
80
- })();
81
- wireCache.set(entry, result);
82
- return result;
83
- }
84
-
85
- function newestFitting(entries: ConsoleLogEntry[]): ConsoleLogEntry[] {
86
- const out: ConsoleLogEntry[] = [];
87
- let bytes = 16; // {"entries":[]}
88
- for (let i = entries.length - 1; i >= 0; i--) {
89
- const wired = toWireEntry(entries[i]);
90
- if (bytes + wired.bytes > MAX_SNAPSHOT_BYTES && out.length > 0) break;
91
- out.push(wired.entry);
92
- bytes += wired.bytes;
93
- }
94
- out.reverse();
95
- return out;
96
- }
97
-
98
- export const consoleSyncAdapter = {
99
- version: 1,
100
-
101
- getSnapshot(): ConsoleSnapshot {
102
- return { entries: newestFitting(getEntries()) };
103
- },
104
-
105
- subscribe(onChange: () => void): () => void {
106
- return subscribe(onChange);
107
- },
108
-
109
- actions: {
110
- clearEntries(): { cleared: true } {
111
- clearEntries();
112
- return { cleared: true };
113
- },
114
- },
115
- };