@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/CHANGELOG.md +28 -0
- package/assets/uno.generated.css +2 -1
- package/dist/index.d.ts +4 -25
- package/dist/index.js +969 -627
- package/package.json +6 -6
- package/src/__tests__/debug.test.ts +82 -12
- package/src/__tests__/event-bus.test.ts +47 -0
- package/src/__tests__/registry.test.ts +55 -0
- package/src/__tests__/trace.test.ts +51 -0
- package/src/core/event-bus.ts +17 -3
- package/src/decorators/debug.ts +91 -147
- package/src/decorators/trace.ts +39 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/devtools",
|
|
3
|
-
"version": "0.2.
|
|
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": "
|
|
16
|
-
"@praxisjs/decorators": "0.
|
|
17
|
-
"@praxisjs/jsx": "0.3.
|
|
18
|
-
"@praxisjs/
|
|
19
|
-
"@praxisjs/
|
|
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 =
|
|
26
|
+
const ctx = mockMethodCtx("doWork");
|
|
17
27
|
const original = vi.fn((_x: unknown) => "result");
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
Debug()(original as never, ctx);
|
|
20
30
|
|
|
21
|
-
class Comp {
|
|
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
|
-
|
|
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 =
|
|
48
|
+
const ctx = mockMethodCtx("boom");
|
|
37
49
|
const original = () => { throw new Error("fail"); };
|
|
38
50
|
|
|
39
|
-
|
|
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(() =>
|
|
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 =
|
|
65
|
+
const ctx = mockMethodCtx("internal");
|
|
53
66
|
const original = vi.fn(() => undefined);
|
|
54
67
|
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
package/src/core/event-bus.ts
CHANGED
|
@@ -12,8 +12,22 @@ export class EventBus {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
emit(event: string, payload: unknown): void {
|
|
15
|
-
this.handlers.get(event)
|
|
16
|
-
|
|
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
|
}
|
package/src/decorators/debug.ts
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
167
|
-
configurable: true,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return;
|
|
127
|
+
};
|
|
171
128
|
}
|
|
172
129
|
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
return slot.computed;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
})(undefined, context);
|
|
233
177
|
};
|
|
234
178
|
|
|
235
179
|
return impl as DebugDecorator;
|