@praxisjs/devtools 0.2.34 → 0.3.0

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/assets/uno.generated.css +5 -0
  3. package/dist/assets/logo.svg.js +4 -0
  4. package/dist/assets/tailwind-v4.css.js +4 -0
  5. package/dist/assets/uno.generated.css.js +4 -0
  6. package/dist/core/event-bus.js +21 -0
  7. package/dist/core/profiler.js +79 -0
  8. package/dist/core/registry.js +147 -0
  9. package/dist/decorators/debug.js +126 -0
  10. package/dist/decorators/trace.js +45 -0
  11. package/dist/icons/ellipsis-vertical.js +38 -0
  12. package/dist/icons/panel-bottom.js +28 -0
  13. package/dist/icons/panel-left.js +28 -0
  14. package/dist/icons/panel-right.js +28 -0
  15. package/dist/icons/panel-top.js +28 -0
  16. package/dist/icons/x.js +22 -0
  17. package/dist/index.d.ts +96 -3
  18. package/dist/index.js +10 -1157
  19. package/dist/plugins/components/components/component-detail.js +53 -0
  20. package/dist/plugins/components/components/component-row.js +22 -0
  21. package/dist/plugins/components/components/detail-row.js +15 -0
  22. package/dist/plugins/components/components/detail-section.js +14 -0
  23. package/dist/plugins/components/components/status-dot.js +15 -0
  24. package/dist/plugins/components/components-tab.js +54 -0
  25. package/dist/plugins/components/index.js +9 -0
  26. package/dist/plugins/performance/index.js +9 -0
  27. package/dist/plugins/performance/performance-tab.js +62 -0
  28. package/dist/plugins/signals/components/signal-detail.js +29 -0
  29. package/dist/plugins/signals/components/signal-row.js +17 -0
  30. package/dist/plugins/signals/index.js +9 -0
  31. package/dist/plugins/signals/signals-tab.js +53 -0
  32. package/dist/plugins/timeline/components/badge.js +16 -0
  33. package/dist/plugins/timeline/components/timeline-row.js +33 -0
  34. package/dist/plugins/timeline/constants.js +54 -0
  35. package/dist/plugins/timeline/index.js +9 -0
  36. package/dist/plugins/timeline/timeline-tab.js +46 -0
  37. package/dist/ui/dev-tools.js +74 -0
  38. package/dist/ui/panel.js +106 -0
  39. package/dist/ui/shared/empty-state.js +14 -0
  40. package/dist/ui/shared/icon-button.js +17 -0
  41. package/dist/ui/shared/panel-section.js +14 -0
  42. package/dist/ui/shared/search-input.js +17 -0
  43. package/dist/ui/shared/side-panel.js +15 -0
  44. package/dist/utils/format-time.js +7 -0
  45. package/dist/utils/format-value.js +14 -0
  46. package/package.json +14 -6
  47. package/src/core/profiler.ts +162 -0
  48. package/src/core/registry.ts +23 -21
  49. package/src/decorators/debug.ts +96 -32
  50. package/src/decorators/index.ts +1 -0
  51. package/src/decorators/trace.ts +27 -4
  52. package/src/index.ts +5 -0
  53. package/src/plugins/performance/index.ts +9 -0
  54. package/src/plugins/performance/performance-tab.tsx +111 -0
  55. package/src/ui/dev-tools.tsx +5 -0
  56. package/src/__tests__/debug.test.ts +0 -333
  57. package/src/__tests__/event-bus.test.ts +0 -123
  58. package/src/__tests__/format-time.test.ts +0 -52
  59. package/src/__tests__/format-value.test.ts +0 -46
  60. package/src/__tests__/plugins.test.ts +0 -101
  61. package/src/__tests__/registry.test.ts +0 -386
  62. package/src/__tests__/trace.test.ts +0 -200
  63. package/tsconfig.json +0 -21
  64. package/uno.config.ts +0 -57
  65. package/vite.config.ts +0 -38
@@ -0,0 +1,106 @@
1
+ import e from "../assets/logo.svg.js";
2
+ import { EllipsisVerticalIcon as t } from "../icons/ellipsis-vertical.js";
3
+ import { PanelBottomIcon as n } from "../icons/panel-bottom.js";
4
+ import { PanelLeftIcon as r } from "../icons/panel-left.js";
5
+ import { PanelRightIcon as i } from "../icons/panel-right.js";
6
+ import { PanelTopIcon as a } from "../icons/panel-top.js";
7
+ import { XIcon as o } from "../icons/x.js";
8
+ import { IconButton as s } from "./shared/icon-button.js";
9
+ import { For as c, _$className as l, _$event as u, _$html as d, _$insert as f, _$insertOnly as p, _$onEvent as m, _$prop as h, _$reactive as g, _$reactiveOnly as _, _$scope as v } from "@praxisjs/runtime";
10
+ import { jsx as y } from "@praxisjs/jsx/jsx-runtime";
11
+ import { StatefulComponent as b } from "@praxisjs/core";
12
+ import { Component as x, Emit as S, Prop as C, State as w } from "@praxisjs/decorators";
13
+ //#region src/ui/panel.tsx
14
+ var T = /* @__PURE__ */ u("onMouseDown"), E = /* @__PURE__ */ u("onClick"), D = /* @__PURE__ */ d("<div><div></div><div><div class=\"flex flex-col flex-1 w-full h-full overflow-hidden\"><div class=\"flex h-10 items-stretch border-b border-border bg-header shrink-0 px-2 gap-[2px]\"><div class=\"flex items-center gap-[6px] pr-3 mr-[2px] border-r border-border shrink-0\"><img class=\"h-[13px] w-[13px]\"><span class=\"text-[10px] font-bold text-accent tracking-widest uppercase\">praxisjs</span></div><div class=\"flex items-stretch flex-1\"></div><div class=\"flex items-center gap-[2px] shrink-0\"><div class=relative><!----></div><div class=\"w-px h-3 bg-border mx-1\"></div></div></div><div class=\"flex-1 overflow-hidden flex flex-col\">"), O = /* @__PURE__ */ d("<button><!---->"), k = /* @__PURE__ */ d("<span class=\"absolute bottom-0 left-0 right-0 h-[2px] bg-accent\">"), A = /* @__PURE__ */ d("<div class=\"absolute right-0 top-full mt-1 flex gap-[2px] bg-bg border border-border rounded-md shadow-lg p-1 z-10\">"), j = 180, M = 300, N = 240, P = 380, F = {
15
+ bottom: n,
16
+ top: a,
17
+ left: r,
18
+ right: i
19
+ }, I = @x() class extends b {
20
+ @w() activeTab = "";
21
+ @w() dock = "bottom";
22
+ @w() dockMenuOpen = !1;
23
+ @w() bottomSize = M;
24
+ @w() vertSize = P;
25
+ @C() plugins = [];
26
+ @C() registry;
27
+ @S("onClose") handleClose() {}
28
+ onMount() {
29
+ this.plugins.length > 0 && (this.activeTab = this.plugins[0].id);
30
+ }
31
+ handleMouseDown(e) {
32
+ e.preventDefault();
33
+ let t = e.clientX, n = e.clientY, r = this.dock, i = r === "bottom" || r === "top" ? this.bottomSize : this.vertSize, a = (e) => {
34
+ let r = this.dock;
35
+ r === "bottom" ? this.bottomSize = Math.max(j, Math.min(window.innerHeight * .85, i + (n - e.clientY))) : r === "top" ? this.bottomSize = Math.max(j, Math.min(window.innerHeight * .85, i + (e.clientY - n))) : r === "left" ? this.vertSize = Math.max(N, Math.min(window.innerWidth * .6, i + (e.clientX - t))) : this.vertSize = Math.max(N, Math.min(window.innerWidth * .6, i + (t - e.clientX)));
36
+ }, o = () => {
37
+ document.removeEventListener("mousemove", a), document.removeEventListener("mouseup", o);
38
+ };
39
+ document.addEventListener("mousemove", a), document.addEventListener("mouseup", o);
40
+ }
41
+ render() {
42
+ {
43
+ let n = v(), r = D(), i = r.firstChild, a = i.nextSibling, u = a.firstChild.firstChild, d = u.nextSibling, b = u.firstChild, x = b.nextSibling, S = x.nextSibling, C = b.firstChild, w = S.firstChild, j = w.firstChild;
44
+ return l(i, () => {
45
+ let e = this.dock;
46
+ return e === "bottom" || e === "top" ? "h-1 cursor-[ns-resize] bg-transparent hover:bg-accent transition-colors duration-200" : "w-1 cursor-[ew-resize] bg-transparent hover:bg-accent transition-colors duration-200";
47
+ }, n), m(i, T, (e) => {
48
+ this.handleMouseDown(e);
49
+ }, n), h(C, "src", e, n), p(x, /* @__PURE__ */ y(c, {
50
+ each: () => this.plugins,
51
+ by: (e) => e.id,
52
+ children: (e) => () => {
53
+ let t = e();
54
+ {
55
+ let e = v(), n = O(), r = n.firstChild;
56
+ return f(n, t.label, r, e), g(r, () => this.activeTab === t.id ? k() : null, e), l(n, () => this.activeTab === t.id ? "relative flex items-center px-3 text-[11px] font-semibold text-accent cursor-pointer" : "flex items-center px-3 text-[11px] text-muted cursor-pointer hover:text-text transition-colors duration-150", e), m(n, E, () => {
57
+ this.activeTab = t.id;
58
+ }, e), n;
59
+ }
60
+ }
61
+ }), n), f(w, /* @__PURE__ */ y(s, {
62
+ title: "Dock position",
63
+ icon: t,
64
+ active: () => this.dockMenuOpen,
65
+ onClick: () => {
66
+ this.dockMenuOpen = !this.dockMenuOpen;
67
+ }
68
+ }), j, n), g(j, () => this.dockMenuOpen ? ((e) => {
69
+ let t = A();
70
+ return p(t, [
71
+ "bottom",
72
+ "top",
73
+ "left",
74
+ "right"
75
+ ].map((e) => /* @__PURE__ */ y(s, {
76
+ title: `Dock ${e}`,
77
+ active: () => this.dock === e,
78
+ icon: F[e],
79
+ onClick: () => {
80
+ this.dock = e, this.dockMenuOpen = !1;
81
+ }
82
+ }, e)), e), t;
83
+ })(v()) : null, n), f(S, /* @__PURE__ */ y(s, {
84
+ title: "Close",
85
+ icon: o,
86
+ onClick: () => {
87
+ this.handleClose();
88
+ }
89
+ }), null, n), _(d, () => {
90
+ let e = this.plugins.find((e) => e.id === this.activeTab)?.component;
91
+ return e ? /* @__PURE__ */ y(e, { registry: this.registry }) : null;
92
+ }, n), l(a, () => {
93
+ let e = this.dock;
94
+ return e === "top" ? "w-full h-full bg-bg border-b border-border" : e === "left" ? "w-full h-full bg-bg border-r border-border" : e === "right" ? "w-full h-full bg-bg border-l border-border" : "w-full h-full bg-bg border-t border-border";
95
+ }, n), h(a, "style", () => {
96
+ let e = this.dock;
97
+ return e === "bottom" || e === "top" ? { height: `${this.bottomSize.toString()}px` } : { width: `${this.vertSize.toString()}px` };
98
+ }, n), l(r, () => {
99
+ let e = this.dock;
100
+ return e === "top" ? "flex flex-col-reverse fixed top-0 left-0 w-full z-[2147483647]" : e === "left" ? "flex flex-row-reverse fixed left-0 top-0 h-screen z-[2147483647]" : e === "right" ? "flex fixed right-0 top-0 h-screen z-[2147483647]" : "flex flex-col fixed bottom-0 left-0 w-full z-[2147483647]";
101
+ }, n), r;
102
+ }
103
+ }
104
+ };
105
+ //#endregion
106
+ export { I as Panel };
@@ -0,0 +1,14 @@
1
+ import { _$html as e, _$insertOnly as t, _$scope as n } from "@praxisjs/runtime";
2
+ import { StatelessComponent as r } from "@praxisjs/core";
3
+ import { Component as i } from "@praxisjs/decorators";
4
+ //#region src/ui/shared/empty-state.tsx
5
+ var a = /* @__PURE__ */ e("<div class=\"flex flex-col items-center justify-center gap-2 py-12\"><span class=\"text-subtle text-[20px] leading-none select-none font-mono\">—</span><p class=\"text-subtle text-[11px] text-center leading-relaxed max-w-[200px]\">"), o = @i() class extends r {
6
+ render() {
7
+ {
8
+ let e = n(), r = a(), i = r.firstChild.nextSibling;
9
+ return t(i, this.props.message, e), r;
10
+ }
11
+ }
12
+ };
13
+ //#endregion
14
+ export { o as EmptyState };
@@ -0,0 +1,17 @@
1
+ import { jsx as e } from "@praxisjs/jsx/jsx-runtime";
2
+ import { StatelessComponent as t } from "@praxisjs/core";
3
+ import { Component as n } from "@praxisjs/decorators";
4
+ //#region src/ui/shared/icon-button.tsx
5
+ var r = @n() class extends t {
6
+ render() {
7
+ let { title: t, active: n, icon: r, onClick: i } = this.props;
8
+ return /* @__PURE__ */ e("button", {
9
+ title: t,
10
+ onClick: i,
11
+ class: () => `w-[22px] h-[22px] flex items-center justify-center rounded-md text-[11px] cursor-pointer transition-colors duration-150 ${(typeof n == "function" ? n() : n) ? "text-accent bg-soft color-white" : "text-subtle hover:color-muted hover:bg-section"}`,
12
+ children: /* @__PURE__ */ e(r, {})
13
+ });
14
+ }
15
+ };
16
+ //#endregion
17
+ export { r as IconButton };
@@ -0,0 +1,14 @@
1
+ import { _$html as e, _$insert as t, _$insertOnly as n, _$scope as r } from "@praxisjs/runtime";
2
+ import { StatelessComponent as i } from "@praxisjs/core";
3
+ import { Component as a } from "@praxisjs/decorators";
4
+ //#region src/ui/shared/panel-section.tsx
5
+ var o = /* @__PURE__ */ e("<div class=\"border-b border-border\"><div class=\"px-3 py-[4px] text-[9px] text-subtle font-bold tracking-[0.12em] uppercase bg-section border-b border-border\">"), s = @a() class extends i {
6
+ render() {
7
+ {
8
+ let e = r(), i = o(), a = i.firstChild;
9
+ return n(a, this.props.label, e), t(i, this.props.children, null, e), i;
10
+ }
11
+ }
12
+ };
13
+ //#endregion
14
+ export { s as PanelSection };
@@ -0,0 +1,17 @@
1
+ import { _$event as e, _$html as t, _$onEvent as n, _$prop as r, _$scope as i } from "@praxisjs/runtime";
2
+ import { StatelessComponent as a } from "@praxisjs/core";
3
+ import { Component as o } from "@praxisjs/decorators";
4
+ //#region src/ui/shared/search-input.tsx
5
+ var s = /* @__PURE__ */ e("onInput"), c = /* @__PURE__ */ t("<input type=text class=\"w-full bg-input border border-border rounded-lg text-text text-[11px] px-3 py-[5px] outline-none box-border transition-shadow duration-150\">"), l = @o() class extends a {
6
+ render() {
7
+ let { placeholder: e, onInput: t } = this.props;
8
+ {
9
+ let a = i(), o = c();
10
+ return r(o, "placeholder", e, a), n(o, s, (e) => {
11
+ t(e.target.value);
12
+ }, a), o;
13
+ }
14
+ }
15
+ };
16
+ //#endregion
17
+ export { l as SearchInput };
@@ -0,0 +1,15 @@
1
+ import { _$html as e, _$insertOnly as t, _$prop as n, _$scope as r } from "@praxisjs/runtime";
2
+ import { StatelessComponent as i } from "@praxisjs/core";
3
+ import { Component as a } from "@praxisjs/decorators";
4
+ //#region src/ui/shared/side-panel.tsx
5
+ var o = /* @__PURE__ */ e("<div class=\"shrink-0 border-l border-border flex flex-col overflow-hidden\">"), s = @a() class extends i {
6
+ render() {
7
+ let { children: e, width: i = "280px" } = this.props;
8
+ {
9
+ let a = r(), s = o();
10
+ return t(s, e, a), n(s, "style", { width: i }, a), s;
11
+ }
12
+ }
13
+ };
14
+ //#endregion
15
+ export { s as SidePanel };
@@ -0,0 +1,7 @@
1
+ //#region src/utils/format-time.ts
2
+ function e(e, t) {
3
+ let n = t ? ` ${t}` : "", r = Date.now() - e;
4
+ return r < 1e3 ? `${r.toString()}ms${n}` : r < 6e4 ? `${(r / 1e3).toFixed(1)}s${n}` : `${Math.floor(r / 6e4).toString()}m${n}`;
5
+ }
6
+ //#endregion
7
+ export { e as time };
@@ -0,0 +1,14 @@
1
+ //#region src/utils/format-value.ts
2
+ function e(e) {
3
+ if (e === null) return "null";
4
+ if (e === void 0) return "undefined";
5
+ if (typeof e == "string") return `"${e}"`;
6
+ if (typeof e == "object") try {
7
+ return JSON.stringify(e);
8
+ } catch {
9
+ return "[object]";
10
+ }
11
+ return String(e);
12
+ }
13
+ //#endregion
14
+ export { e as formatValue };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@praxisjs/devtools",
3
- "version": "0.2.34",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
+ "sideEffects": false,
5
6
  "main": "./dist/index.js",
6
7
  "types": "./dist/index.d.ts",
7
8
  "exports": {
@@ -10,13 +11,20 @@
10
11
  "types": "./dist/index.d.ts"
11
12
  }
12
13
  },
14
+ "files": [
15
+ "dist",
16
+ "assets",
17
+ "src",
18
+ "!src/**/__tests__",
19
+ "CHANGELOG.md"
20
+ ],
13
21
  "dependencies": {
14
22
  "@unocss/reset": "^66.6.7",
15
- "@praxisjs/core": "2.2.0",
16
- "@praxisjs/decorators": "1.7.0",
17
- "@praxisjs/jsx": "0.7.7",
18
- "@praxisjs/runtime": "0.7.1",
19
- "@praxisjs/shared": "0.3.1"
23
+ "@praxisjs/core": "2.3.0",
24
+ "@praxisjs/decorators": "2.0.0",
25
+ "@praxisjs/jsx": "0.8.0",
26
+ "@praxisjs/runtime": "0.8.0",
27
+ "@praxisjs/shared": "0.3.2"
20
28
  },
21
29
  "devDependencies": {
22
30
  "@types/node": "^25.9.1",
@@ -0,0 +1,162 @@
1
+ import {
2
+ setReactiveObserver,
3
+ type ComputedNode,
4
+ type EffectNode,
5
+ type ReactiveNode,
6
+ type ReactiveObserver,
7
+ } from "@praxisjs/core/internal";
8
+
9
+ import { EventBus } from "./event-bus";
10
+
11
+ export interface OwnerStats {
12
+ /** Constructor name of the component the effects belong to, or "(no component)". */
13
+ name: string;
14
+ effectRuns: number;
15
+ effectMs: number;
16
+ /** Slowest single effect run, in ms. */
17
+ maxMs: number;
18
+ }
19
+
20
+ export interface ProfileSnapshot {
21
+ recording: boolean;
22
+ /** Milliseconds covered by the recording so far. */
23
+ duration: number;
24
+ writes: number;
25
+ effectRuns: number;
26
+ effectMs: number;
27
+ computeRuns: number;
28
+ computeMs: number;
29
+ /** Per component, busiest first. */
30
+ owners: OwnerStats[];
31
+ }
32
+
33
+ const UNOWNED = "(no component)";
34
+
35
+ /**
36
+ * Records what the reactive graph does while recording: signal writes, computed recomputations
37
+ * and effect runs, with their cost, grouped by the component that created each effect. It installs
38
+ * itself as the graph's observer only while recording, so it costs nothing otherwise.
39
+ */
40
+ export class Profiler implements ReactiveObserver {
41
+ readonly bus = new EventBus();
42
+
43
+ private recording = false;
44
+ private startedAt = 0;
45
+ private stoppedAt = 0;
46
+ private writes = 0;
47
+ private effectRuns = 0;
48
+ private effectMs = 0;
49
+ private computeRuns = 0;
50
+ private computeMs = 0;
51
+ private readonly byOwner = new Map<object | undefined, OwnerStats>();
52
+ private previous: ReactiveObserver | null = null;
53
+ private notifyScheduled = false;
54
+ private mutedDepth = 0;
55
+
56
+ get isRecording(): boolean {
57
+ return this.recording;
58
+ }
59
+
60
+ /** Runs `fn` without recording what it causes: the devtools panel updating itself. */
61
+ muted<T>(fn: () => T): T {
62
+ this.mutedDepth++;
63
+ try {
64
+ return fn();
65
+ } finally {
66
+ this.mutedDepth--;
67
+ }
68
+ }
69
+
70
+ start(): void {
71
+ if (this.recording) return;
72
+ this.reset();
73
+ this.recording = true;
74
+ this.startedAt = performance.now();
75
+ this.previous = setReactiveObserver(this);
76
+ this.changed();
77
+ }
78
+
79
+ stop(): void {
80
+ if (!this.recording) return;
81
+ this.recording = false;
82
+ this.stoppedAt = performance.now();
83
+ setReactiveObserver(this.previous);
84
+ this.previous = null;
85
+ this.changed();
86
+ }
87
+
88
+ reset(): void {
89
+ this.writes = 0;
90
+ this.effectRuns = 0;
91
+ this.effectMs = 0;
92
+ this.computeRuns = 0;
93
+ this.computeMs = 0;
94
+ this.byOwner.clear();
95
+ this.startedAt = this.stoppedAt = performance.now();
96
+ this.changed();
97
+ }
98
+
99
+ write(_node: ReactiveNode): void {
100
+ if (this.mutedDepth > 0) return;
101
+ this.writes++;
102
+ this.changed();
103
+ }
104
+
105
+ compute(_node: ComputedNode, ms: number): void {
106
+ if (this.mutedDepth > 0) return;
107
+ this.computeRuns++;
108
+ this.computeMs += ms;
109
+ this.changed();
110
+ }
111
+
112
+ effect(_node: EffectNode, ms: number, owner: object | undefined): void {
113
+ if (this.mutedDepth > 0) return;
114
+ this.effectRuns++;
115
+ this.effectMs += ms;
116
+ let stats = this.byOwner.get(owner);
117
+ if (stats === undefined) {
118
+ stats = { name: owner === undefined ? UNOWNED : owner.constructor.name, effectRuns: 0, effectMs: 0, maxMs: 0 };
119
+ this.byOwner.set(owner, stats);
120
+ }
121
+ stats.effectRuns++;
122
+ stats.effectMs += ms;
123
+ if (ms > stats.maxMs) stats.maxMs = ms;
124
+ this.changed();
125
+ }
126
+
127
+ /** Totals plus per-component stats merged by component name, busiest first. */
128
+ snapshot(): ProfileSnapshot {
129
+ const merged = new Map<string, OwnerStats>();
130
+ for (const stats of this.byOwner.values()) {
131
+ const entry = merged.get(stats.name);
132
+ if (entry === undefined) {
133
+ merged.set(stats.name, { ...stats });
134
+ } else {
135
+ entry.effectRuns += stats.effectRuns;
136
+ entry.effectMs += stats.effectMs;
137
+ entry.maxMs = Math.max(entry.maxMs, stats.maxMs);
138
+ }
139
+ }
140
+ return {
141
+ recording: this.recording,
142
+ duration: (this.recording ? performance.now() : this.stoppedAt) - this.startedAt,
143
+ writes: this.writes,
144
+ effectRuns: this.effectRuns,
145
+ effectMs: this.effectMs,
146
+ computeRuns: this.computeRuns,
147
+ computeMs: this.computeMs,
148
+ owners: [...merged.values()].sort((a, b) => b.effectMs - a.effectMs || b.effectRuns - a.effectRuns),
149
+ };
150
+ }
151
+
152
+ // Coalesced to one notification per task, so a burst of thousands of effect runs does not
153
+ // make the panel re-render thousands of times.
154
+ private changed(): void {
155
+ if (this.notifyScheduled) return;
156
+ this.notifyScheduled = true;
157
+ setTimeout(() => {
158
+ this.notifyScheduled = false;
159
+ this.bus.emit("profile:changed", null);
160
+ }, 0);
161
+ }
162
+ }
@@ -1,4 +1,5 @@
1
1
  import { EventBus } from "./event-bus";
2
+ import { Profiler } from "./profiler";
2
3
 
3
4
  import type {
4
5
  ComponentEntry,
@@ -25,9 +26,12 @@ export class Registry {
25
26
  }
26
27
 
27
28
  readonly bus = new EventBus();
29
+ readonly profiler = new Profiler();
28
30
 
29
31
  private readonly instanceIds = new WeakMap<object, string>();
30
32
  private readonly signals = new Map<string, SignalEntry>();
33
+ // Signal ids per component, so unmounting one component drops its signals without scanning all.
34
+ private readonly signalsByComponent = new Map<string, Set<string>>();
31
35
  private readonly components = new Map<string, ComponentEntry>();
32
36
  private readonly timeline: TimelineEntry[] = [];
33
37
 
@@ -58,6 +62,12 @@ export class Registry {
58
62
  };
59
63
 
60
64
  this.signals.set(id, entry);
65
+ let ids = this.signalsByComponent.get(componentId);
66
+ if (ids === undefined) {
67
+ ids = new Set();
68
+ this.signalsByComponent.set(componentId, ids);
69
+ }
70
+ ids.add(id);
61
71
  this.bus.emit("signal:registered", entry);
62
72
  }
63
73
 
@@ -73,20 +83,13 @@ export class Registry {
73
83
  const entry = this.signals.get(id);
74
84
  if (!entry) return;
75
85
 
76
- const history = [
77
- ...entry.history,
78
- { value: newValue, timestamp: Date.now() },
79
- ].slice(-MAX_HISTORY);
80
-
81
- const updated: SignalEntry = {
82
- ...entry,
83
- value: newValue,
84
- history,
85
- changedAt: Date.now(),
86
- };
86
+ const now = Date.now();
87
+ entry.history.push({ value: newValue, timestamp: now });
88
+ if (entry.history.length > MAX_HISTORY) entry.history.shift();
89
+ entry.value = newValue;
90
+ entry.changedAt = now;
87
91
 
88
- this.signals.set(id, updated);
89
- this.bus.emit("signal:changed", { entry: updated, oldValue });
92
+ this.bus.emit("signal:changed", { entry, oldValue });
90
93
  this.pushTimeline({
91
94
  type: "signal:change",
92
95
  label: `${entry.componentName}.${key}`,
@@ -158,9 +161,8 @@ export class Registry {
158
161
  entry.status = "unmounted";
159
162
  this.bus.emit("component:unmount", { ...entry });
160
163
  this.components.delete(id);
161
- for (const [sid, s] of this.signals) {
162
- if (s.componentId === id) this.signals.delete(sid);
163
- }
164
+ for (const sid of this.signalsByComponent.get(id) ?? []) this.signals.delete(sid);
165
+ this.signalsByComponent.delete(id);
164
166
  }
165
167
  }
166
168
 
@@ -198,7 +200,10 @@ export class Registry {
198
200
  }
199
201
 
200
202
  getSignalsByComponent(componentId: string): SignalEntry[] {
201
- return this.getSignals().filter((s) => s.componentId === componentId);
203
+ const out: SignalEntry[] = [];
204
+ // The index and the signal map are updated together, so every id here has an entry.
205
+ for (const id of this.signalsByComponent.get(componentId) ?? []) out.push(this.signals.get(id) as SignalEntry);
206
+ return out;
202
207
  }
203
208
 
204
209
  private pushTimeline(data: Omit<TimelineEntry, "id" | "timestamp">): void {
@@ -209,10 +214,7 @@ export class Registry {
209
214
  };
210
215
 
211
216
  this.timeline.push(entry);
212
-
213
- if (this.timeline.length > MAX_TIMELINE) {
214
- this.timeline.shift();
215
- }
217
+ if (this.timeline.length > MAX_TIMELINE) this.timeline.shift();
216
218
 
217
219
  this.bus.emit("timeline:push", entry);
218
220
  }