@praxisjs/devtools 0.2.5 → 0.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/devtools",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -12,11 +12,11 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@unocss/reset": "^66.6.2",
15
- "@praxisjs/core": "0.4.2",
16
- "@praxisjs/decorators": "0.4.3",
17
- "@praxisjs/jsx": "0.3.3",
18
- "@praxisjs/shared": "0.2.0",
19
- "@praxisjs/runtime": "0.2.4"
15
+ "@praxisjs/core": "1.1.0",
16
+ "@praxisjs/decorators": "0.6.0",
17
+ "@praxisjs/jsx": "0.3.5",
18
+ "@praxisjs/runtime": "0.2.6",
19
+ "@praxisjs/shared": "0.2.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^25.3.0",
@@ -11,19 +11,31 @@ beforeEach(() => {
11
11
 
12
12
  // ── Method decorator ──────────────────────────────────────────────────────────
13
13
 
14
+ function mockMethodCtx(name: string) {
15
+ const initializers: Array<(this: object) => void> = [];
16
+ return {
17
+ kind: "method" as const,
18
+ name,
19
+ addInitializer(fn: (this: object) => void) { initializers.push(fn); },
20
+ runInitializers(instance: object) { initializers.forEach((fn) => { fn.call(instance); }); },
21
+ } as unknown as ClassMethodDecoratorContext & { runInitializers(instance: object): void };
22
+ }
23
+
14
24
  describe("Debug — method decorator", () => {
15
25
  it("records a method:call timeline entry", () => {
16
- const ctx = { kind: "method" as const, name: "doWork" } as ClassMethodDecoratorContext;
26
+ const ctx = mockMethodCtx("doWork");
17
27
  const original = vi.fn((_x: unknown) => "result");
18
28
 
19
- const wrapped = Debug()(original as never, ctx) as (...args: unknown[]) => unknown;
29
+ Debug()(original as never, ctx);
20
30
 
21
- class Comp { constructor() {} }
31
+ class Comp { doWork(_x: unknown) { return "result"; } }
22
32
  const instance = new Comp();
33
+ ctx.runInitializers(instance);
34
+
23
35
  const registry = Registry.instance;
24
36
  registry.registerComponent(instance, "Comp");
25
37
 
26
- wrapped.call(instance, 42);
38
+ instance.doWork(42);
27
39
 
28
40
  const entry = registry.getTimeline().find((t) => t.type === "method:call");
29
41
  expect(entry).toBeDefined();
@@ -33,31 +45,33 @@ describe("Debug — method decorator", () => {
33
45
  });
34
46
 
35
47
  it("records throw result when method throws", () => {
36
- const ctx = { kind: "method" as const, name: "boom" } as ClassMethodDecoratorContext;
48
+ const ctx = mockMethodCtx("boom");
37
49
  const original = () => { throw new Error("fail"); };
38
50
 
39
- const wrapped = Debug()(original as never, ctx) as () => void;
51
+ Debug()(original as never, ctx);
40
52
 
41
- class Comp {}
53
+ class Comp { boom() { throw new Error("fail"); } }
42
54
  const instance = new Comp();
55
+ ctx.runInitializers(instance);
43
56
  Registry.instance.registerComponent(instance, "Comp");
44
57
 
45
- expect(() => wrapped.call(instance)).toThrow("fail");
58
+ expect(() => (instance as unknown as Record<string, () => void>).boom()).toThrow("fail");
46
59
 
47
60
  const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
48
61
  expect(entry?.data.result).toContain("throw");
49
62
  });
50
63
 
51
64
  it("uses custom label from options", () => {
52
- const ctx = { kind: "method" as const, name: "internal" } as ClassMethodDecoratorContext;
65
+ const ctx = mockMethodCtx("internal");
53
66
  const original = vi.fn(() => undefined);
54
67
 
55
- const wrapped = Debug({ label: "myLabel" })(original as never, ctx) as () => void;
68
+ Debug({ label: "myLabel" })(original as never, ctx);
56
69
 
57
- class Comp {}
70
+ class Comp { internal() { return undefined; } }
58
71
  const instance = new Comp();
72
+ ctx.runInitializers(instance);
59
73
  Registry.instance.registerComponent(instance, "Comp");
60
- wrapped.call(instance);
74
+ (instance as unknown as Record<string, () => void>).internal();
61
75
 
62
76
  const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
63
77
  expect(entry?.label).toContain("myLabel");
@@ -227,6 +241,62 @@ describe("Debug — field decorator with computed value", () => {
227
241
  });
228
242
  });
229
243
 
244
+ // ── Async method decorator ────────────────────────────────────────────────────
245
+
246
+ describe("Debug — async method decorator", () => {
247
+ it("timing is recorded for the synchronous wrapper (documents limitation: async duration is not measured)", () => {
248
+ // @Debug wraps methods synchronously. For async methods, the recorded duration
249
+ // covers only until the Promise is returned, not until it resolves.
250
+ const ctx = mockMethodCtx("fetchData");
251
+ const original = vi.fn(async () => {
252
+ await new Promise((r) => setTimeout(r, 10));
253
+ return "done";
254
+ });
255
+
256
+ Debug()(original as never, ctx);
257
+
258
+ class Comp { async fetchData() { return "done"; } }
259
+ const instance = new Comp();
260
+ ctx.runInitializers(instance);
261
+ Registry.instance.registerComponent(instance, "Comp");
262
+
263
+ // Call and await
264
+ const promise = (instance as unknown as Record<string, () => unknown>).fetchData() as Promise<unknown>;
265
+ expect(promise).toBeInstanceOf(Promise);
266
+
267
+ // Timeline entry is recorded synchronously (before the promise resolves)
268
+ const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
269
+ expect(entry).toBeDefined();
270
+ expect(entry?.label).toContain("fetchData");
271
+ // Duration is near-zero since it only measured until Promise was returned
272
+ expect(typeof entry?.data.duration).toBe("number");
273
+
274
+ return promise;
275
+ });
276
+ });
277
+
278
+ // ── Empty constructor name ─────────────────────────────────────────────────────
279
+
280
+ describe("Debug — class with empty constructor name", () => {
281
+ it("falls back gracefully when constructor.name is empty string", () => {
282
+ const ctx = mockMethodCtx("doWork");
283
+ const original = vi.fn(() => 42);
284
+
285
+ Debug()(original as never, ctx);
286
+
287
+ // Simulate a class whose constructor.name is empty (e.g. minified code)
288
+ const instance = Object.create({ constructor: { name: "" } }) as object;
289
+ ctx.runInitializers(instance);
290
+
291
+ expect(() => {
292
+ (instance as Record<string, () => void>).doWork();
293
+ }).not.toThrow();
294
+
295
+ const entry = Registry.instance.getTimeline().find((t) => t.type === "method:call");
296
+ expect(entry).toBeDefined();
297
+ });
298
+ });
299
+
230
300
  // ── Getter decorator ──────────────────────────────────────────────────────────
231
301
 
232
302
  describe("Debug — getter decorator", () => {
@@ -73,4 +73,51 @@ describe("EventBus", () => {
73
73
  bus2.emit("event", "data");
74
74
  expect(handler).not.toHaveBeenCalled();
75
75
  });
76
+
77
+ it("handler that throws does not prevent subsequent handlers from being called", () => {
78
+ const bus = new EventBus();
79
+ const h1 = vi.fn().mockImplementation(() => { throw new Error("h1 failed"); });
80
+ const h2 = vi.fn();
81
+ const h3 = vi.fn();
82
+ bus.on("event", h1);
83
+ bus.on("event", h2);
84
+ bus.on("event", h3);
85
+ expect(() => bus.emit("event", "payload")).toThrow();
86
+ expect(h2).toHaveBeenCalledWith("payload");
87
+ expect(h3).toHaveBeenCalledWith("payload");
88
+ });
89
+
90
+ it("handlers are called in registration order", () => {
91
+ const bus = new EventBus();
92
+ const order: number[] = [];
93
+ bus.on("event", () => { order.push(1); });
94
+ bus.on("event", () => { order.push(2); });
95
+ bus.on("event", () => { order.push(3); });
96
+ bus.emit("event", null);
97
+ expect(order).toEqual([1, 2, 3]);
98
+ });
99
+
100
+ it("error from a throwing handler is re-thrown after all handlers run", () => {
101
+ const bus = new EventBus();
102
+ const err = new Error("boom");
103
+ bus.on("event", () => { throw err; });
104
+ const h2 = vi.fn();
105
+ bus.on("event", h2);
106
+ expect(() => bus.emit("event", "x")).toThrow(err);
107
+ expect(h2).toHaveBeenCalledOnce();
108
+ });
109
+
110
+ it("throws AggregateError when multiple handlers throw", () => {
111
+ const bus = new EventBus();
112
+ bus.on("event", () => { throw new Error("first"); });
113
+ bus.on("event", () => { throw new Error("second"); });
114
+ let caught: unknown;
115
+ try {
116
+ bus.emit("event", null);
117
+ } catch (e) {
118
+ caught = e;
119
+ }
120
+ expect(caught).toBeInstanceOf(AggregateError);
121
+ expect((caught as AggregateError).errors).toHaveLength(2);
122
+ });
76
123
  });
@@ -329,3 +329,58 @@ describe("Registry timeline cap", () => {
329
329
  expect(registry.getTimeline().length).toBe(200);
330
330
  });
331
331
  });
332
+
333
+ describe("Registry — rapid signal updates", () => {
334
+ it("records all rapid updates within the same millisecond separately", () => {
335
+ const registry = Registry.instance;
336
+ const instance = {};
337
+ registry.registerSignal(instance, "count", 0, "Counter");
338
+
339
+ // Perform multiple rapid updates (may share the same Date.now() value)
340
+ registry.updateSignal(instance, "count", 1, 0);
341
+ registry.updateSignal(instance, "count", 2, 1);
342
+ registry.updateSignal(instance, "count", 3, 2);
343
+
344
+ const signal = registry.getSignals()[0];
345
+ // initial + 3 updates = 4 history entries
346
+ expect(signal.history).toHaveLength(4);
347
+ expect(signal.history.map((h) => h.value)).toEqual([0, 1, 2, 3]);
348
+ });
349
+ });
350
+
351
+ describe("Registry — re-registering same signal", () => {
352
+ it("overwrites the previous entry when the same instance+key is re-registered", () => {
353
+ const registry = Registry.instance;
354
+ const instance = {};
355
+ registry.registerSignal(instance, "x", 10, "Comp");
356
+ registry.registerSignal(instance, "x", 99, "Comp");
357
+
358
+ const signals = registry.getSignals();
359
+ expect(signals).toHaveLength(1);
360
+ expect(signals[0].value).toBe(99);
361
+ expect(signals[0].history).toHaveLength(1);
362
+ expect(signals[0].history[0].value).toBe(99);
363
+ });
364
+ });
365
+
366
+ describe("Registry timeline cap — oldest entry removed", () => {
367
+ it("removes the oldest entry when the 201st entry is pushed", () => {
368
+ const registry = Registry.instance;
369
+ const instance = {};
370
+ registry.registerComponent(instance, "Comp");
371
+ registry.registerSignal(instance, "n", 0, "Comp");
372
+
373
+ // Push exactly 200 entries
374
+ for (let i = 1; i <= 200; i++) {
375
+ registry.updateSignal(instance, "n", i, i - 1);
376
+ }
377
+ const firstId = registry.getTimeline()[0].id;
378
+
379
+ // Push one more — the oldest should be evicted
380
+ registry.updateSignal(instance, "n", 201, 200);
381
+
382
+ const timeline = registry.getTimeline();
383
+ expect(timeline).toHaveLength(200);
384
+ expect(timeline[0].id).not.toBe(firstId);
385
+ });
386
+ });
@@ -110,6 +110,57 @@ describe("Trace — onBeforeMount()", () => {
110
110
  });
111
111
  });
112
112
 
113
+ describe("Trace — class with no render() method", () => {
114
+ it("does not crash and no duration is recorded when render is absent", () => {
115
+ class NoRender {
116
+ onBeforeMount() {}
117
+ }
118
+ const Traced = applyTrace(NoRender);
119
+ const instance = new Traced() as unknown as Record<string, () => void>;
120
+
121
+ const registry = Registry.instance;
122
+ instance.onBeforeMount();
123
+
124
+ // render does not exist — calling it should not be attempted
125
+ expect(instance.render).toBeUndefined();
126
+
127
+ // No component:render timeline entries should exist
128
+ const renderEntries = registry.getTimeline().filter((e) => e.type === "component:render");
129
+ expect(renderEntries).toHaveLength(0);
130
+ });
131
+ });
132
+
133
+ describe("Trace — lifecycle hook call order", () => {
134
+ it("calls onBeforeMount → render → (async) onMount in correct order", async () => {
135
+ const order: string[] = [];
136
+
137
+ class App {
138
+ onBeforeMount() { order.push("onBeforeMount"); }
139
+ render() { order.push("render"); return "html"; }
140
+ onMount() { order.push("onMount"); }
141
+ }
142
+ const Traced = applyTrace(App);
143
+ const instance = new Traced() as {
144
+ onBeforeMount: () => void;
145
+ render: () => string;
146
+ onMount: () => void;
147
+ };
148
+
149
+ const registry = Registry.instance;
150
+
151
+ // Simulate the component lifecycle
152
+ instance.onBeforeMount();
153
+ instance.render();
154
+
155
+ // onMount is called async (via the framework's create() hook), simulate it
156
+ registry.registerComponent(instance, "App");
157
+ instance.onMount();
158
+
159
+ // The original methods (including Trace wrappers) run in call order
160
+ expect(order).toEqual(["onBeforeMount", "render", "onMount"]);
161
+ });
162
+ });
163
+
113
164
  describe("Trace — lifecycle hooks", () => {
114
165
  const hooks = ["onMount", "onUnmount", "onBeforeUpdate", "onUpdate", "onAfterUpdate"] as const;
115
166
 
@@ -12,8 +12,22 @@ export class EventBus {
12
12
  }
13
13
 
14
14
  emit(event: string, payload: unknown): void {
15
- this.handlers.get(event)?.forEach((h) => {
16
- h(payload);
17
- });
15
+ const handlers = this.handlers.get(event);
16
+ if (!handlers) return;
17
+
18
+ const errors: unknown[] = [];
19
+ for (const h of handlers) {
20
+ try {
21
+ h(payload);
22
+ } catch (err) {
23
+ errors.push(err);
24
+ }
25
+ }
26
+
27
+ if (errors.length === 1) {
28
+ throw errors[0];
29
+ } else if (errors.length > 1) {
30
+ throw new AggregateError(errors, "Multiple event handler errors");
31
+ }
18
32
  }
19
33
  }
@@ -1,4 +1,10 @@
1
1
  import { computed } from "@praxisjs/core/internal";
2
+ import {
3
+ createMethodDecorator,
4
+ createGetterObserverDecorator,
5
+ createFieldDecorator,
6
+ type FieldBinding,
7
+ } from "@praxisjs/decorators";
2
8
 
3
9
  import { Registry } from "../core/registry";
4
10
 
@@ -15,8 +21,7 @@ interface TrackedComputed {
15
21
  function isComputed(value: unknown): value is TrackedComputed {
16
22
  return (
17
23
  typeof value === "function" &&
18
- typeof (value as unknown as Record<string, unknown>).subscribe ===
19
- "function" &&
24
+ typeof (value as unknown as Record<string, unknown>).subscribe === "function" &&
20
25
  !("set" in (value as object))
21
26
  );
22
27
  }
@@ -27,29 +32,14 @@ interface ComputedSlot {
27
32
  }
28
33
 
29
34
  interface DebugDecorator {
30
- (
31
- value: (...args: unknown[]) => unknown,
32
- context: ClassMethodDecoratorContext,
33
- ): (...args: unknown[]) => unknown;
35
+ (value: (...args: unknown[]) => unknown, context: ClassMethodDecoratorContext): void;
34
36
  (value: unknown, context: ClassGetterDecoratorContext): void;
35
37
  (value: undefined, context: ClassFieldDecoratorContext): void;
36
38
  }
37
39
 
38
40
  /**
39
41
  * Tracks state, computed values, and methods in the devtools panel.
40
- *
41
- * On @State() properties (stacked):
42
- * @Debug()
43
- * @State() count = 0;
44
- *
45
- * On @Computed() getters (stacked):
46
- * @Debug({ label: "doubled" })
47
- * @Computed()
48
- * get doubled() { return this.count * 2; }
49
- *
50
- * On methods:
51
- * @Debug()
52
- * increment() { ... }
42
+ * Supports @State() fields, @Computed() getters, and methods.
53
43
  */
54
44
  export function Debug(options: DebugOptions = {}): DebugDecorator {
55
45
  const impl = function (
@@ -61,120 +51,83 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
61
51
  ) {
62
52
  const label = options.label ?? (context.name as string);
63
53
 
64
- // ── Method decorator ─────────────────────────────────────────────────
65
54
  if (context.kind === "method") {
66
55
  const original = value as (...args: unknown[]) => unknown;
67
-
68
- return function (this: object, ...args: unknown[]) {
69
- const componentName = (this.constructor as { name: string }).name;
70
- const start = performance.now();
71
- let result: unknown;
72
- let threw = false;
73
-
74
- try {
75
- result = original.apply(this, args);
76
- } catch (err) {
77
- threw = true;
78
- result = err;
79
- throw err;
80
- } finally {
81
- const duration = performance.now() - start;
82
- Registry.instance.recordMethodCall(
83
- this,
84
- label,
85
- args,
86
- threw ? `throw ${String(result)}` : result,
87
- duration,
88
- componentName,
89
- );
90
- }
91
-
92
- return result;
93
- };
56
+ createMethodDecorator({
57
+ wrap(_original, _instance, _name) {
58
+ return function (this: object, ...args: unknown[]) {
59
+ const componentName = (this.constructor as { name: string }).name;
60
+ const start = performance.now();
61
+ let result: unknown;
62
+ let threw = false;
63
+ try {
64
+ result = original.apply(this, args);
65
+ } catch (err) {
66
+ threw = true;
67
+ result = err;
68
+ throw err;
69
+ } finally {
70
+ const duration = performance.now() - start;
71
+ Registry.instance.recordMethodCall(
72
+ this, label, args,
73
+ threw ? `throw ${String(result)}` : result,
74
+ duration, componentName,
75
+ );
76
+ }
77
+ return result;
78
+ };
79
+ },
80
+ })(original, context);
81
+ return;
94
82
  }
95
83
 
96
- // ── Getter decorator (@Computed() getters) ────────────────────────────
97
84
  if (context.kind === "getter") {
98
85
  const originalGetter = value as (this: object) => unknown;
99
-
100
- context.addInitializer(function (this: unknown) {
101
- const instance = this as object & Record<string, unknown>;
102
- const componentName = (instance.constructor as { name: string }).name;
103
-
104
- // Defer until the current synchronous class initialization (including
105
- // field initializers like `count = 0`) has fully completed. Without
106
- // this, accessing `this.doubled` here could read state signals before
107
- // their field initializers have run, producing NaN or undefined.
108
- queueMicrotask(() => {
109
- const c = computed(() => originalGetter.call(instance));
110
- let skipFirst = true;
111
- let prevValue = c();
112
-
113
- Registry.instance.registerSignal(instance, label, prevValue, componentName);
114
-
115
- c.subscribe((newValue) => {
116
- if (skipFirst) {
117
- skipFirst = false;
118
- return;
119
- }
120
- Registry.instance.updateSignal(instance, label, newValue, prevValue);
121
- prevValue = newValue;
86
+ createGetterObserverDecorator({
87
+ observe(getter, instance, _name) {
88
+ const componentName = (instance.constructor as { name: string }).name;
89
+ // Defer so field initializers (e.g. @State()) have run before we read them.
90
+ queueMicrotask(() => {
91
+ const c = computed(() => getter.call(instance));
92
+ let skipFirst = true;
93
+ let prevValue = c();
94
+ Registry.instance.registerSignal(instance, label, prevValue, componentName);
95
+ c.subscribe((newValue) => {
96
+ if (skipFirst) { skipFirst = false; return; }
97
+ Registry.instance.updateSignal(instance, label, newValue, prevValue);
98
+ prevValue = newValue;
99
+ });
122
100
  });
123
- });
124
- });
125
-
101
+ },
102
+ })(originalGetter, context);
126
103
  return;
127
104
  }
128
105
 
129
- // ── Field decorator ───────────────────────────────────────────────────
130
- context.addInitializer(function (
131
- this: unknown,
132
- ) {
133
- const instance = this as object & Record<string, unknown>;
134
- const name = context.name as string;
135
- const componentName = (
136
- instance.constructor as { name: string }
137
- ).name;
138
-
139
- // ── Wrapping @State() getter/setter ─────────────────────────────
140
- // @State() initializer runs before @Debug() initializer (inner-first),
141
- // so the getter/setter is already installed on the instance here.
142
- const existingDesc = Object.getOwnPropertyDescriptor(instance, name);
106
+ createFieldDecorator({
107
+ bind(instance, name, _initialValue): FieldBinding {
108
+ const componentName = (instance.constructor as { name: string }).name;
143
109
 
110
+ // @State() runs inner-first, so its getter/setter is already on the instance.
111
+ const existingDesc = Object.getOwnPropertyDescriptor(instance, name);
144
112
  if (existingDesc?.get && existingDesc.set) {
145
113
  // eslint-disable-next-line @typescript-eslint/unbound-method
146
114
  const originalGet = existingDesc.get;
147
115
  // eslint-disable-next-line @typescript-eslint/unbound-method
148
116
  const originalSet = existingDesc.set;
149
-
150
- Registry.instance.registerSignal(
151
- instance,
152
- label,
153
- originalGet.call(instance),
154
- componentName,
155
- );
156
-
157
- Object.defineProperty(instance, name, {
158
- get(this: object) {
159
- return originalGet.call(this) as unknown;
160
- },
161
- set(this: object, newValue: unknown) {
162
- const oldValue: unknown = originalGet.call(this);
163
- originalSet.call(this, newValue);
164
- Registry.instance.updateSignal(this, label, newValue, oldValue);
117
+ Registry.instance.registerSignal(instance, label, originalGet.call(instance), componentName);
118
+ return {
119
+ descriptor: {
120
+ get(this: object) { return originalGet.call(this) as unknown; },
121
+ set(this: object, newValue: unknown) {
122
+ const oldValue: unknown = originalGet.call(this);
123
+ originalSet.call(this, newValue);
124
+ Registry.instance.updateSignal(this, label, newValue, oldValue);
125
+ },
165
126
  },
166
- enumerable: true,
167
- configurable: true,
168
- });
169
-
170
- return;
127
+ };
171
128
  }
172
129
 
173
- // ── Computed class field ─────────────────────────────────────────
174
- // The field initializer has already run, so read the assigned value.
175
- const initialValue = instance[name];
176
- Reflect.deleteProperty(instance, name);
177
-
130
+ const initialValue = _initialValue;
178
131
  if (!isComputed(initialValue)) {
179
132
  if (initialValue !== undefined) {
180
133
  console.warn(
@@ -182,54 +135,45 @@ export function Debug(options: DebugOptions = {}): DebugDecorator {
182
135
  `expected a computed() value but got ${typeof initialValue}. Skipping.`,
183
136
  );
184
137
  }
185
- instance[name] = initialValue;
186
- return;
138
+ return {};
187
139
  }
188
140
 
189
- // subscribe() calls the callback immediately (synchronously), so we
190
- // use a flag to skip the first call and register via registerSignal instead.
191
141
  let slot: ComputedSlot;
192
142
 
193
- function subscribe(computed: TrackedComputed): () => void {
143
+ // subscribe() fires the callback immediately — skip that first call
144
+ // and register the initial value via registerSignal instead.
145
+ function subscribe(comp: TrackedComputed): () => void {
194
146
  let skipFirst = true;
195
- let prevValue = computed();
196
-
197
- const unsub = computed.subscribe((newValue) => {
198
- if (skipFirst) {
199
- skipFirst = false;
200
- return;
201
- }
147
+ let prevValue = comp();
148
+ const unsub = comp.subscribe((newValue) => {
149
+ if (skipFirst) { skipFirst = false; return; }
202
150
  Registry.instance.updateSignal(instance, label, newValue, prevValue);
203
151
  prevValue = newValue;
204
152
  });
205
-
206
153
  Registry.instance.registerSignal(instance, label, prevValue, componentName);
207
154
  return unsub;
208
155
  }
209
156
 
210
157
  slot = { computed: initialValue, unsub: subscribe(initialValue) };
211
158
 
212
- Object.defineProperty(instance, name, {
213
- get() {
214
- return slot.computed;
215
- },
216
- set(newValue: unknown) {
217
- slot.unsub();
218
-
219
- if (!isComputed(newValue)) {
220
- console.warn(
221
- `[PraxisJS DevTools] @Debug() on "${componentName}.${name}": ` +
222
- `expected a computed() value but got ${typeof newValue}. Skipping.`,
223
- );
224
- return;
225
- }
226
-
227
- slot = { computed: newValue, unsub: subscribe(newValue) };
159
+ return {
160
+ descriptor: {
161
+ get() { return slot.computed; },
162
+ set(newValue: unknown) {
163
+ slot.unsub();
164
+ if (!isComputed(newValue)) {
165
+ console.warn(
166
+ `[PraxisJS DevTools] @Debug() on "${componentName}.${name}": ` +
167
+ `expected a computed() value but got ${typeof newValue}. Skipping.`,
168
+ );
169
+ return;
170
+ }
171
+ slot = { computed: newValue, unsub: subscribe(newValue) };
172
+ },
228
173
  },
229
- enumerable: true,
230
- configurable: true,
231
- });
232
- });
174
+ };
175
+ },
176
+ })(undefined, context);
233
177
  };
234
178
 
235
179
  return impl as DebugDecorator;