bee-ladybug 0.3.0 → 0.4.1
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 +57 -6
- package/package.json +3 -3
- package/src/adapters/AntAdapter.js +144 -0
- package/src/adapters/CanvasAdapter.js +63 -0
- package/src/adapters/SpiderAdapter.js +160 -0
- package/src/core/BeeLadybugCore.js +96 -3
- package/src/core/UIOverlay.js +113 -1
- package/src/index.d.ts +61 -1
- package/src/index.js +2 -0
package/README.MD
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
# BeeLadybug
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
<p align="center">
|
|
5
|
+
<img src="https://raw.githubusercontent.com/antonioprosperi2-svg/BeeLadybug-universal/main/docs/logo.jpg" alt="BeeLadybug" width="720">
|
|
6
|
+
</p>
|
|
5
7
|
|
|
6
8
|
Universal telemetry and visual inspection overlay.
|
|
7
9
|
|
|
@@ -37,6 +39,8 @@ bee-ladybug/
|
|
|
37
39
|
│ └── adapters/
|
|
38
40
|
│ ├── CanvasAdapter.js
|
|
39
41
|
│ ├── WebDOMAdapter.js
|
|
42
|
+
│ ├── AntAdapter.js
|
|
43
|
+
│ ├── SpiderAdapter.js
|
|
40
44
|
│ └── python/PythonBridge.js
|
|
41
45
|
├── examples/
|
|
42
46
|
│ ├── core.html
|
|
@@ -96,6 +100,7 @@ Gli adapter parlano **solo** così. Uno scalare viene avvolto in `{ value }`.
|
|
|
96
100
|
| `log` | `{ message, level?, source? }` | console |
|
|
97
101
|
| `warn` | `{ message, source? }` | console ambra |
|
|
98
102
|
| `error` | `{ message, source? }` | console rossa |
|
|
103
|
+
| `telemetry` | `{ kind?, duration?, source? }` | history + lampeggio pulsante adapter |
|
|
99
104
|
| `clear` | `{ channel?: 'logs'\|'history'\|'hud' }` | reset buffer |
|
|
100
105
|
| `sys` | emesso dal Core | freeze / visibilità / scala |
|
|
101
106
|
|
|
@@ -116,7 +121,7 @@ bee.subscribe((packet) => {
|
|
|
116
121
|
});
|
|
117
122
|
```
|
|
118
123
|
|
|
119
|
-

|
|
124
|
+

|
|
120
125
|
|
|
121
126
|
## Tasti
|
|
122
127
|
|
|
@@ -172,6 +177,50 @@ adapter.attach();
|
|
|
172
177
|
|
|
173
178
|
Traduce il DOM in pacchetti: nodo sotto il puntatore, box model (margin / padding / content), conteggio nodi, mutazioni (`+n -n attr`). `F4` congela l'hover. `F2` nasconde overlay e highlight. Il Core non sa che esiste HTML.
|
|
174
179
|
|
|
180
|
+
## Registro adapter (overlay)
|
|
181
|
+
|
|
182
|
+
Il Core non conosce ANT/SPIDER. Gli adapter si iscrivono con hook propri; l'overlay disegna un toggle per ciascuno.
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
bee.registerAdapter('probe', {
|
|
186
|
+
label: 'PROBE',
|
|
187
|
+
enable: () => observer.observe(root, opts),
|
|
188
|
+
disable: () => observer.disconnect()
|
|
189
|
+
});
|
|
190
|
+
adapter.detach(); // chiama bee.unregisterAdapter('probe')
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Click sul pulsante → `enable()` / `disable()`. Un pacchetto `warn` o `telemetry` con `source` uguale al nome registrato fa lampeggiare quel pulsante, al massimo una volta ogni 1.5s.
|
|
194
|
+
|
|
195
|
+
## AntAdapter
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
import { BeeLadybugCore, AntAdapter } from './src/index.js';
|
|
199
|
+
|
|
200
|
+
const bee = new BeeLadybugCore();
|
|
201
|
+
const ant = new AntAdapter(bee, {
|
|
202
|
+
root: document.body,
|
|
203
|
+
threshold: 30 // mutazioni / secondo per nodo
|
|
204
|
+
});
|
|
205
|
+
ant.attach();
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`MutationObserver` indipendente da `WebDOMAdapter` (possono condividere lo stesso `root`). Se un nodo supera la soglia, arriva un `warn` `source: 'ant'`.
|
|
209
|
+
|
|
210
|
+
## SpiderAdapter
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
import { BeeLadybugCore, SpiderAdapter } from './src/index.js';
|
|
214
|
+
|
|
215
|
+
const bee = new BeeLadybugCore();
|
|
216
|
+
const spider = new SpiderAdapter(bee, {
|
|
217
|
+
resourceThreshold: 500 // ms
|
|
218
|
+
});
|
|
219
|
+
spider.attach();
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`PerformanceObserver`: `longtask` > 50ms → `telemetry`; risorse > soglia → `warn`. Se il browser non espone `longtask` (Firefox/Safari), un log una tantum e quella parte si spegne senza errori.
|
|
223
|
+
|
|
175
224
|
## PythonBridge
|
|
176
225
|
|
|
177
226
|
Python non vede l'overlay. Manda JSON. Il browser lo srotola in `sendData()`.
|
|
@@ -202,7 +251,7 @@ python examples/python/sender.py
|
|
|
202
251
|
|
|
203
252
|
Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
|
|
204
253
|
|
|
205
|
-

|
|
254
|
+

|
|
206
255
|
|
|
207
256
|
## npm / TypeScript
|
|
208
257
|
|
|
@@ -228,7 +277,7 @@ npm login
|
|
|
228
277
|
npm publish --access public
|
|
229
278
|
```
|
|
230
279
|
|
|
231
|
-
Versione runtime e pacchetto: **0.
|
|
280
|
+
Versione runtime e pacchetto: **0.4.1** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali).
|
|
232
281
|
|
|
233
282
|
## Assistente AI (gancio, nessun vendor)
|
|
234
283
|
|
|
@@ -293,13 +342,15 @@ bee.applySlowMo(); bee.restoreRealtime();
|
|
|
293
342
|
bee.registerAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
|
|
294
343
|
bee.setActiveAssistant('mock');
|
|
295
344
|
await bee.ask('why fps drop?');
|
|
345
|
+
bee.registerAdapter('probe', { label: 'PROBE', enable() {}, disable() {} });
|
|
346
|
+
bee.toggleAdapter('probe');
|
|
296
347
|
bee.destroy();
|
|
297
348
|
```
|
|
298
349
|
|
|
299
350
|
## Roadmap
|
|
300
351
|
|
|
301
|
-
1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
|
|
302
|
-
2. **npm 0.
|
|
352
|
+
1. **Ora** — Core, adapter, registro toggle overlay, Ant/Spider, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
|
|
353
|
+
2. **npm 0.4.1** — `npm publish`.
|
|
303
354
|
|
|
304
355
|
## Come contribuire
|
|
305
356
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bee-ladybug",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
21
|
"lint": "eslint src examples",
|
|
22
|
-
"test": "node --test tests
|
|
22
|
+
"test": "node --test tests/*.test.js"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"debug",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://github.com/antonioprosperi2-svg/BeeLadybug-universal#readme",
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
48
|
+
"node": ">=20"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.36.0",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AntAdapter — DOM mutation rate tracker.
|
|
3
|
+
*
|
|
4
|
+
* Independent MutationObserver. Can sit on the same root as WebDOMAdapter.
|
|
5
|
+
* The core must never import this file.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const ant = new AntAdapter(core, { root: document.body, threshold: 30 });
|
|
9
|
+
* ant.attach();
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const ANT_ADAPTER_DEFAULTS = Object.freeze({
|
|
13
|
+
threshold: 30,
|
|
14
|
+
windowMs: 1000,
|
|
15
|
+
source: 'ant'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export class AntAdapter {
|
|
19
|
+
/**
|
|
20
|
+
* @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
|
|
21
|
+
* @param {Partial<typeof ANT_ADAPTER_DEFAULTS> & {
|
|
22
|
+
* root?: ParentNode | null
|
|
23
|
+
* }} [options]
|
|
24
|
+
*/
|
|
25
|
+
constructor(core, options = {}) {
|
|
26
|
+
const cfg = { ...ANT_ADAPTER_DEFAULTS, ...options };
|
|
27
|
+
this.core = core;
|
|
28
|
+
this.root = options.root !== undefined
|
|
29
|
+
? options.root
|
|
30
|
+
: (typeof document !== 'undefined' ? document.body : null);
|
|
31
|
+
this.threshold = positive(cfg.threshold, ANT_ADAPTER_DEFAULTS.threshold);
|
|
32
|
+
this.windowMs = positive(cfg.windowMs, ANT_ADAPTER_DEFAULTS.windowMs);
|
|
33
|
+
this.source = cfg.source;
|
|
34
|
+
|
|
35
|
+
this.#attached = false;
|
|
36
|
+
this.#observer = null;
|
|
37
|
+
this.#counts = new WeakMap();
|
|
38
|
+
this.#onMutate = (records) => this.#ingest(records);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#attached;
|
|
42
|
+
#observer;
|
|
43
|
+
#counts;
|
|
44
|
+
#onMutate;
|
|
45
|
+
|
|
46
|
+
attach() {
|
|
47
|
+
if (this.#attached) return this;
|
|
48
|
+
this.#attached = true;
|
|
49
|
+
this.#enable();
|
|
50
|
+
this.core?.registerAdapter('ant', {
|
|
51
|
+
enable: () => this.#enable(),
|
|
52
|
+
disable: () => this.#disable(),
|
|
53
|
+
label: 'ANT'
|
|
54
|
+
});
|
|
55
|
+
this.core?.sendData('log', {
|
|
56
|
+
source: this.source,
|
|
57
|
+
message: 'AntAdapter attached. Watching DOM mutation rate.'
|
|
58
|
+
});
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
detach() {
|
|
63
|
+
if (!this.#attached) return this;
|
|
64
|
+
this.#attached = false;
|
|
65
|
+
this.#disable();
|
|
66
|
+
this.core?.unregisterAdapter('ant');
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#enable() {
|
|
71
|
+
if (!this.#attached || this.#observer) return;
|
|
72
|
+
if (typeof MutationObserver === 'undefined' || !this.root) return;
|
|
73
|
+
this.#counts = new WeakMap();
|
|
74
|
+
this.#observer = new MutationObserver(this.#onMutate);
|
|
75
|
+
try {
|
|
76
|
+
this.#observer.observe(this.root, {
|
|
77
|
+
childList: true,
|
|
78
|
+
subtree: true,
|
|
79
|
+
attributes: true,
|
|
80
|
+
characterData: true
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
this.#observer.disconnect();
|
|
84
|
+
this.#observer = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#disable() {
|
|
89
|
+
if (this.#observer) this.#observer.disconnect();
|
|
90
|
+
this.#observer = null;
|
|
91
|
+
this.#counts = new WeakMap();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#ingest(records) {
|
|
95
|
+
if (!this.#attached || !this.core || !records?.length) return;
|
|
96
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
97
|
+
for (let i = 0; i < records.length; i++) {
|
|
98
|
+
const node = records[i]?.target;
|
|
99
|
+
if (!node || isBeeChrome(node)) continue;
|
|
100
|
+
|
|
101
|
+
let slot = this.#counts.get(node);
|
|
102
|
+
if (!slot || now - slot.windowStart >= this.windowMs) {
|
|
103
|
+
slot = { count: 0, windowStart: now, warned: false };
|
|
104
|
+
this.#counts.set(node, slot);
|
|
105
|
+
}
|
|
106
|
+
slot.count += 1;
|
|
107
|
+
if (slot.count < this.threshold || slot.warned) continue;
|
|
108
|
+
|
|
109
|
+
slot.warned = true;
|
|
110
|
+
const selector = nodeSelector(node);
|
|
111
|
+
this.core.sendData('warn', {
|
|
112
|
+
source: this.source,
|
|
113
|
+
message: `High Mutation Rate on ${selector}`,
|
|
114
|
+
node: selector
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function positive(value, fallback) {
|
|
121
|
+
const n = Number(value);
|
|
122
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function nodeSelector(node) {
|
|
126
|
+
const el = node && node.nodeType === 1 ? node : node?.parentElement;
|
|
127
|
+
if (!el || !el.tagName) return 'node';
|
|
128
|
+
if (el.id) return `#${el.id}`;
|
|
129
|
+
const cls = el.classList && el.classList[0];
|
|
130
|
+
if (cls) return `.${cls}`;
|
|
131
|
+
return el.tagName.toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isBeeChrome(node) {
|
|
135
|
+
let el = node && node.nodeType === 1 ? node : node?.parentElement;
|
|
136
|
+
while (el) {
|
|
137
|
+
const tag = el.tagName;
|
|
138
|
+
if (tag === 'BEE-LADYBUG-OVERLAY') return true;
|
|
139
|
+
if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
|
|
140
|
+
if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
|
|
141
|
+
el = el.parentElement;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
@@ -15,11 +15,17 @@
|
|
|
15
15
|
* adapter.attach();
|
|
16
16
|
* // after updating entity positions each host frame:
|
|
17
17
|
* adapter.pump();
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // third-party / bookmarklet: no access to the host loop
|
|
21
|
+
* const remote = new CanvasAdapter(core, { canvas, entities, autoPump: true });
|
|
22
|
+
* remote.attach();
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
25
|
export const CANVAS_ADAPTER_DEFAULTS = Object.freeze({
|
|
21
26
|
overlay: true,
|
|
22
27
|
computeCollisions: true,
|
|
28
|
+
autoPump: false,
|
|
23
29
|
colorActive: '#3dff6a',
|
|
24
30
|
colorColliding: '#ff3b3b',
|
|
25
31
|
colorInactive: '#8a8a8a',
|
|
@@ -41,6 +47,7 @@ export class CanvasAdapter {
|
|
|
41
47
|
this.entities = options.entities ?? [];
|
|
42
48
|
this.overlayEnabled = cfg.overlay;
|
|
43
49
|
this.computeCollisions = cfg.computeCollisions;
|
|
50
|
+
this.autoPump = Boolean(cfg.autoPump);
|
|
44
51
|
this.colorActive = cfg.colorActive;
|
|
45
52
|
this.colorColliding = cfg.colorColliding;
|
|
46
53
|
this.colorInactive = cfg.colorInactive;
|
|
@@ -54,19 +61,35 @@ export class CanvasAdapter {
|
|
|
54
61
|
this.#attached = false;
|
|
55
62
|
this.#overlay = null;
|
|
56
63
|
this.#unsubscribe = null;
|
|
64
|
+
this.#rafId = 0;
|
|
65
|
+
this.#pumpFromAuto = false;
|
|
66
|
+
this.#manualPumpWarned = false;
|
|
57
67
|
this.#lastPump = 0;
|
|
58
68
|
this.#fps = 0;
|
|
59
69
|
this.#wasColliding = new Set();
|
|
60
70
|
this.#onResize = () => this.#syncOverlay();
|
|
71
|
+
this.#onTick = () => {
|
|
72
|
+
this.#rafId = requestAnimationFrame(this.#onTick);
|
|
73
|
+
this.#pumpFromAuto = true;
|
|
74
|
+
try {
|
|
75
|
+
this.pump();
|
|
76
|
+
} finally {
|
|
77
|
+
this.#pumpFromAuto = false;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
#attached;
|
|
64
83
|
#overlay;
|
|
65
84
|
#unsubscribe;
|
|
85
|
+
#rafId;
|
|
86
|
+
#pumpFromAuto;
|
|
87
|
+
#manualPumpWarned;
|
|
66
88
|
#lastPump;
|
|
67
89
|
#fps;
|
|
68
90
|
#wasColliding;
|
|
69
91
|
#onResize;
|
|
92
|
+
#onTick;
|
|
70
93
|
|
|
71
94
|
attach() {
|
|
72
95
|
if (this.#attached) return this;
|
|
@@ -83,6 +106,7 @@ export class CanvasAdapter {
|
|
|
83
106
|
window.addEventListener('resize', this.#onResize);
|
|
84
107
|
window.addEventListener('scroll', this.#onResize, true);
|
|
85
108
|
}
|
|
109
|
+
if (this.autoPump) this.startAutoPump();
|
|
86
110
|
|
|
87
111
|
this.core?.sendData('log', {
|
|
88
112
|
source: this.source,
|
|
@@ -99,6 +123,7 @@ export class CanvasAdapter {
|
|
|
99
123
|
detach() {
|
|
100
124
|
if (!this.#attached) return this;
|
|
101
125
|
this.#attached = false;
|
|
126
|
+
this.#stopAutoPumpLoop();
|
|
102
127
|
if (this.#unsubscribe) this.#unsubscribe();
|
|
103
128
|
this.#unsubscribe = null;
|
|
104
129
|
this.#unmountOverlay();
|
|
@@ -109,6 +134,20 @@ export class CanvasAdapter {
|
|
|
109
134
|
return this;
|
|
110
135
|
}
|
|
111
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Drive `pump()` from an internal `requestAnimationFrame` loop.
|
|
139
|
+
* Use this when the host game loop cannot call `pump()` (bookmarklet / console).
|
|
140
|
+
*/
|
|
141
|
+
startAutoPump() {
|
|
142
|
+
this.autoPump = true;
|
|
143
|
+
if (!this.#attached) return this;
|
|
144
|
+
if (this.#rafId) return this;
|
|
145
|
+
if (typeof requestAnimationFrame !== 'function') return this;
|
|
146
|
+
this.#manualPumpWarned = false;
|
|
147
|
+
this.#rafId = requestAnimationFrame(this.#onTick);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
/** @param {HTMLCanvasElement | null} canvas */
|
|
113
152
|
setCanvas(canvas) {
|
|
114
153
|
this.canvas = canvas;
|
|
@@ -126,10 +165,15 @@ export class CanvasAdapter {
|
|
|
126
165
|
* Call once per host frame, after entity positions are updated.
|
|
127
166
|
* Pass a 2D context only if overlay is disabled and you want hitboxes
|
|
128
167
|
* drawn onto the game canvas itself.
|
|
168
|
+
* Ignored (with a warning) while `autoPump` owns the frame loop.
|
|
129
169
|
* @param {CanvasRenderingContext2D} [ctx]
|
|
130
170
|
*/
|
|
131
171
|
pump(ctx) {
|
|
132
172
|
if (!this.#attached || !this.core) return this;
|
|
173
|
+
if (this.#rafId && !this.#pumpFromAuto) {
|
|
174
|
+
this.#warnManualPump();
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
133
177
|
|
|
134
178
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
135
179
|
if (this.#lastPump) {
|
|
@@ -147,6 +191,25 @@ export class CanvasAdapter {
|
|
|
147
191
|
return this;
|
|
148
192
|
}
|
|
149
193
|
|
|
194
|
+
#stopAutoPumpLoop() {
|
|
195
|
+
if (this.#rafId && typeof cancelAnimationFrame === 'function') {
|
|
196
|
+
cancelAnimationFrame(this.#rafId);
|
|
197
|
+
}
|
|
198
|
+
this.#rafId = 0;
|
|
199
|
+
this.#pumpFromAuto = false;
|
|
200
|
+
this.#manualPumpWarned = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#warnManualPump() {
|
|
204
|
+
if (this.#manualPumpWarned) return;
|
|
205
|
+
this.#manualPumpWarned = true;
|
|
206
|
+
const message = 'pump() skipped: autoPump already drives the frame loop.';
|
|
207
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
208
|
+
console.warn(`[BeeLadybug] CanvasAdapter ${message}`);
|
|
209
|
+
}
|
|
210
|
+
this.core.sendData('warn', { source: this.source, message });
|
|
211
|
+
}
|
|
212
|
+
|
|
150
213
|
#onCore(packet) {
|
|
151
214
|
if (packet.type !== 'sys') return;
|
|
152
215
|
const op = packet.payload.op;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpiderAdapter — long-task and slow-resource tracker.
|
|
3
|
+
*
|
|
4
|
+
* Native PerformanceObserver only. The core must never import this file.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const spider = new SpiderAdapter(core, { resourceThreshold: 500 });
|
|
8
|
+
* spider.attach();
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SPIDER_ADAPTER_DEFAULTS = Object.freeze({
|
|
12
|
+
longTaskThreshold: 50,
|
|
13
|
+
resourceThreshold: 500,
|
|
14
|
+
source: 'spider'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export class SpiderAdapter {
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
|
|
20
|
+
* @param {Partial<typeof SPIDER_ADAPTER_DEFAULTS>} [options]
|
|
21
|
+
*/
|
|
22
|
+
constructor(core, options = {}) {
|
|
23
|
+
const cfg = { ...SPIDER_ADAPTER_DEFAULTS, ...options };
|
|
24
|
+
this.core = core;
|
|
25
|
+
this.longTaskThreshold = positive(cfg.longTaskThreshold, SPIDER_ADAPTER_DEFAULTS.longTaskThreshold);
|
|
26
|
+
this.resourceThreshold = positive(cfg.resourceThreshold, SPIDER_ADAPTER_DEFAULTS.resourceThreshold);
|
|
27
|
+
this.source = cfg.source;
|
|
28
|
+
|
|
29
|
+
this.#attached = false;
|
|
30
|
+
this.#longObserver = null;
|
|
31
|
+
this.#resourceObserver = null;
|
|
32
|
+
this.#longTaskMissingLogged = false;
|
|
33
|
+
this.#onLongTask = (list) => this.#ingestLongTasks(list);
|
|
34
|
+
this.#onResource = (list) => this.#ingestResources(list);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#attached;
|
|
38
|
+
#longObserver;
|
|
39
|
+
#resourceObserver;
|
|
40
|
+
#longTaskMissingLogged;
|
|
41
|
+
#onLongTask;
|
|
42
|
+
#onResource;
|
|
43
|
+
|
|
44
|
+
attach() {
|
|
45
|
+
if (this.#attached) return this;
|
|
46
|
+
this.#attached = true;
|
|
47
|
+
this.#enable();
|
|
48
|
+
this.core?.registerAdapter('spider', {
|
|
49
|
+
enable: () => this.#enable(),
|
|
50
|
+
disable: () => this.#disable(),
|
|
51
|
+
label: 'SPIDER'
|
|
52
|
+
});
|
|
53
|
+
this.core?.sendData('log', {
|
|
54
|
+
source: this.source,
|
|
55
|
+
message: 'SpiderAdapter attached. Watching long tasks and slow resources.'
|
|
56
|
+
});
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
detach() {
|
|
61
|
+
if (!this.#attached) return this;
|
|
62
|
+
this.#attached = false;
|
|
63
|
+
this.#disable();
|
|
64
|
+
this.core?.unregisterAdapter('spider');
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#enable() {
|
|
69
|
+
if (!this.#attached) return;
|
|
70
|
+
if (typeof PerformanceObserver !== 'function') {
|
|
71
|
+
this.#logLongTaskUnavailable();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const types = supportedEntryTypes();
|
|
75
|
+
if (types.includes('longtask')) this.#watchLongTasks();
|
|
76
|
+
else this.#logLongTaskUnavailable();
|
|
77
|
+
if (types.includes('resource')) this.#watchResources();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#disable() {
|
|
81
|
+
if (this.#longObserver) this.#longObserver.disconnect();
|
|
82
|
+
if (this.#resourceObserver) this.#resourceObserver.disconnect();
|
|
83
|
+
this.#longObserver = null;
|
|
84
|
+
this.#resourceObserver = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#watchLongTasks() {
|
|
88
|
+
if (this.#longObserver) return;
|
|
89
|
+
try {
|
|
90
|
+
this.#longObserver = new PerformanceObserver(this.#onLongTask);
|
|
91
|
+
this.#longObserver.observe({ entryTypes: ['longtask'] });
|
|
92
|
+
} catch {
|
|
93
|
+
this.#longObserver = null;
|
|
94
|
+
this.#logLongTaskUnavailable();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#watchResources() {
|
|
99
|
+
if (this.#resourceObserver) return;
|
|
100
|
+
try {
|
|
101
|
+
this.#resourceObserver = new PerformanceObserver(this.#onResource);
|
|
102
|
+
this.#resourceObserver.observe({ entryTypes: ['resource'] });
|
|
103
|
+
} catch {
|
|
104
|
+
this.#resourceObserver = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#ingestLongTasks(list) {
|
|
109
|
+
if (!this.#attached || !this.core) return;
|
|
110
|
+
const entries = list?.getEntries?.() ?? [];
|
|
111
|
+
for (let i = 0; i < entries.length; i++) {
|
|
112
|
+
const entry = entries[i];
|
|
113
|
+
if (!entry || entry.duration <= this.longTaskThreshold) continue;
|
|
114
|
+
this.core.sendData('telemetry', {
|
|
115
|
+
source: this.source,
|
|
116
|
+
kind: 'longtask',
|
|
117
|
+
duration: entry.duration,
|
|
118
|
+
attribution: entry.attribution?.[0]?.containerType ?? 'unknown'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#ingestResources(list) {
|
|
124
|
+
if (!this.#attached || !this.core) return;
|
|
125
|
+
const entries = list?.getEntries?.() ?? [];
|
|
126
|
+
for (let i = 0; i < entries.length; i++) {
|
|
127
|
+
const entry = entries[i];
|
|
128
|
+
if (!entry || entry.duration <= this.resourceThreshold) continue;
|
|
129
|
+
this.core.sendData('warn', {
|
|
130
|
+
source: this.source,
|
|
131
|
+
message: `Risorsa lenta: ${entry.name} (${Math.round(entry.duration)}ms)`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#logLongTaskUnavailable() {
|
|
137
|
+
if (this.#longTaskMissingLogged) return;
|
|
138
|
+
this.#longTaskMissingLogged = true;
|
|
139
|
+
this.core?.sendData('log', {
|
|
140
|
+
source: this.source,
|
|
141
|
+
message: 'Long task detection non disponibile in questo browser'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function positive(value, fallback) {
|
|
147
|
+
const n = Number(value);
|
|
148
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function supportedEntryTypes() {
|
|
152
|
+
if (typeof PerformanceObserver !== 'function') return [];
|
|
153
|
+
const list = PerformanceObserver.supportedEntryTypes;
|
|
154
|
+
if (!list) return [];
|
|
155
|
+
try {
|
|
156
|
+
return Array.from(list);
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { UIOverlay } from './UIOverlay.js';
|
|
10
10
|
import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
|
|
11
11
|
|
|
12
|
-
export const BEE_LADYBUG_VERSION = '0.
|
|
12
|
+
export const BEE_LADYBUG_VERSION = '0.4.1';
|
|
13
13
|
|
|
14
14
|
export const CORE_DEFAULTS = Object.freeze({
|
|
15
15
|
toggleKey: 'F2',
|
|
@@ -35,7 +35,8 @@ export const WELL_KNOWN_TYPES = Object.freeze([
|
|
|
35
35
|
'error',
|
|
36
36
|
'state',
|
|
37
37
|
'sys',
|
|
38
|
-
'clear'
|
|
38
|
+
'clear',
|
|
39
|
+
'telemetry'
|
|
39
40
|
]);
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -96,6 +97,7 @@ export class BeeLadybugCore {
|
|
|
96
97
|
this.#assistants = new Map();
|
|
97
98
|
this.#activeAssistant = null;
|
|
98
99
|
this.#askBusy = false;
|
|
100
|
+
this.#adapters = new Map();
|
|
99
101
|
|
|
100
102
|
this.ui = null;
|
|
101
103
|
if (cfg.ui !== false && typeof document !== 'undefined') {
|
|
@@ -130,6 +132,8 @@ export class BeeLadybugCore {
|
|
|
130
132
|
/** @type {string | null} */
|
|
131
133
|
#activeAssistant;
|
|
132
134
|
#askBusy;
|
|
135
|
+
/** @type {Map<string, { name: string, label: string, enabled: boolean, enable: Function, disable: Function }>} */
|
|
136
|
+
#adapters;
|
|
133
137
|
|
|
134
138
|
/**
|
|
135
139
|
* Public adapter contract. `type` is a channel name; `payload` is raw data.
|
|
@@ -281,6 +285,86 @@ export class BeeLadybugCore {
|
|
|
281
285
|
return this.#activeAssistant ? this.#assistants.get(this.#activeAssistant) ?? null : null;
|
|
282
286
|
}
|
|
283
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Register a toggleable diagnostic adapter. The core stores hooks only —
|
|
290
|
+
* it does not import or special-case adapter modules.
|
|
291
|
+
* @param {string} name
|
|
292
|
+
* @param {{ enable: () => void, disable: () => void, label?: string }} hooks
|
|
293
|
+
*/
|
|
294
|
+
registerAdapter(name, hooks = {}) {
|
|
295
|
+
const key = String(name || '').trim();
|
|
296
|
+
if (!key || typeof hooks.enable !== 'function' || typeof hooks.disable !== 'function') {
|
|
297
|
+
this.sendData('warn', {
|
|
298
|
+
source: 'core',
|
|
299
|
+
message: 'registerAdapter() needs a name and { enable, disable }.'
|
|
300
|
+
});
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
const label = String(hooks.label || key).trim() || key;
|
|
304
|
+
this.#adapters.set(key, {
|
|
305
|
+
name: key,
|
|
306
|
+
label,
|
|
307
|
+
enabled: true,
|
|
308
|
+
enable: hooks.enable,
|
|
309
|
+
disable: hooks.disable
|
|
310
|
+
});
|
|
311
|
+
this.sendData('sys', { op: 'adapter', name: key, registered: true, label, enabled: true });
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {string} name
|
|
317
|
+
*/
|
|
318
|
+
unregisterAdapter(name) {
|
|
319
|
+
const key = String(name || '').trim();
|
|
320
|
+
if (!key || !this.#adapters.has(key)) return this;
|
|
321
|
+
this.#adapters.delete(key);
|
|
322
|
+
this.sendData('sys', { op: 'adapter', name: key, registered: false });
|
|
323
|
+
return this;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Registered diagnostic adapters, insertion order. */
|
|
327
|
+
getAdapters() {
|
|
328
|
+
return [...this.#adapters.values()].map((entry) => ({
|
|
329
|
+
name: entry.name,
|
|
330
|
+
label: entry.label,
|
|
331
|
+
enabled: entry.enabled
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* @param {string} name
|
|
337
|
+
*/
|
|
338
|
+
toggleAdapter(name) {
|
|
339
|
+
const entry = this.#adapters.get(String(name || '').trim());
|
|
340
|
+
if (!entry) return this;
|
|
341
|
+
return this.setAdapterEnabled(entry.name, !entry.enabled);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {string} name
|
|
346
|
+
* @param {boolean} enabled
|
|
347
|
+
*/
|
|
348
|
+
setAdapterEnabled(name, enabled) {
|
|
349
|
+
const entry = this.#adapters.get(String(name || '').trim());
|
|
350
|
+
if (!entry) return this;
|
|
351
|
+
const next = Boolean(enabled);
|
|
352
|
+
if (entry.enabled === next) return this;
|
|
353
|
+
try {
|
|
354
|
+
if (next) entry.enable();
|
|
355
|
+
else entry.disable();
|
|
356
|
+
entry.enabled = next;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.warn('[BeeLadybug] adapter toggle error', err);
|
|
359
|
+
}
|
|
360
|
+
this.sendData('sys', {
|
|
361
|
+
op: 'adapter',
|
|
362
|
+
name: entry.name,
|
|
363
|
+
enabled: entry.enabled
|
|
364
|
+
});
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
284
368
|
/**
|
|
285
369
|
* @param {{ name?: string, complete: Function }} provider
|
|
286
370
|
* @param {{ activate: boolean, via: string }} opts
|
|
@@ -353,6 +437,7 @@ export class BeeLadybugCore {
|
|
|
353
437
|
this.#assistants.clear();
|
|
354
438
|
this.#activeAssistant = null;
|
|
355
439
|
this.#askBusy = false;
|
|
440
|
+
this.#adapters.clear();
|
|
356
441
|
this.ui?.destroy();
|
|
357
442
|
this.ui = null;
|
|
358
443
|
return this;
|
|
@@ -441,7 +526,8 @@ export class BeeLadybugCore {
|
|
|
441
526
|
hud: this.getHud(),
|
|
442
527
|
mode: this.#modeLabel(),
|
|
443
528
|
assistant: this.#activeAssistant,
|
|
444
|
-
assistants: this.getAssistants()
|
|
529
|
+
assistants: this.getAssistants(),
|
|
530
|
+
adapters: this.getAdapters()
|
|
445
531
|
};
|
|
446
532
|
}
|
|
447
533
|
|
|
@@ -553,6 +639,13 @@ export class BeeLadybugCore {
|
|
|
553
639
|
return;
|
|
554
640
|
}
|
|
555
641
|
|
|
642
|
+
if (type === 'telemetry') {
|
|
643
|
+
const name = String(payload.kind ?? payload.name ?? 'telemetry');
|
|
644
|
+
const value = numeric(payload.duration ?? payload.value);
|
|
645
|
+
if (value != null) this.#pushHistory(name, value);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
556
649
|
if (isConsoleType(type) || packet.level) {
|
|
557
650
|
this.#pushLog(packet);
|
|
558
651
|
}
|
package/src/core/UIOverlay.js
CHANGED
|
@@ -247,6 +247,41 @@ const STYLES = `
|
|
|
247
247
|
.bl-providers:empty {
|
|
248
248
|
display: none;
|
|
249
249
|
}
|
|
250
|
+
.bl-adapters {
|
|
251
|
+
display: inline-flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
gap: 3px;
|
|
254
|
+
}
|
|
255
|
+
.bl-adapters:empty {
|
|
256
|
+
display: none;
|
|
257
|
+
}
|
|
258
|
+
.bl-adapter {
|
|
259
|
+
appearance: none;
|
|
260
|
+
background: #16181f;
|
|
261
|
+
color: #8b91a3;
|
|
262
|
+
border: 1px solid rgba(139, 145, 163, 0.45);
|
|
263
|
+
font: 700 9px ui-monospace, Consolas, monospace;
|
|
264
|
+
letter-spacing: 0.06em;
|
|
265
|
+
padding: 5px 7px;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
}
|
|
268
|
+
.bl-adapter[data-hot="true"] {
|
|
269
|
+
color: #fff6d8;
|
|
270
|
+
border-color: #ffd36a;
|
|
271
|
+
}
|
|
272
|
+
.bl-adapter[data-flash="true"] {
|
|
273
|
+
animation: bl-adapter-flash 0.55s ease;
|
|
274
|
+
}
|
|
275
|
+
@keyframes bl-adapter-flash {
|
|
276
|
+
0% {
|
|
277
|
+
background: #ffd36a;
|
|
278
|
+
color: #05060a;
|
|
279
|
+
border-color: #ffd36a;
|
|
280
|
+
}
|
|
281
|
+
100% {
|
|
282
|
+
background: #16181f;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
250
285
|
.bl-provider {
|
|
251
286
|
appearance: none;
|
|
252
287
|
width: 22px;
|
|
@@ -271,6 +306,12 @@ const STYLES = `
|
|
|
271
306
|
}
|
|
272
307
|
@media (prefers-reduced-motion: reduce) {
|
|
273
308
|
.bl-scan { display: none; }
|
|
309
|
+
.bl-adapter[data-flash="true"] {
|
|
310
|
+
animation: none;
|
|
311
|
+
background: #ffd36a;
|
|
312
|
+
color: #05060a;
|
|
313
|
+
border-color: #ffd36a;
|
|
314
|
+
}
|
|
274
315
|
}
|
|
275
316
|
`;
|
|
276
317
|
|
|
@@ -289,6 +330,9 @@ export class UIOverlay {
|
|
|
289
330
|
this.#lastLogId = 0;
|
|
290
331
|
this.#mounted = false;
|
|
291
332
|
this.#providerSig = '';
|
|
333
|
+
this.#adapterSig = '';
|
|
334
|
+
this.#flashAt = new Map();
|
|
335
|
+
this.#flashTimers = new Map();
|
|
292
336
|
}
|
|
293
337
|
|
|
294
338
|
#host;
|
|
@@ -298,6 +342,9 @@ export class UIOverlay {
|
|
|
298
342
|
#lastLogId;
|
|
299
343
|
#mounted;
|
|
300
344
|
#providerSig;
|
|
345
|
+
#adapterSig;
|
|
346
|
+
#flashAt;
|
|
347
|
+
#flashTimers;
|
|
301
348
|
|
|
302
349
|
/** @param {'auto' | HTMLElement} [target] */
|
|
303
350
|
mount(target = 'auto') {
|
|
@@ -353,6 +400,7 @@ export class UIOverlay {
|
|
|
353
400
|
<button class="bl-btn" data-act="live" type="button">1x LIVE</button>
|
|
354
401
|
<button class="bl-btn" data-act="ask" type="button">ASK AI</button>
|
|
355
402
|
<span class="bl-providers" data-el="providers"></span>
|
|
403
|
+
<span class="bl-adapters" data-el="adapters"></span>
|
|
356
404
|
<span class="bl-hint">F2 overlay</span>
|
|
357
405
|
</div>
|
|
358
406
|
</div>
|
|
@@ -371,7 +419,8 @@ export class UIOverlay {
|
|
|
371
419
|
graphStats: this.#root.querySelector('[data-el="graphStats"]'),
|
|
372
420
|
hud: this.#root.querySelector('[data-el="hud"]'),
|
|
373
421
|
console: this.#root.querySelector('[data-el="console"]'),
|
|
374
|
-
providers: this.#root.querySelector('[data-el="providers"]')
|
|
422
|
+
providers: this.#root.querySelector('[data-el="providers"]'),
|
|
423
|
+
adapters: this.#root.querySelector('[data-el="adapters"]')
|
|
375
424
|
};
|
|
376
425
|
|
|
377
426
|
this.#root.querySelector('.bl-foot').addEventListener('click', (event) => {
|
|
@@ -383,6 +432,7 @@ export class UIOverlay {
|
|
|
383
432
|
else if (act === 'live') this.core.restoreRealtime();
|
|
384
433
|
else if (act === 'ask') this.core.ask();
|
|
385
434
|
else if (act === 'assist') this.core.setActiveAssistant(btn.getAttribute('data-name'));
|
|
435
|
+
else if (act === 'adapter') this.core.toggleAdapter(btn.getAttribute('data-name'));
|
|
386
436
|
});
|
|
387
437
|
|
|
388
438
|
this.#els.console.addEventListener('scroll', () => {
|
|
@@ -405,6 +455,9 @@ export class UIOverlay {
|
|
|
405
455
|
this.#els = null;
|
|
406
456
|
this.#mounted = false;
|
|
407
457
|
this.#providerSig = '';
|
|
458
|
+
this.#adapterSig = '';
|
|
459
|
+
this.#clearFlashTimers();
|
|
460
|
+
this.#flashAt.clear();
|
|
408
461
|
return this;
|
|
409
462
|
}
|
|
410
463
|
|
|
@@ -423,6 +476,7 @@ export class UIOverlay {
|
|
|
423
476
|
onPacket(packet) {
|
|
424
477
|
if (!this.#els || !packet) return;
|
|
425
478
|
if (packet.type === 'sys') return;
|
|
479
|
+
this.#flashAdapterButton(packet);
|
|
426
480
|
if (packet.level || packet.type === 'log' || packet.type === 'warn' || packet.type === 'error') {
|
|
427
481
|
this.#appendLine(packet);
|
|
428
482
|
}
|
|
@@ -454,6 +508,7 @@ export class UIOverlay {
|
|
|
454
508
|
this.#renderHud(state.hud);
|
|
455
509
|
this.#syncButtons(state);
|
|
456
510
|
this.#syncProviders(state);
|
|
511
|
+
this.#syncAdapters(state);
|
|
457
512
|
|
|
458
513
|
if (this.#stickBottom) {
|
|
459
514
|
this.#els.console.scrollTop = this.#els.console.scrollHeight;
|
|
@@ -529,6 +584,63 @@ export class UIOverlay {
|
|
|
529
584
|
wrap.innerHTML = html;
|
|
530
585
|
}
|
|
531
586
|
|
|
587
|
+
#syncAdapters(state) {
|
|
588
|
+
const wrap = this.#els.adapters;
|
|
589
|
+
if (!wrap) return;
|
|
590
|
+
const list = Array.isArray(state.adapters) ? state.adapters : [];
|
|
591
|
+
const sig = list.map((entry) => `${entry.name}\0${entry.label}\0${entry.enabled ? 1 : 0}`).join('\n');
|
|
592
|
+
if (sig === this.#adapterSig) return;
|
|
593
|
+
this.#adapterSig = sig;
|
|
594
|
+
let html = '';
|
|
595
|
+
for (let i = 0; i < list.length; i++) {
|
|
596
|
+
const entry = list[i];
|
|
597
|
+
const hot = entry.enabled ? 'true' : 'false';
|
|
598
|
+
html += `<button class="bl-adapter" data-act="adapter" data-name="${escapeHtml(entry.name)}" data-hot="${hot}" type="button" title="${escapeHtml(entry.label)}">${escapeHtml(entry.label)}</button>`;
|
|
599
|
+
}
|
|
600
|
+
wrap.innerHTML = html;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#flashAdapterButton(packet) {
|
|
604
|
+
if (packet.type !== 'warn' && packet.type !== 'telemetry') return;
|
|
605
|
+
const wrap = this.#els.adapters;
|
|
606
|
+
if (!wrap || !this.core || typeof this.core.getAdapters !== 'function') return;
|
|
607
|
+
const name = packet.source;
|
|
608
|
+
if (!this.core.getAdapters().some((entry) => entry.name === name)) return;
|
|
609
|
+
|
|
610
|
+
const escaped = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
|
|
611
|
+
? CSS.escape(name)
|
|
612
|
+
: name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
613
|
+
const btn = wrap.querySelector(`[data-name="${escaped}"]`);
|
|
614
|
+
if (!btn) return;
|
|
615
|
+
|
|
616
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
617
|
+
const last = this.#flashAt.get(name) ?? 0;
|
|
618
|
+
if (now - last < 1500) return;
|
|
619
|
+
this.#flashAt.set(name, now);
|
|
620
|
+
|
|
621
|
+
btn.dataset.flash = 'false';
|
|
622
|
+
void btn.offsetWidth;
|
|
623
|
+
btn.dataset.flash = 'true';
|
|
624
|
+
|
|
625
|
+
const prev = this.#flashTimers.get(name);
|
|
626
|
+
if (prev && typeof clearTimeout === 'function') clearTimeout(prev);
|
|
627
|
+
if (typeof setTimeout !== 'function') return;
|
|
628
|
+
const timer = setTimeout(() => {
|
|
629
|
+
this.#flashTimers.delete(name);
|
|
630
|
+
if (btn.dataset.flash === 'true') btn.dataset.flash = 'false';
|
|
631
|
+
}, 560);
|
|
632
|
+
this.#flashTimers.set(name, timer);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#clearFlashTimers() {
|
|
636
|
+
if (typeof clearTimeout !== 'function') {
|
|
637
|
+
this.#flashTimers.clear();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
for (const timer of this.#flashTimers.values()) clearTimeout(timer);
|
|
641
|
+
this.#flashTimers.clear();
|
|
642
|
+
}
|
|
643
|
+
|
|
532
644
|
#paintGraph(series) {
|
|
533
645
|
const canvas = this.#els.graph;
|
|
534
646
|
const ctx = canvas.getContext('2d');
|
package/src/index.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export const WELL_KNOWN_TYPES: readonly [
|
|
|
14
14
|
'error',
|
|
15
15
|
'state',
|
|
16
16
|
'sys',
|
|
17
|
-
'clear'
|
|
17
|
+
'clear',
|
|
18
|
+
'telemetry'
|
|
18
19
|
];
|
|
19
20
|
|
|
20
21
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
@@ -59,6 +60,7 @@ export interface CoreState {
|
|
|
59
60
|
mode: string;
|
|
60
61
|
assistant: string | null;
|
|
61
62
|
assistants: string[];
|
|
63
|
+
adapters: AdapterInfo[];
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export interface CoreOptions {
|
|
@@ -106,6 +108,18 @@ export interface AssistantProvider {
|
|
|
106
108
|
complete(input: { question: string; snapshot: AssistantSnapshot }): unknown;
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
export interface AdapterHooks {
|
|
112
|
+
enable(): void;
|
|
113
|
+
disable(): void;
|
|
114
|
+
label?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface AdapterInfo {
|
|
118
|
+
name: string;
|
|
119
|
+
label: string;
|
|
120
|
+
enabled: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
export function formatSimTime(ms: number): string;
|
|
110
124
|
|
|
111
125
|
export class BeeLadybugCore {
|
|
@@ -155,6 +169,11 @@ export class BeeLadybugCore {
|
|
|
155
169
|
setAssistant(provider: AssistantProvider): this;
|
|
156
170
|
clearAssistant(name?: string): this;
|
|
157
171
|
ask(question?: string): Promise<string | null>;
|
|
172
|
+
registerAdapter(name: string, hooks: AdapterHooks): this;
|
|
173
|
+
unregisterAdapter(name: string): this;
|
|
174
|
+
getAdapters(): AdapterInfo[];
|
|
175
|
+
toggleAdapter(name: string): this;
|
|
176
|
+
setAdapterEnabled(name: string, enabled: boolean): this;
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
export class UIOverlay {
|
|
@@ -193,6 +212,7 @@ export interface CanvasAdapterOptions {
|
|
|
193
212
|
entities?: CanvasEntity[];
|
|
194
213
|
overlay?: boolean;
|
|
195
214
|
computeCollisions?: boolean;
|
|
215
|
+
autoPump?: boolean;
|
|
196
216
|
colorActive?: string;
|
|
197
217
|
colorColliding?: string;
|
|
198
218
|
colorInactive?: string;
|
|
@@ -209,11 +229,13 @@ export class CanvasAdapter {
|
|
|
209
229
|
entities: CanvasEntity[];
|
|
210
230
|
overlayEnabled: boolean;
|
|
211
231
|
computeCollisions: boolean;
|
|
232
|
+
autoPump: boolean;
|
|
212
233
|
frozen: boolean;
|
|
213
234
|
timeScale: number;
|
|
214
235
|
constructor(core: BeeLadybugCore, options?: CanvasAdapterOptions);
|
|
215
236
|
attach(): this;
|
|
216
237
|
detach(): this;
|
|
238
|
+
startAutoPump(): this;
|
|
217
239
|
setCanvas(canvas: HTMLCanvasElement | null): this;
|
|
218
240
|
setEntities(entities: CanvasEntity[]): this;
|
|
219
241
|
pump(ctx?: CanvasRenderingContext2D): this;
|
|
@@ -250,6 +272,44 @@ export class WebDOMAdapter {
|
|
|
250
272
|
pump(): this;
|
|
251
273
|
}
|
|
252
274
|
|
|
275
|
+
export interface AntAdapterOptions {
|
|
276
|
+
root?: ParentNode | null;
|
|
277
|
+
threshold?: number;
|
|
278
|
+
windowMs?: number;
|
|
279
|
+
source?: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const ANT_ADAPTER_DEFAULTS: Required<Omit<AntAdapterOptions, 'root'>>;
|
|
283
|
+
|
|
284
|
+
export class AntAdapter {
|
|
285
|
+
core: BeeLadybugCore | null;
|
|
286
|
+
root: ParentNode | null;
|
|
287
|
+
threshold: number;
|
|
288
|
+
windowMs: number;
|
|
289
|
+
source: string;
|
|
290
|
+
constructor(core: BeeLadybugCore, options?: AntAdapterOptions);
|
|
291
|
+
attach(): this;
|
|
292
|
+
detach(): this;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface SpiderAdapterOptions {
|
|
296
|
+
longTaskThreshold?: number;
|
|
297
|
+
resourceThreshold?: number;
|
|
298
|
+
source?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export const SPIDER_ADAPTER_DEFAULTS: Required<SpiderAdapterOptions>;
|
|
302
|
+
|
|
303
|
+
export class SpiderAdapter {
|
|
304
|
+
core: BeeLadybugCore | null;
|
|
305
|
+
longTaskThreshold: number;
|
|
306
|
+
resourceThreshold: number;
|
|
307
|
+
source: string;
|
|
308
|
+
constructor(core: BeeLadybugCore, options?: SpiderAdapterOptions);
|
|
309
|
+
attach(): this;
|
|
310
|
+
detach(): this;
|
|
311
|
+
}
|
|
312
|
+
|
|
253
313
|
export type PythonBridgeStatus = 'idle' | 'connecting' | 'open' | 'closed';
|
|
254
314
|
|
|
255
315
|
export interface PythonBridgeOptions {
|
package/src/index.js
CHANGED
|
@@ -2,4 +2,6 @@ export { BeeLadybugCore, BEE_LADYBUG_VERSION, CORE_DEFAULTS, WELL_KNOWN_TYPES, f
|
|
|
2
2
|
export { UIOverlay } from './core/UIOverlay.js';
|
|
3
3
|
export { CanvasAdapter, CANVAS_ADAPTER_DEFAULTS } from './adapters/CanvasAdapter.js';
|
|
4
4
|
export { WebDOMAdapter, WEB_DOM_ADAPTER_DEFAULTS } from './adapters/WebDOMAdapter.js';
|
|
5
|
+
export { AntAdapter, ANT_ADAPTER_DEFAULTS } from './adapters/AntAdapter.js';
|
|
6
|
+
export { SpiderAdapter, SPIDER_ADAPTER_DEFAULTS } from './adapters/SpiderAdapter.js';
|
|
5
7
|
export { PythonBridge, PYTHON_BRIDGE_DEFAULTS } from './adapters/python/PythonBridge.js';
|