bee-ladybug 0.6.4 → 0.6.5

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/README.MD CHANGED
@@ -1,10 +1,19 @@
1
1
 
2
2
  # BeeLadybug
3
+ [![types](https://img.shields.io/npm/types/bee-ladybug)](https://www.npmjs.com/package/bee-ladybug)
4
+ [![npm](https://img.shields.io/npm/v/bee-ladybug)](https://www.npmjs.com/package/bee-ladybug)
5
+ [![download](https://img.shields.io/npm/dm/bee-ladybug)](https://www.npmjs.com/package/bee-ladybug)
6
+ [![](https://data.jsdelivr.com/v1/package/npm/bee-ladybug/badge)](https://www.jsdelivr.com/package/npm/bee-ladybug)
7
+ ![licenza](https://img.shields.io/npm/l/bee-ladybug)
8
+ [![Socket Badge](https://badge.socket.dev/npm/package/bee-ladybug/0.6.5)](https://badge.socket.dev/npm/package/bee-ladybug/0.6.5)
9
+ [![size](https://img.shields.io/bundlephobia/minzip/bee-ladybug)](https://bundlephobia.com/package/bee-ladybug)
10
+ [![Snyk](https://snyk.io/test/npm/bee-ladybug/badge.svg)](https://security.snyk.io/package/npm/bee-ladybug)
3
11
 
4
12
  <p align="center">
5
13
  <img src="https://beeenginejs.com/wp-content/uploads/2026/10/beeladybug-logo-animato-ezgif.com-effects.gif" alt="BeeEngine Logo" width="900" />
6
14
  </p>
7
15
 
16
+
8
17
  Universal telemetry and visual inspection overlay.
9
18
 
10
19
  BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Universal telemetry and visual inspection overlay for Canvas, DOM, Python and AI flows.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -38,6 +38,7 @@ export class AntAdapter {
38
38
  this.source = cfg.source;
39
39
 
40
40
  this.#attached = false;
41
+ this.#active = false;
41
42
  this.#observer = null;
42
43
  this.#counts = new WeakMap();
43
44
  this.#hot = [];
@@ -45,6 +46,8 @@ export class AntAdapter {
45
46
  }
46
47
 
47
48
  #attached;
49
+ /** False while the ANT button is off: setRoot() must not restart the observer then. */
50
+ #active;
48
51
  #observer;
49
52
  #counts;
50
53
  #hot;
@@ -53,17 +56,19 @@ export class AntAdapter {
53
56
  attach() {
54
57
  if (this.#attached) return this;
55
58
  this.#attached = true;
56
- this.#enable();
59
+ this.#turnOn();
57
60
  this.core?.registerAdapter('ant', {
58
- enable: () => this.#enable(),
61
+ enable: () => this.#turnOn(),
59
62
  disable: () => this.#disable(),
60
63
  label: 'ANT',
61
64
  snapshot: () => this.snapshot()
62
65
  });
63
- this.core?.sendData('log', {
64
- source: this.source,
65
- message: 'AntAdapter attached. Watching DOM mutation rate.'
66
- });
66
+ if (this.#observer) {
67
+ this.core?.sendData('log', {
68
+ source: this.source,
69
+ message: 'AntAdapter attached. Watching DOM mutation rate.'
70
+ });
71
+ }
67
72
  return this;
68
73
  }
69
74
 
@@ -75,6 +80,19 @@ export class AntAdapter {
75
80
  return this;
76
81
  }
77
82
 
83
+ /**
84
+ * Watch another root. Counters and hot rows restart; `null` stops watching.
85
+ * @param {ParentNode | null} root
86
+ */
87
+ setRoot(root) {
88
+ this.root = root;
89
+ if (!this.#attached) return this;
90
+ this.#reset();
91
+ if (!root || !this.#active) return this;
92
+ this.#warnNotWatching(this.#observe());
93
+ return this;
94
+ }
95
+
78
96
  /**
79
97
  * Nodes whose current window is still at or above the threshold.
80
98
  * Hidden boxes, the WordPress admin bar, and BeeLadybug chrome are skipped.
@@ -99,49 +117,83 @@ export class AntAdapter {
99
117
  return { name: 'ant', items };
100
118
  }
101
119
 
102
- #enable() {
103
- if (!this.#attached || this.#observer) return;
104
- if (typeof MutationObserver === 'undefined' || !this.root) return;
120
+ #turnOn() {
121
+ if (!this.#attached) return;
122
+ this.#active = true;
123
+ this.#warnNotWatching(this.#observe());
124
+ }
125
+
126
+ #disable() {
127
+ this.#active = false;
128
+ this.#reset();
129
+ }
130
+
131
+ #reset() {
132
+ if (this.#observer) this.#observer.disconnect();
133
+ this.#observer = null;
134
+ this.#counts = new WeakMap();
135
+ this.#hot = [];
136
+ }
137
+
138
+ /** @returns {'' | 'no-root' | 'no-observer' | 'failed'} why nothing is watched, empty when it is */
139
+ #observe() {
140
+ if (this.#observer) return '';
141
+ if (!this.root) return 'no-root';
142
+ if (typeof MutationObserver === 'undefined') return 'no-observer';
105
143
  this.#counts = new WeakMap();
106
- this.#observer = new MutationObserver(this.#onMutate);
144
+ const observer = new MutationObserver(this.#onMutate);
107
145
  try {
108
- this.#observer.observe(this.root, {
146
+ observer.observe(this.root, {
109
147
  childList: true,
110
148
  subtree: true,
111
149
  attributes: true,
112
150
  characterData: true
113
151
  });
114
152
  } catch {
115
- this.#observer.disconnect();
116
- this.#observer = null;
153
+ observer.disconnect();
154
+ return 'failed';
117
155
  }
156
+ this.#observer = observer;
157
+ return '';
118
158
  }
119
159
 
120
- #disable() {
121
- if (this.#observer) this.#observer.disconnect();
122
- this.#observer = null;
123
- this.#counts = new WeakMap();
124
- this.#hot = [];
160
+ #warnNotWatching(reason) {
161
+ if (!reason) return;
162
+ const messages = {
163
+ 'no-root': 'no root: mutation rate is not watched. Pass { root } or call setRoot().',
164
+ 'no-observer': 'MutationObserver is missing: mutation rate is not watched.',
165
+ failed: 'observe() failed: mutation rate is not watched.'
166
+ };
167
+ this.core?.sendData('warn', { source: this.source, message: messages[reason] });
125
168
  }
126
169
 
127
170
  #ingest(records) {
128
171
  if (!this.#attached || !this.core || !records?.length) return;
129
172
  const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
173
+ this.#prune(now);
174
+ // One layout read per element per batch, however many records it produced.
175
+ const skipped = new Map();
130
176
  for (let i = 0; i < records.length; i++) {
131
- const node = records[i]?.target;
132
- if (!node || isBeeChrome(node)) continue;
177
+ const el = elementOf(records[i]?.target);
178
+ if (!el) continue;
179
+ let skip = skipped.get(el);
180
+ if (skip === undefined) {
181
+ skip = isBeeChrome(el) || isReportNoise(el);
182
+ skipped.set(el, skip);
183
+ }
184
+ if (skip) continue;
133
185
 
134
- let slot = this.#counts.get(node);
186
+ let slot = this.#counts.get(el);
135
187
  if (!slot || now - slot.windowStart >= this.windowMs) {
136
188
  slot = { count: 0, windowStart: now, warned: false };
137
- this.#counts.set(node, slot);
189
+ this.#counts.set(el, slot);
138
190
  }
139
191
  slot.count += 1;
140
- this.#track(node, slot);
192
+ this.#track(el, slot);
141
193
  if (slot.count < this.threshold || slot.warned) continue;
142
194
 
143
195
  slot.warned = true;
144
- const selector = nodeSelector(node);
196
+ const selector = nodeSelector(el);
145
197
  this.core.sendData('warn', {
146
198
  source: this.source,
147
199
  message: `High Mutation Rate on ${selector}`,
@@ -150,21 +202,36 @@ export class AntAdapter {
150
202
  }
151
203
  }
152
204
 
153
- #track(node, slot) {
154
- const index = this.#hot.findIndex((row) => row.node === node);
205
+ #prune(now) {
206
+ this.#hot = this.#hot.filter((row) => now - row.windowStart < this.windowMs);
207
+ }
208
+
209
+ #track(el, slot) {
210
+ const index = this.#hot.findIndex((row) => row.node === el);
155
211
  if (slot.count < this.threshold) {
156
212
  if (index >= 0) this.#hot.splice(index, 1);
157
213
  return;
158
214
  }
159
215
  const row = {
160
- node,
161
- selector: nodeSelector(node),
216
+ node: el,
217
+ selector: nodeSelector(el),
162
218
  count: slot.count,
163
219
  windowStart: slot.windowStart
164
220
  };
165
221
  if (index >= 0) this.#hot[index] = row;
166
222
  else this.#hot.push(row);
167
- if (this.#hot.length > MAX_HOT) this.#hot.splice(0, this.#hot.length - MAX_HOT);
223
+ while (this.#hot.length > MAX_HOT) this.#hot.splice(this.#lowestRow(), 1);
224
+ }
225
+
226
+ /** Fewest mutations first; on a tie the older window goes. */
227
+ #lowestRow() {
228
+ let worst = 0;
229
+ for (let i = 1; i < this.#hot.length; i++) {
230
+ const row = this.#hot[i];
231
+ const low = this.#hot[worst];
232
+ if (row.count < low.count || (row.count === low.count && row.windowStart < low.windowStart)) worst = i;
233
+ }
234
+ return worst;
168
235
  }
169
236
  }
170
237
 
@@ -173,15 +240,26 @@ function positive(value, fallback) {
173
240
  return Number.isFinite(n) && n > 0 ? n : fallback;
174
241
  }
175
242
 
243
+ /** A text node counts on its parent element, so one element keeps one window. */
244
+ function elementOf(node) {
245
+ if (!node) return null;
246
+ if (node.nodeType === 1) return node;
247
+ return node.parentElement || null;
248
+ }
249
+
176
250
  function nodeSelector(node) {
177
- const el = node && node.nodeType === 1 ? node : node?.parentElement;
251
+ const el = elementOf(node);
178
252
  if (!el || !el.tagName) return 'node';
179
- if (el.id) return `#${el.id}`;
253
+ if (el.id) return `#${cssEscape(el.id)}`;
180
254
  const cls = el.classList && el.classList[0];
181
- if (cls) return `.${cls}`;
255
+ if (cls) return `.${cssEscape(cls)}`;
182
256
  return el.tagName.toLowerCase();
183
257
  }
184
258
 
259
+ function cssEscape(value) {
260
+ return String(value).replace(/[^\w-]/g, '\\$&');
261
+ }
262
+
185
263
  function isBeeChrome(node) {
186
264
  let el = node && node.nodeType === 1 ? node : node?.parentElement;
187
265
  while (el) {
@@ -10,7 +10,7 @@ import { UIOverlay } from './UIOverlay.js';
10
10
  import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
11
11
  import { exportReport as writeReport } from './ReportExporter.js';
12
12
 
13
- export const BEE_LADYBUG_VERSION = '0.6.4';
13
+ export const BEE_LADYBUG_VERSION = '0.6.5';
14
14
 
15
15
  export const CORE_DEFAULTS = Object.freeze({
16
16
  toggleKey: 'F2',
package/src/index.d.ts CHANGED
@@ -424,6 +424,7 @@ export class AntAdapter {
424
424
  constructor(core: BeeLadybugCore, options?: AntAdapterOptions);
425
425
  attach(): this;
426
426
  detach(): this;
427
+ setRoot(root: ParentNode | null): this;
427
428
  snapshot(): ReportSnapshot | null;
428
429
  }
429
430