@buoy-gg/console 7.0.16 → 7.0.18
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/lib/commonjs/store/sanitize.js +28 -9
- package/lib/commonjs/store/sanitize.js.map +1 -1
- package/lib/commonjs/sync/consoleSyncAdapter.js +64 -6
- package/lib/commonjs/sync/consoleSyncAdapter.js.map +1 -1
- package/lib/module/store/sanitize.js +26 -9
- package/lib/module/store/sanitize.js.map +1 -1
- package/lib/module/sync/consoleSyncAdapter.js +65 -7
- package/lib/module/sync/consoleSyncAdapter.js.map +1 -1
- package/lib/typescript/store/sanitize.d.ts +7 -0
- package/lib/typescript/store/sanitize.d.ts.map +1 -1
- package/lib/typescript/sync/consoleSyncAdapter.d.ts +3 -1
- package/lib/typescript/sync/consoleSyncAdapter.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/store/__tests__/consoleSyncAdapter.test.ts +156 -0
- package/src/store/sanitize.ts +33 -8
- package/src/sync/consoleSyncAdapter.ts +73 -7
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
if (
|
|
27
|
-
if (
|
|
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","
|
|
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","
|
|
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":";;;;;;AAiBA,IAAAA,gBAAA,GAAAC,OAAA;AAMA,IAAAC,SAAA,GAAAD,OAAA;AACA,IAAAE,SAAA,GAAAF,OAAA;AAxBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAeA;AACA,MAAMG,kBAAkB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI;AAC5C;AACA,MAAMC,eAAe,GAAG,EAAE,GAAG,IAAI;AAEjC,MAAMC,YAAY,GAAGC,MAAM,CAACC,MAAM,CAAC;EACjCC,eAAe,EAAE,IAAI;EACrBC,IAAI,EAAE;AACR,CAAC,CAAC;AAOF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,SAAS,GAAG,IAAIC,OAAO,CAA6B,CAAC;AAE3D,SAASC,WAAWA,CAACC,KAAsB,EAAa;EACtD,MAAMC,MAAM,GAAGJ,SAAS,CAACK,GAAG,CAACF,KAAK,CAAC;EACnC,IAAIC,MAAM,EAAE,OAAOA,MAAM;EAEzB,MAAME,KAAsB,GAAG;IAC7B,GAAGH,KAAK;IACRI,IAAI,EAAE,IAAAC,sBAAY,EAACL,KAAK,CAACI,IAAI,CAAC;IAC9BE,OAAO,EAAE,IAAAC,mBAAS,EAACP,KAAK,CAACM,OAAO,CAAC;IACjCE,KAAK,EAAER,KAAK,CAACQ,KAAK,GAAG,IAAAD,mBAAS,EAACP,KAAK,CAACQ,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,GAAGR,KAAK,CAACQ;EAChE,CAAC;EACD;EACA;EACA,MAAMC,IAAI,GAAG,IAAAC,wBAAc,EAACP,KAAK,EAAEZ,eAAe,CAAC;EACnD,MAAMoB,MAAiB,GACrBF,IAAI,CAACG,KAAK,IAAIrB,eAAe,GACzB;IAAES,KAAK,EAAEG,KAAK;IAAES,KAAK,EAAEH,IAAI,CAACG;EAAM,CAAC,GACnC,CAAC,MAAM;IACL,MAAMC,IAAI,GAAG;MAAE,GAAGV,KAAK;MAAEC,IAAI,EAAE,CAACZ,YAAY;IAAE,CAAC;IAC/C,OAAO;MACLQ,KAAK,EAAEa,IAAI;MACXD,KAAK,EAAE,IAAAF,wBAAc,EAACG,IAAI,EAAEtB,eAAe,CAAC,CAACqB;IAC/C,CAAC;EACH,CAAC,EAAE,CAAC;EACVf,SAAS,CAACiB,GAAG,CAACd,KAAK,EAAEW,MAAM,CAAC;EAC5B,OAAOA,MAAM;AACf;AAEA,SAASI,aAAaA,CAACC,OAA0B,EAAqB;EACpE,MAAMC,GAAsB,GAAG,EAAE;EACjC,IAAIL,KAAK,GAAG,EAAE,CAAC,CAAC;EAChB,KAAK,IAAIM,CAAC,GAAGF,OAAO,CAACG,MAAM,GAAG,CAAC,EAAED,CAAC,IAAI,CAAC,EAAEA,CAAC,EAAE,EAAE;IAC5C,MAAMf,KAAK,GAAGJ,WAAW,CAACiB,OAAO,CAACE,CAAC,CAAC,CAAC;IACrC,IAAIN,KAAK,GAAGT,KAAK,CAACS,KAAK,GAAGtB,kBAAkB,IAAI2B,GAAG,CAACE,MAAM,GAAG,CAAC,EAAE;IAChEF,GAAG,CAACG,IAAI,CAACjB,KAAK,CAACH,KAAK,CAAC;IACrBY,KAAK,IAAIT,KAAK,CAACS,KAAK;EACtB;EACAK,GAAG,CAACI,OAAO,CAAC,CAAC;EACb,OAAOJ,GAAG;AACZ;AAEO,MAAMK,kBAAkB,GAAAC,OAAA,CAAAD,kBAAA,GAAG;EAChCE,OAAO,EAAE,CAAC;EAEVC,WAAWA,CAAA,EAAoB;IAC7B,OAAO;MAAET,OAAO,EAAED,aAAa,CAAC,IAAAW,2BAAU,EAAC,CAAC;IAAE,CAAC;EACjD,CAAC;EAEDC,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":[]}
|
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
if (
|
|
23
|
-
if (
|
|
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","
|
|
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 {
|
|
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","
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "7.0.18",
|
|
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.
|
|
16
|
+
"@buoy-gg/shared-ui": "^7.0.18"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"react": "*",
|
|
@@ -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: FrameRequestCallback) => {
|
|
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: "a", message: "hello", args: ["hello"] }),
|
|
117
|
+
entry({
|
|
118
|
+
id: "b",
|
|
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: `e${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("e999");
|
|
155
|
+
});
|
|
156
|
+
});
|
package/src/store/sanitize.ts
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
if (
|
|
22
|
-
if (
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 {
|