@buoy-gg/console 7.0.17 → 7.0.19

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.
@@ -3,6 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.SNAPSHOT_STRING_LIMIT = void 0;
7
+ exports.capString = capString;
6
8
  exports.sanitizeArgs = sanitizeArgs;
7
9
  /**
8
10
  * sanitizeArgs — deep-convert captured console arguments into JSON-safe values.
@@ -11,25 +13,42 @@ exports.sanitizeArgs = sanitizeArgs;
11
13
  * references. Both the external-sync snapshot (device → desktop) and the
12
14
  * preserve-log persistence need plain JSON, so this produces a structural copy
13
15
  * (objects/arrays preserved so they stay expandable) with cycles broken.
16
+ *
17
+ * Strings are capped: a single `console.log` of a multi-MB payload (API dump,
18
+ * base64, …) used to ride every snapshot uncapped and drop the whole console
19
+ * panel at the 2MB emit budget.
14
20
  */
15
21
 
16
22
  const MAX_DEPTH = 6;
17
23
  const MAX_ARRAY = 100;
18
24
  const MAX_KEYS = 100;
25
+
26
+ /** Max chars of one string on the wire / in the persisted buffer. */
27
+ const SNAPSHOT_STRING_LIMIT = exports.SNAPSHOT_STRING_LIMIT = 2 * 1024;
28
+ function capString(value, limit = SNAPSHOT_STRING_LIMIT) {
29
+ if (value.length <= limit) return value;
30
+ return `${value.slice(0, limit)}… [${value.length - limit} more]`;
31
+ }
19
32
  function sanitizeValue(value, depth, seen) {
20
33
  if (value === null) return null;
21
- const t = typeof value;
22
- if (t === "string" || t === "boolean") return value;
23
- if (t === "number") return Number.isFinite(value) ? value : String(value);
24
- if (t === "undefined") return "undefined";
25
- if (t === "bigint") return `${String(value)}n`;
26
- if (t === "function") return `ƒ ${value.name || "(anonymous)"}`;
27
- if (t === "symbol") return value.toString();
34
+ // Checked inline rather than through a `const t = typeof value` alias: the
35
+ // alias does not narrow `value`, so the string branch could not be handed to
36
+ // a `(s: string)` function without a cast — which is how this file stopped
37
+ // compiling.
38
+ if (typeof value === "string") return capString(value);
39
+ if (typeof value === "boolean") return value;
40
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
41
+ if (typeof value === "undefined") return "undefined";
42
+ if (typeof value === "bigint") return `${String(value)}n`;
43
+ if (typeof value === "function") {
44
+ return `ƒ ${value.name || "(anonymous)"}`;
45
+ }
46
+ if (typeof value === "symbol") return value.toString();
28
47
  if (value instanceof Error) {
29
48
  return {
30
49
  name: value.name,
31
- message: value.message,
32
- stack: value.stack
50
+ message: capString(value.message),
51
+ stack: value.stack ? capString(value.stack, 4 * 1024) : value.stack
33
52
  };
34
53
  }
35
54
  if (depth >= MAX_DEPTH) return Array.isArray(value) ? "[Array]" : "{…}";
@@ -1 +1 @@
1
- {"version":3,"names":["MAX_DEPTH","MAX_ARRAY","MAX_KEYS","sanitizeValue","value","depth","seen","t","Number","isFinite","String","name","toString","Error","message","stack","Array","isArray","obj","has","add","out","slice","map","v","length","push","key","Object","keys","delete","sanitizeArgs","args","WeakSet","arg"],"sourceRoot":"../../../src","sources":["store/sanitize.ts"],"mappings":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,MAAMA,SAAS,GAAG,CAAC;AACnB,MAAMC,SAAS,GAAG,GAAG;AACrB,MAAMC,QAAQ,GAAG,GAAG;AAEpB,SAASC,aAAaA,CAACC,KAAc,EAAEC,KAAa,EAAEC,IAAqB,EAAW;EACpF,IAAIF,KAAK,KAAK,IAAI,EAAE,OAAO,IAAI;EAC/B,MAAMG,CAAC,GAAG,OAAOH,KAAK;EACtB,IAAIG,CAAC,KAAK,QAAQ,IAAIA,CAAC,KAAK,SAAS,EAAE,OAAOH,KAAK;EACnD,IAAIG,CAAC,KAAK,QAAQ,EAAE,OAAOC,MAAM,CAACC,QAAQ,CAACL,KAAe,CAAC,GAAGA,KAAK,GAAGM,MAAM,CAACN,KAAK,CAAC;EACnF,IAAIG,CAAC,KAAK,WAAW,EAAE,OAAO,WAAW;EACzC,IAAIA,CAAC,KAAK,QAAQ,EAAE,OAAO,GAAGG,MAAM,CAACN,KAAK,CAAC,GAAG;EAC9C,IAAIG,CAAC,KAAK,UAAU,EAAE,OAAO,KAAMH,KAAK,CAAuBO,IAAI,IAAI,aAAa,EAAE;EACtF,IAAIJ,CAAC,KAAK,QAAQ,EAAE,OAAQH,KAAK,CAAYQ,QAAQ,CAAC,CAAC;EAEvD,IAAIR,KAAK,YAAYS,KAAK,EAAE;IAC1B,OAAO;MAAEF,IAAI,EAAEP,KAAK,CAACO,IAAI;MAAEG,OAAO,EAAEV,KAAK,CAACU,OAAO;MAAEC,KAAK,EAAEX,KAAK,CAACW;IAAM,CAAC;EACzE;EAEA,IAAIV,KAAK,IAAIL,SAAS,EAAE,OAAOgB,KAAK,CAACC,OAAO,CAACb,KAAK,CAAC,GAAG,SAAS,GAAG,KAAK;EAEvE,MAAMc,GAAG,GAAGd,KAAe;EAC3B,IAAIE,IAAI,CAACa,GAAG,CAACD,GAAG,CAAC,EAAE,OAAO,YAAY;EACtCZ,IAAI,CAACc,GAAG,CAACF,GAAG,CAAC;EAEb,IAAI;IACF,IAAIF,KAAK,CAACC,OAAO,CAACb,KAAK,CAAC,EAAE;MACxB,MAAMiB,GAAG,GAAGjB,KAAK,CAACkB,KAAK,CAAC,CAAC,EAAErB,SAAS,CAAC,CAACsB,GAAG,CAAEC,CAAC,IAAKrB,aAAa,CAACqB,CAAC,EAAEnB,KAAK,GAAG,CAAC,EAAEC,IAAI,CAAC,CAAC;MACnF,IAAIF,KAAK,CAACqB,MAAM,GAAGxB,SAAS,EAAEoB,GAAG,CAACK,IAAI,CAAC,KAAKtB,KAAK,CAACqB,MAAM,GAAGxB,SAAS,OAAO,CAAC;MAC5E,OAAOoB,GAAG;IACZ;IACA,MAAMA,GAA4B,GAAG,CAAC,CAAC;IACvC,KAAK,MAAMM,GAAG,IAAIC,MAAM,CAACC,IAAI,CAACX,GAAG,CAAC,CAACI,KAAK,CAAC,CAAC,EAAEpB,QAAQ,CAAC,EAAE;MACrD,IAAI;QACFmB,GAAG,CAACM,GAAG,CAAC,GAAGxB,aAAa,CAAEe,GAAG,CAA6BS,GAAG,CAAC,EAAEtB,KAAK,GAAG,CAAC,EAAEC,IAAI,CAAC;MAClF,CAAC,CAAC,MAAM;QACNe,GAAG,CAACM,GAAG,CAAC,GAAG,kBAAkB;MAC/B;IACF;IACA,OAAON,GAAG;EACZ,CAAC,SAAS;IACRf,IAAI,CAACwB,MAAM,CAACZ,GAAG,CAAC;EAClB;AACF;;AAEA;AACO,SAASa,YAAYA,CAACC,IAAe,EAAa;EACvD,MAAM1B,IAAI,GAAG,IAAI2B,OAAO,CAAS,CAAC;EAClC,OAAOD,IAAI,CAACT,GAAG,CAAEW,GAAG,IAAK/B,aAAa,CAAC+B,GAAG,EAAE,CAAC,EAAE5B,IAAI,CAAC,CAAC;AACvD","ignoreList":[]}
1
+ {"version":3,"names":["MAX_DEPTH","MAX_ARRAY","MAX_KEYS","SNAPSHOT_STRING_LIMIT","exports","capString","value","limit","length","slice","sanitizeValue","depth","seen","Number","isFinite","String","name","toString","Error","message","stack","Array","isArray","obj","has","add","out","map","v","push","key","Object","keys","delete","sanitizeArgs","args","WeakSet","arg"],"sourceRoot":"../../../src","sources":["store/sanitize.ts"],"mappings":"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","ignoreList":[]}
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.consoleSyncAdapter = void 0;
7
7
  var _consoleLogStore = require("../store/consoleLogStore");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
9
  var _sanitize = require("../store/sanitize");
9
10
  /**
10
11
  * consoleSyncAdapter — streams captured console entries from the device to the
@@ -18,18 +19,75 @@ var _sanitize = require("../store/sanitize");
18
19
  * Snapshots MUST be JSON-serializable. Captured `args` can hold Errors,
19
20
  * functions, bigints, and circular references, so we deep-sanitize them into
20
21
  * plain JSON values (objects/arrays preserved so they stay expandable on the
21
- * dashboard).
22
+ * dashboard). Oversized strings and the pre-rendered `message` are capped so
23
+ * a single fat `console.log` cannot drop the whole panel at the 2MB emit
24
+ * budget; the on-device store keeps the raw args.
22
25
  */
23
26
 
27
+ /** Soft cap so a burst of fat logs still fits under the 2MB emit budget. */
28
+ const MAX_SNAPSHOT_BYTES = 1.5 * 1024 * 1024;
29
+ /** One entry's args/message after string caps; fatter args collapse to a stub. */
30
+ const MAX_ENTRY_BYTES = 16 * 1024;
31
+ const ARGS_OMITTED = Object.freeze({
32
+ __buoyTruncated: true,
33
+ note: "Log payload omitted — inspect it on-device."
34
+ });
35
+ /**
36
+ * Per-entry wire cache. A captured entry is never mutated — the store only
37
+ * appends and trims — so keying on identity is correct and self-invalidating.
38
+ *
39
+ * This is the whole cost of the tool. `sanitizeArgs` deep-copies every arg to
40
+ * make it JSON-safe, and without a cache that copy is redone for all 1000
41
+ * entries on every snapshot, ~5x/sec: measured at 12.6ms per tick on ordinary
42
+ * object logs, and it was that expensive BEFORE any of the size guards
43
+ * existed. Converting each entry once removes it from the hot path entirely.
44
+ */
45
+ const wireCache = new WeakMap();
46
+ function toWireEntry(entry) {
47
+ const cached = wireCache.get(entry);
48
+ if (cached) return cached;
49
+ const wired = {
50
+ ...entry,
51
+ args: (0, _sanitize.sanitizeArgs)(entry.args),
52
+ message: (0, _sanitize.capString)(entry.message),
53
+ stack: entry.stack ? (0, _sanitize.capString)(entry.stack, 4 * 1024) : entry.stack
54
+ };
55
+ // One walk decides both "is this entry too fat" and what it costs the
56
+ // snapshot budget — the previous code walked every entry twice.
57
+ const size = (0, _sharedUi.approxJsonSize)(wired, MAX_ENTRY_BYTES);
58
+ const result = size.bytes <= MAX_ENTRY_BYTES ? {
59
+ entry: wired,
60
+ bytes: size.bytes
61
+ } : (() => {
62
+ const stub = {
63
+ ...wired,
64
+ args: [ARGS_OMITTED]
65
+ };
66
+ return {
67
+ entry: stub,
68
+ bytes: (0, _sharedUi.approxJsonSize)(stub, MAX_ENTRY_BYTES).bytes
69
+ };
70
+ })();
71
+ wireCache.set(entry, result);
72
+ return result;
73
+ }
74
+ function newestFitting(entries) {
75
+ const out = [];
76
+ let bytes = 16; // {"entries":[]}
77
+ for (let i = entries.length - 1; i >= 0; i--) {
78
+ const wired = toWireEntry(entries[i]);
79
+ if (bytes + wired.bytes > MAX_SNAPSHOT_BYTES && out.length > 0) break;
80
+ out.push(wired.entry);
81
+ bytes += wired.bytes;
82
+ }
83
+ out.reverse();
84
+ return out;
85
+ }
24
86
  const consoleSyncAdapter = exports.consoleSyncAdapter = {
25
87
  version: 1,
26
88
  getSnapshot() {
27
- const entries = (0, _consoleLogStore.getEntries)().map(entry => ({
28
- ...entry,
29
- args: (0, _sanitize.sanitizeArgs)(entry.args)
30
- }));
31
89
  return {
32
- entries
90
+ entries: newestFitting((0, _consoleLogStore.getEntries)())
33
91
  };
34
92
  },
35
93
  subscribe(onChange) {
@@ -1 +1 @@
1
- {"version":3,"names":["_consoleLogStore","require","_sanitize","consoleSyncAdapter","exports","version","getSnapshot","entries","getEntries","map","entry","args","sanitizeArgs","subscribe","onChange","actions","clearEntries","cleared"],"sourceRoot":"../../../src","sources":["sync/consoleSyncAdapter.ts"],"mappings":";;;;;;AAeA,IAAAA,gBAAA,GAAAC,OAAA;AAMA,IAAAC,SAAA,GAAAD,OAAA;AArBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAcO,MAAME,kBAAkB,GAAAC,OAAA,CAAAD,kBAAA,GAAG;EAChCE,OAAO,EAAE,CAAC;EAEVC,WAAWA,CAAA,EAAoB;IAC7B,MAAMC,OAAO,GAAG,IAAAC,2BAAU,EAAC,CAAC,CAACC,GAAG,CAAEC,KAAK,KAAM;MAC3C,GAAGA,KAAK;MACRC,IAAI,EAAE,IAAAC,sBAAY,EAACF,KAAK,CAACC,IAAI;IAC/B,CAAC,CAAC,CAAC;IACH,OAAO;MAAEJ;IAAQ,CAAC;EACpB,CAAC;EAEDM,SAASA,CAACC,QAAoB,EAAc;IAC1C,OAAO,IAAAD,0BAAS,EAACC,QAAQ,CAAC;EAC5B,CAAC;EAEDC,OAAO,EAAE;IACPC,YAAYA,CAAA,EAAsB;MAChC,IAAAA,6BAAY,EAAC,CAAC;MACd,OAAO;QAAEC,OAAO,EAAE;MAAK,CAAC;IAC1B;EACF;AACF,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_consoleLogStore","require","_sharedUi","_sanitize","MAX_SNAPSHOT_BYTES","MAX_ENTRY_BYTES","ARGS_OMITTED","Object","freeze","__buoyTruncated","note","wireCache","WeakMap","toWireEntry","entry","cached","get","wired","args","sanitizeArgs","message","capString","stack","size","approxJsonSize","result","bytes","stub","set","newestFitting","entries","out","i","length","push","reverse","consoleSyncAdapter","exports","version","getSnapshot","getEntries","subscribe","onChange","actions","clearEntries","cleared"],"sourceRoot":"../../../src","sources":["sync/consoleSyncAdapter.ts"],"mappings":"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","ignoreList":[]}
@@ -7,25 +7,42 @@
7
7
  * references. Both the external-sync snapshot (device → desktop) and the
8
8
  * preserve-log persistence need plain JSON, so this produces a structural copy
9
9
  * (objects/arrays preserved so they stay expandable) with cycles broken.
10
+ *
11
+ * Strings are capped: a single `console.log` of a multi-MB payload (API dump,
12
+ * base64, …) used to ride every snapshot uncapped and drop the whole console
13
+ * panel at the 2MB emit budget.
10
14
  */
11
15
 
12
16
  const MAX_DEPTH = 6;
13
17
  const MAX_ARRAY = 100;
14
18
  const MAX_KEYS = 100;
19
+
20
+ /** Max chars of one string on the wire / in the persisted buffer. */
21
+ export const SNAPSHOT_STRING_LIMIT = 2 * 1024;
22
+ export function capString(value, limit = SNAPSHOT_STRING_LIMIT) {
23
+ if (value.length <= limit) return value;
24
+ return `${value.slice(0, limit)}… [${value.length - limit} more]`;
25
+ }
15
26
  function sanitizeValue(value, depth, seen) {
16
27
  if (value === null) return null;
17
- const t = typeof value;
18
- if (t === "string" || t === "boolean") return value;
19
- if (t === "number") return Number.isFinite(value) ? value : String(value);
20
- if (t === "undefined") return "undefined";
21
- if (t === "bigint") return `${String(value)}n`;
22
- if (t === "function") return `ƒ ${value.name || "(anonymous)"}`;
23
- if (t === "symbol") return value.toString();
28
+ // Checked inline rather than through a `const t = typeof value` alias: the
29
+ // alias does not narrow `value`, so the string branch could not be handed to
30
+ // a `(s: string)` function without a cast — which is how this file stopped
31
+ // compiling.
32
+ if (typeof value === "string") return capString(value);
33
+ if (typeof value === "boolean") return value;
34
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
35
+ if (typeof value === "undefined") return "undefined";
36
+ if (typeof value === "bigint") return `${String(value)}n`;
37
+ if (typeof value === "function") {
38
+ return `ƒ ${value.name || "(anonymous)"}`;
39
+ }
40
+ if (typeof value === "symbol") return value.toString();
24
41
  if (value instanceof Error) {
25
42
  return {
26
43
  name: value.name,
27
- message: value.message,
28
- stack: value.stack
44
+ message: capString(value.message),
45
+ stack: value.stack ? capString(value.stack, 4 * 1024) : value.stack
29
46
  };
30
47
  }
31
48
  if (depth >= MAX_DEPTH) return Array.isArray(value) ? "[Array]" : "{…}";
@@ -1 +1 @@
1
- {"version":3,"names":["MAX_DEPTH","MAX_ARRAY","MAX_KEYS","sanitizeValue","value","depth","seen","t","Number","isFinite","String","name","toString","Error","message","stack","Array","isArray","obj","has","add","out","slice","map","v","length","push","key","Object","keys","delete","sanitizeArgs","args","WeakSet","arg"],"sourceRoot":"../../../src","sources":["store/sanitize.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,MAAMA,SAAS,GAAG,CAAC;AACnB,MAAMC,SAAS,GAAG,GAAG;AACrB,MAAMC,QAAQ,GAAG,GAAG;AAEpB,SAASC,aAAaA,CAACC,KAAc,EAAEC,KAAa,EAAEC,IAAqB,EAAW;EACpF,IAAIF,KAAK,KAAK,IAAI,EAAE,OAAO,IAAI;EAC/B,MAAMG,CAAC,GAAG,OAAOH,KAAK;EACtB,IAAIG,CAAC,KAAK,QAAQ,IAAIA,CAAC,KAAK,SAAS,EAAE,OAAOH,KAAK;EACnD,IAAIG,CAAC,KAAK,QAAQ,EAAE,OAAOC,MAAM,CAACC,QAAQ,CAACL,KAAe,CAAC,GAAGA,KAAK,GAAGM,MAAM,CAACN,KAAK,CAAC;EACnF,IAAIG,CAAC,KAAK,WAAW,EAAE,OAAO,WAAW;EACzC,IAAIA,CAAC,KAAK,QAAQ,EAAE,OAAO,GAAGG,MAAM,CAACN,KAAK,CAAC,GAAG;EAC9C,IAAIG,CAAC,KAAK,UAAU,EAAE,OAAO,KAAMH,KAAK,CAAuBO,IAAI,IAAI,aAAa,EAAE;EACtF,IAAIJ,CAAC,KAAK,QAAQ,EAAE,OAAQH,KAAK,CAAYQ,QAAQ,CAAC,CAAC;EAEvD,IAAIR,KAAK,YAAYS,KAAK,EAAE;IAC1B,OAAO;MAAEF,IAAI,EAAEP,KAAK,CAACO,IAAI;MAAEG,OAAO,EAAEV,KAAK,CAACU,OAAO;MAAEC,KAAK,EAAEX,KAAK,CAACW;IAAM,CAAC;EACzE;EAEA,IAAIV,KAAK,IAAIL,SAAS,EAAE,OAAOgB,KAAK,CAACC,OAAO,CAACb,KAAK,CAAC,GAAG,SAAS,GAAG,KAAK;EAEvE,MAAMc,GAAG,GAAGd,KAAe;EAC3B,IAAIE,IAAI,CAACa,GAAG,CAACD,GAAG,CAAC,EAAE,OAAO,YAAY;EACtCZ,IAAI,CAACc,GAAG,CAACF,GAAG,CAAC;EAEb,IAAI;IACF,IAAIF,KAAK,CAACC,OAAO,CAACb,KAAK,CAAC,EAAE;MACxB,MAAMiB,GAAG,GAAGjB,KAAK,CAACkB,KAAK,CAAC,CAAC,EAAErB,SAAS,CAAC,CAACsB,GAAG,CAAEC,CAAC,IAAKrB,aAAa,CAACqB,CAAC,EAAEnB,KAAK,GAAG,CAAC,EAAEC,IAAI,CAAC,CAAC;MACnF,IAAIF,KAAK,CAACqB,MAAM,GAAGxB,SAAS,EAAEoB,GAAG,CAACK,IAAI,CAAC,KAAKtB,KAAK,CAACqB,MAAM,GAAGxB,SAAS,OAAO,CAAC;MAC5E,OAAOoB,GAAG;IACZ;IACA,MAAMA,GAA4B,GAAG,CAAC,CAAC;IACvC,KAAK,MAAMM,GAAG,IAAIC,MAAM,CAACC,IAAI,CAACX,GAAG,CAAC,CAACI,KAAK,CAAC,CAAC,EAAEpB,QAAQ,CAAC,EAAE;MACrD,IAAI;QACFmB,GAAG,CAACM,GAAG,CAAC,GAAGxB,aAAa,CAAEe,GAAG,CAA6BS,GAAG,CAAC,EAAEtB,KAAK,GAAG,CAAC,EAAEC,IAAI,CAAC;MAClF,CAAC,CAAC,MAAM;QACNe,GAAG,CAACM,GAAG,CAAC,GAAG,kBAAkB;MAC/B;IACF;IACA,OAAON,GAAG;EACZ,CAAC,SAAS;IACRf,IAAI,CAACwB,MAAM,CAACZ,GAAG,CAAC;EAClB;AACF;;AAEA;AACA,OAAO,SAASa,YAAYA,CAACC,IAAe,EAAa;EACvD,MAAM1B,IAAI,GAAG,IAAI2B,OAAO,CAAS,CAAC;EAClC,OAAOD,IAAI,CAACT,GAAG,CAAEW,GAAG,IAAK/B,aAAa,CAAC+B,GAAG,EAAE,CAAC,EAAE5B,IAAI,CAAC,CAAC;AACvD","ignoreList":[]}
1
+ {"version":3,"names":["MAX_DEPTH","MAX_ARRAY","MAX_KEYS","SNAPSHOT_STRING_LIMIT","capString","value","limit","length","slice","sanitizeValue","depth","seen","Number","isFinite","String","name","toString","Error","message","stack","Array","isArray","obj","has","add","out","map","v","push","key","Object","keys","delete","sanitizeArgs","args","WeakSet","arg"],"sourceRoot":"../../../src","sources":["store/sanitize.ts"],"mappings":"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","ignoreList":[]}
@@ -12,20 +12,78 @@
12
12
  * Snapshots MUST be JSON-serializable. Captured `args` can hold Errors,
13
13
  * functions, bigints, and circular references, so we deep-sanitize them into
14
14
  * plain JSON values (objects/arrays preserved so they stay expandable on the
15
- * dashboard).
15
+ * dashboard). Oversized strings and the pre-rendered `message` are capped so
16
+ * a single fat `console.log` cannot drop the whole panel at the 2MB emit
17
+ * budget; the on-device store keeps the raw args.
16
18
  */
17
19
 
18
20
  import { subscribe, getEntries, clearEntries } from "../store/consoleLogStore";
19
- import { sanitizeArgs } from "../store/sanitize";
21
+ import { approxJsonSize } from "@buoy-gg/shared-ui";
22
+ import { capString, sanitizeArgs } from "../store/sanitize";
23
+ /** Soft cap so a burst of fat logs still fits under the 2MB emit budget. */
24
+ const MAX_SNAPSHOT_BYTES = 1.5 * 1024 * 1024;
25
+ /** One entry's args/message after string caps; fatter args collapse to a stub. */
26
+ const MAX_ENTRY_BYTES = 16 * 1024;
27
+ const ARGS_OMITTED = Object.freeze({
28
+ __buoyTruncated: true,
29
+ note: "Log payload omitted — inspect it on-device."
30
+ });
31
+ /**
32
+ * Per-entry wire cache. A captured entry is never mutated — the store only
33
+ * appends and trims — so keying on identity is correct and self-invalidating.
34
+ *
35
+ * This is the whole cost of the tool. `sanitizeArgs` deep-copies every arg to
36
+ * make it JSON-safe, and without a cache that copy is redone for all 1000
37
+ * entries on every snapshot, ~5x/sec: measured at 12.6ms per tick on ordinary
38
+ * object logs, and it was that expensive BEFORE any of the size guards
39
+ * existed. Converting each entry once removes it from the hot path entirely.
40
+ */
41
+ const wireCache = new WeakMap();
42
+ function toWireEntry(entry) {
43
+ const cached = wireCache.get(entry);
44
+ if (cached) return cached;
45
+ const wired = {
46
+ ...entry,
47
+ args: sanitizeArgs(entry.args),
48
+ message: capString(entry.message),
49
+ stack: entry.stack ? capString(entry.stack, 4 * 1024) : entry.stack
50
+ };
51
+ // One walk decides both "is this entry too fat" and what it costs the
52
+ // snapshot budget — the previous code walked every entry twice.
53
+ const size = approxJsonSize(wired, MAX_ENTRY_BYTES);
54
+ const result = size.bytes <= MAX_ENTRY_BYTES ? {
55
+ entry: wired,
56
+ bytes: size.bytes
57
+ } : (() => {
58
+ const stub = {
59
+ ...wired,
60
+ args: [ARGS_OMITTED]
61
+ };
62
+ return {
63
+ entry: stub,
64
+ bytes: approxJsonSize(stub, MAX_ENTRY_BYTES).bytes
65
+ };
66
+ })();
67
+ wireCache.set(entry, result);
68
+ return result;
69
+ }
70
+ function newestFitting(entries) {
71
+ const out = [];
72
+ let bytes = 16; // {"entries":[]}
73
+ for (let i = entries.length - 1; i >= 0; i--) {
74
+ const wired = toWireEntry(entries[i]);
75
+ if (bytes + wired.bytes > MAX_SNAPSHOT_BYTES && out.length > 0) break;
76
+ out.push(wired.entry);
77
+ bytes += wired.bytes;
78
+ }
79
+ out.reverse();
80
+ return out;
81
+ }
20
82
  export const consoleSyncAdapter = {
21
83
  version: 1,
22
84
  getSnapshot() {
23
- const entries = getEntries().map(entry => ({
24
- ...entry,
25
- args: sanitizeArgs(entry.args)
26
- }));
27
85
  return {
28
- entries
86
+ entries: newestFitting(getEntries())
29
87
  };
30
88
  },
31
89
  subscribe(onChange) {
@@ -1 +1 @@
1
- {"version":3,"names":["subscribe","getEntries","clearEntries","sanitizeArgs","consoleSyncAdapter","version","getSnapshot","entries","map","entry","args","onChange","actions","cleared"],"sourceRoot":"../../../src","sources":["sync/consoleSyncAdapter.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SACEA,SAAS,EACTC,UAAU,EACVC,YAAY,QAEP,0BAA0B;AACjC,SAASC,YAAY,QAAQ,mBAAmB;AAMhD,OAAO,MAAMC,kBAAkB,GAAG;EAChCC,OAAO,EAAE,CAAC;EAEVC,WAAWA,CAAA,EAAoB;IAC7B,MAAMC,OAAO,GAAGN,UAAU,CAAC,CAAC,CAACO,GAAG,CAAEC,KAAK,KAAM;MAC3C,GAAGA,KAAK;MACRC,IAAI,EAAEP,YAAY,CAACM,KAAK,CAACC,IAAI;IAC/B,CAAC,CAAC,CAAC;IACH,OAAO;MAAEH;IAAQ,CAAC;EACpB,CAAC;EAEDP,SAASA,CAACW,QAAoB,EAAc;IAC1C,OAAOX,SAAS,CAACW,QAAQ,CAAC;EAC5B,CAAC;EAEDC,OAAO,EAAE;IACPV,YAAYA,CAAA,EAAsB;MAChCA,YAAY,CAAC,CAAC;MACd,OAAO;QAAEW,OAAO,EAAE;MAAK,CAAC;IAC1B;EACF;AACF,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["subscribe","getEntries","clearEntries","approxJsonSize","capString","sanitizeArgs","MAX_SNAPSHOT_BYTES","MAX_ENTRY_BYTES","ARGS_OMITTED","Object","freeze","__buoyTruncated","note","wireCache","WeakMap","toWireEntry","entry","cached","get","wired","args","message","stack","size","result","bytes","stub","set","newestFitting","entries","out","i","length","push","reverse","consoleSyncAdapter","version","getSnapshot","onChange","actions","cleared"],"sourceRoot":"../../../src","sources":["sync/consoleSyncAdapter.ts"],"mappings":"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","ignoreList":[]}
@@ -5,7 +5,14 @@
5
5
  * references. Both the external-sync snapshot (device → desktop) and the
6
6
  * preserve-log persistence need plain JSON, so this produces a structural copy
7
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.
8
12
  */
13
+ /** Max chars of one string on the wire / in the persisted buffer. */
14
+ export declare const SNAPSHOT_STRING_LIMIT: number;
15
+ export declare function capString(value: string, limit?: number): string;
9
16
  /** Deep-sanitize an array of console arguments into JSON-safe values. */
10
17
  export declare function sanitizeArgs(args: unknown[]): unknown[];
11
18
  //# sourceMappingURL=sanitize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../../src/store/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA8CH,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAGvD"}
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../../src/store/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAA8B,GACpC,MAAM,CAGR;AAoDD,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAGvD"}
@@ -10,7 +10,9 @@
10
10
  * Snapshots MUST be JSON-serializable. Captured `args` can hold Errors,
11
11
  * functions, bigints, and circular references, so we deep-sanitize them into
12
12
  * plain JSON values (objects/arrays preserved so they stay expandable on the
13
- * dashboard).
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.
14
16
  */
15
17
  import { type ConsoleLogEntry } from "../store/consoleLogStore";
16
18
  interface ConsoleSnapshot {
@@ -1 +1 @@
1
- {"version":3,"file":"consoleSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/consoleSyncAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAGlC,UAAU,eAAe;IACvB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,eAAO,MAAM,kBAAkB;;mBAGd,eAAe;wBAQV,MAAM,IAAI,GAAG,MAAM,IAAI;;wBAKzB;YAAE,OAAO,EAAE,IAAI,CAAA;SAAE;;CAKpC,CAAC"}
1
+ {"version":3,"file":"consoleSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/consoleSyncAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAIlC,UAAU,eAAe;IACvB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAqED,eAAO,MAAM,kBAAkB;;mBAGd,eAAe;wBAIV,MAAM,IAAI,GAAG,MAAM,IAAI;;wBAKzB;YAAE,OAAO,EAAE,IAAI,CAAA;SAAE;;CAKpC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/console",
3
- "version": "7.0.17",
3
+ "version": "7.0.19",
4
4
  "description": "Chrome DevTools-style console for React Native — captures every console.* on device, even in release builds. Part of Buoy devtools.",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "sideEffects": false,
15
15
  "dependencies": {
16
- "@buoy-gg/shared-ui": "^7.0.17"
16
+ "@buoy-gg/shared-ui": "^7.0.19"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "react": "*",
@@ -30,7 +30,12 @@
30
30
  "targets": [
31
31
  "commonjs",
32
32
  "module",
33
- "typescript"
33
+ [
34
+ "typescript",
35
+ {
36
+ "project": "tsconfig.build.json"
37
+ }
38
+ ]
34
39
  ]
35
40
  },
36
41
  "repository": {
@@ -0,0 +1,156 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { capString, sanitizeArgs, SNAPSHOT_STRING_LIMIT } from "../sanitize";
3
+ import { clearEntries, replaceEntries } from "../consoleLogStore";
4
+ import { consoleSyncAdapter } from "../../sync/consoleSyncAdapter";
5
+ import type { ConsoleLogEntry } from "../consoleLogStore";
6
+
7
+ // Node has no rAF; the store coalesces listener notifies onto one frame.
8
+ globalThis.requestAnimationFrame = ((cb: (t: number) => void) => {
9
+ cb(0);
10
+ return 0;
11
+ }) as typeof requestAnimationFrame;
12
+
13
+ function entry(
14
+ partial: Partial<ConsoleLogEntry> & Pick<ConsoleLogEntry, "id" | "args" | "message">,
15
+ ): ConsoleLogEntry {
16
+ return {
17
+ method: "log",
18
+ level: "info",
19
+ timestamp: Date.now(),
20
+ ...partial,
21
+ };
22
+ }
23
+
24
+ afterEach(() => {
25
+ clearEntries();
26
+ });
27
+
28
+ describe("sanitizeArgs", () => {
29
+ it("round-trips Errors, functions, and circular refs as JSON", () => {
30
+ const circular: { self?: unknown } = {};
31
+ circular.self = circular;
32
+ const sanitized = sanitizeArgs([
33
+ new Error("boom"),
34
+ () => {},
35
+ circular,
36
+ 1n,
37
+ ]);
38
+ expect(() => JSON.stringify(sanitized)).not.toThrow();
39
+ expect(sanitized[0]).toEqual(
40
+ expect.objectContaining({ name: "Error", message: "boom" }),
41
+ );
42
+ expect(sanitized[2]).toEqual(
43
+ expect.objectContaining({ self: "[Circular]" }),
44
+ );
45
+ });
46
+
47
+ it("caps oversized strings so a fat console.log cannot blow the snapshot", () => {
48
+ const huge = "x".repeat(SNAPSHOT_STRING_LIMIT + 50_000);
49
+ const sanitized = sanitizeArgs([huge]);
50
+ expect(typeof sanitized[0]).toBe("string");
51
+ expect((sanitized[0] as string).length).toBeLessThan(huge.length);
52
+ expect((sanitized[0] as string).startsWith("x".repeat(32))).toBe(true);
53
+ expect(capString("short")).toBe("short");
54
+ });
55
+ });
56
+
57
+ describe("consoleSyncAdapter", () => {
58
+ it("snapshots { entries } as JSON and clearEntries does not hang", () => {
59
+ expect(consoleSyncAdapter.version).toBe(1);
60
+ clearEntries();
61
+ const snapshot = consoleSyncAdapter.getSnapshot();
62
+ expect(snapshot).toEqual({ entries: expect.any(Array) });
63
+ expect(() => JSON.stringify(snapshot)).not.toThrow();
64
+ expect(consoleSyncAdapter.actions.clearEntries()).toEqual({ cleared: true });
65
+ expect(consoleSyncAdapter.getSnapshot()).toEqual({ entries: [] });
66
+ });
67
+
68
+ it("does not drop the console snapshot when one log is multi-MB", () => {
69
+ const huge = "x".repeat(3 * 1024 * 1024);
70
+ replaceEntries([
71
+ entry({
72
+ id: 1,
73
+ args: [huge],
74
+ message: huge,
75
+ }),
76
+ ]);
77
+
78
+ const snapshot = consoleSyncAdapter.getSnapshot();
79
+ expect(snapshot.entries).toHaveLength(1);
80
+ expect(snapshot.entries[0]?.id).toBe(1);
81
+ expect(snapshot.entries[0]?.method).toBe("log");
82
+ expect(String(snapshot.entries[0]?.args[0]).length).toBeLessThan(huge.length);
83
+ expect(snapshot.entries[0]?.message.length).toBeLessThan(huge.length);
84
+ expect(() => JSON.stringify(snapshot)).not.toThrow();
85
+ expect(JSON.stringify(snapshot).length).toBeLessThan(2 * 1024 * 1024);
86
+ });
87
+
88
+ it("keeps a burst of fat logs under the 2MB emit budget", () => {
89
+ const huge = "x".repeat(20 * 1024);
90
+ replaceEntries(
91
+ Array.from({ length: 80 }, (_, i) =>
92
+ entry({
93
+ id: i + 1,
94
+ args: [huge],
95
+ message: huge,
96
+ }),
97
+ ),
98
+ );
99
+
100
+ const snapshot = consoleSyncAdapter.getSnapshot();
101
+ expect(snapshot.entries.length).toBeGreaterThan(0);
102
+ expect(snapshot.entries[snapshot.entries.length - 1]?.id).toBe(80);
103
+ expect(JSON.stringify(snapshot).length).toBeLessThan(2 * 1024 * 1024);
104
+ });
105
+
106
+ // ---------------------------------------------------------------------
107
+ // Cost guard. The snapshot is rebuilt ~5x/sec while a dashboard watches,
108
+ // and `sanitizeArgs` deep-copies every argument to make it JSON-safe — so
109
+ // WHETHER that copy is redone per tick is part of the contract. Measured at
110
+ // 12.6ms/tick on 1000 ordinary object logs before this cache, and note that
111
+ // cost predates the size guards entirely.
112
+ // ---------------------------------------------------------------------
113
+
114
+ it("reuses the wire form of an unchanged entry across snapshots", () => {
115
+ replaceEntries([
116
+ entry({ id: 1, message: "hello", args: ["hello"] }),
117
+ entry({
118
+ id: 2,
119
+ message: "payload",
120
+ args: ["payload", { rows: Array.from({ length: 40 }, (_, i) => ({ i })) }],
121
+ }),
122
+ ]);
123
+
124
+ const first = consoleSyncAdapter.getSnapshot().entries;
125
+ const second = consoleSyncAdapter.getSnapshot().entries;
126
+ // Identity, not deep equality: a fresh sanitized copy per tick is exactly
127
+ // the waste this guards, and the desktop mirror also feeds these into
128
+ // effect dependencies.
129
+ expect(second[0]).toBe(first[0]);
130
+ expect(second[1]).toBe(first[1]);
131
+ });
132
+
133
+ it("keeps the newest entries when the budget forces a cut", () => {
134
+ // 1000 logs of a ~4KB object are each individually legal and still total
135
+ // over the 2MB emit budget, which would drop the WHOLE console panel.
136
+ replaceEntries(
137
+ Array.from({ length: 1000 }, (_, i) =>
138
+ entry({
139
+ id: i,
140
+ message: `line ${i}`,
141
+ // ~4KB each: individually far under the per-entry cap, and 1000 of
142
+ // them still exceed the snapshot budget.
143
+ args: [`line ${i}`, { blob: "p".repeat(4 * 1024) }],
144
+ }),
145
+ ),
146
+ );
147
+
148
+ const { entries } = consoleSyncAdapter.getSnapshot();
149
+ expect(JSON.stringify({ entries }).length).toBeLessThan(2 * 1024 * 1024);
150
+ // Not a drop, and not everything — the budget genuinely bit.
151
+ expect(entries.length).toBeGreaterThan(0);
152
+ expect(entries.length).toBeLessThan(1000);
153
+ // A console reader cares about the most RECENT lines, so those survive.
154
+ expect(entries[entries.length - 1]?.id).toBe(999);
155
+ });
156
+ });
@@ -5,24 +5,49 @@
5
5
  * references. Both the external-sync snapshot (device → desktop) and the
6
6
  * preserve-log persistence need plain JSON, so this produces a structural copy
7
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.
8
12
  */
9
13
 
10
14
  const MAX_DEPTH = 6;
11
15
  const MAX_ARRAY = 100;
12
16
  const MAX_KEYS = 100;
13
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
+
14
29
  function sanitizeValue(value: unknown, depth: number, seen: WeakSet<object>): unknown {
15
30
  if (value === null) return null;
16
- const t = typeof value;
17
- if (t === "string" || t === "boolean") return value;
18
- if (t === "number") return Number.isFinite(value as number) ? value : String(value);
19
- if (t === "undefined") return "undefined";
20
- if (t === "bigint") return `${String(value)}n`;
21
- if (t === "function") return `ƒ ${(value as { name?: string }).name || "(anonymous)"}`;
22
- if (t === "symbol") return (value as symbol).toString();
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();
23
44
 
24
45
  if (value instanceof Error) {
25
- return { name: value.name, message: value.message, stack: value.stack };
46
+ return {
47
+ name: value.name,
48
+ message: capString(value.message),
49
+ stack: value.stack ? capString(value.stack, 4 * 1024) : value.stack,
50
+ };
26
51
  }
27
52
 
28
53
  if (depth >= MAX_DEPTH) return Array.isArray(value) ? "[Array]" : "{…}";
@@ -10,7 +10,9 @@
10
10
  * Snapshots MUST be JSON-serializable. Captured `args` can hold Errors,
11
11
  * functions, bigints, and circular references, so we deep-sanitize them into
12
12
  * plain JSON values (objects/arrays preserved so they stay expandable on the
13
- * dashboard).
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.
14
16
  */
15
17
 
16
18
  import {
@@ -19,21 +21,85 @@ import {
19
21
  clearEntries,
20
22
  type ConsoleLogEntry,
21
23
  } from "../store/consoleLogStore";
22
- import { sanitizeArgs } from "../store/sanitize";
24
+ import { approxJsonSize } from "@buoy-gg/shared-ui";
25
+ import { capString, sanitizeArgs } from "../store/sanitize";
23
26
 
24
27
  interface ConsoleSnapshot {
25
28
  entries: ConsoleLogEntry[];
26
29
  }
27
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
+
28
98
  export const consoleSyncAdapter = {
29
99
  version: 1,
30
100
 
31
101
  getSnapshot(): ConsoleSnapshot {
32
- const entries = getEntries().map((entry) => ({
33
- ...entry,
34
- args: sanitizeArgs(entry.args),
35
- }));
36
- return { entries };
102
+ return { entries: newestFitting(getEntries()) };
37
103
  },
38
104
 
39
105
  subscribe(onChange: () => void): () => void {