@pyreon/reactivity 0.22.0 → 0.24.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.
- package/README.md +141 -36
- package/lib/_chunks/reactive-devtools-BCpGoGZ5.js +280 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +16 -173
- package/lib/lpih.js +177 -0
- package/lib/types/index.d.ts +116 -2
- package/lib/types/lpih.d.ts +111 -0
- package/package.json +6 -1
- package/src/computed.ts +47 -6
- package/src/effect.ts +33 -4
- package/src/index.ts +8 -0
- package/src/lpih.ts +227 -0
- package/src/reactive-devtools.ts +213 -0
- package/src/signal.ts +23 -3
- package/src/tests/lpih-source-location.test.ts +277 -0
- package/src/tests/lpih.test.ts +351 -0
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @pyreon/reactivity
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Standalone fine-grained reactivity primitives — signals, computeds, effects, stores, resources, scopes.
|
|
4
|
+
|
|
5
|
+
`@pyreon/reactivity` is the foundation layer every other Pyreon package builds on, but it has zero framework dependencies and works on its own in Node, Bun, edge workers, or any JavaScript environment without DOM or JSX. Subscribers are tracked via a `Set<() => void>`; batches use a pointer swap for zero-allocation grouping. Two-tier batch flush (computed recompute → effect run) prevents stale reads in diamond-shaped dependency graphs.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -8,70 +10,173 @@ Signal-based fine-grained reactivity primitives for the Pyreon framework.
|
|
|
8
10
|
bun add @pyreon/reactivity
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
## Quick
|
|
13
|
+
## Quick start
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
signal, computed, effect, batch, onCleanup, watch, untrack,
|
|
18
|
+
createStore, createResource, effectScope,
|
|
19
|
+
} from '@pyreon/reactivity'
|
|
15
20
|
|
|
16
21
|
const count = signal(0)
|
|
17
22
|
const doubled = computed(() => count() * 2)
|
|
18
23
|
|
|
19
|
-
effect(() => {
|
|
24
|
+
const dispose = effect(() => {
|
|
20
25
|
console.log('doubled:', doubled())
|
|
26
|
+
onCleanup(() => console.log('cleaning up'))
|
|
21
27
|
})
|
|
22
28
|
|
|
29
|
+
batch(() => {
|
|
30
|
+
count.set(1)
|
|
31
|
+
count.set(2) // subscribers fire once, with doubled = 4
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
watch(() => count(), (next, prev) => console.log(`${prev} → ${next}`))
|
|
35
|
+
|
|
36
|
+
const store = createStore({ todos: [{ text: 'Learn Pyreon', done: false }] })
|
|
37
|
+
store.todos[0].done = true // fine-grained update, no immer
|
|
38
|
+
|
|
39
|
+
dispose()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The signal contract
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const x = signal(0)
|
|
46
|
+
x() // read (subscribes if inside a tracked scope)
|
|
47
|
+
x.set(1) // write
|
|
48
|
+
x.update(n => n + 1)
|
|
49
|
+
x.peek() // read without subscribing
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Signals are **callable functions**, not `.value` getters (Vue) and not `[state, setState]` tuples (React). Calling the signal as a function is the read; `signal(5)` does NOT set the value — it reads and discards the argument. Dev mode warns; the `@pyreon/lint` rule `signal-write-as-call` flags it statically.
|
|
53
|
+
|
|
54
|
+
Optional `name` for debugging: `signal(0, { name: 'count' })` — the `@pyreon/vite-plugin` injects names automatically in dev.
|
|
55
|
+
|
|
56
|
+
## Computed
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const doubled = computed(() => count() * 2)
|
|
60
|
+
const sameRef = computed(() => obj(), { equals: (a, b) => a.id === b.id })
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Lazy, memoized, auto-tracking. Recomputes only when a dependency changes AND a subscriber actually reads it. Pass a custom `equals` to dedupe by structural identity instead of `Object.is`.
|
|
64
|
+
|
|
65
|
+
## Effects
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const dispose = effect(() => {
|
|
69
|
+
console.log(count())
|
|
70
|
+
onCleanup(() => console.log('before next run / on dispose'))
|
|
71
|
+
})
|
|
72
|
+
dispose()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`effect()` re-runs on tracked-signal change; the returned function disposes. Returning a cleanup function from the effect body is supported; `onCleanup(fn)` is the explicit form. `renderEffect()` is a lighter DOM-targeted variant that does NOT support `onCleanup` and does NOT register with `EffectScope` — used internally by `@pyreon/runtime-dom`.
|
|
76
|
+
|
|
77
|
+
`watch(source, callback)` is the explicit-source variant: `source` is evaluated for tracking, `callback(next, prev)` runs on change, and returning a cleanup function is honored.
|
|
78
|
+
|
|
79
|
+
## Batching
|
|
80
|
+
|
|
81
|
+
```ts
|
|
23
82
|
batch(() => {
|
|
24
83
|
count.set(1)
|
|
25
84
|
count.set(2)
|
|
85
|
+
}) // subscribers notified ONCE with count=2
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`batch()` defers subscriber notifications until the end of the callback. `nextTick(): Promise<void>` resolves after the current flush — useful for awaiting DOM updates in tests.
|
|
89
|
+
|
|
90
|
+
## Stores
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const store = createStore({ count: 0, todos: [{ text: 'a', done: false }] })
|
|
94
|
+
store.count++ // notifies
|
|
95
|
+
store.todos[0].done = true // deep — notifies
|
|
96
|
+
|
|
97
|
+
const shallow = shallowReactive({ user: { name: 'a' } })
|
|
98
|
+
shallow.user = { name: 'b' } // notifies
|
|
99
|
+
shallow.user.name = 'c' // does NOT notify (shallow)
|
|
100
|
+
|
|
101
|
+
const raw = markRaw(thirdPartyClassInstance) // skip proxy
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`createStore` returns a deeply-reactive proxy. `shallowReactive` proxies only the top level. `markRaw` opts an object out of proxying — useful for class instances, DOM nodes, third-party objects. `reconcile(target, source)` patches an existing store to match `source` without remounting.
|
|
105
|
+
|
|
106
|
+
**Caveat:** `Map`, `Set`, `WeakMap`, `WeakSet`, `Date`, `RegExp`, `Promise`, `Error` are returned RAW. Mutating them does not notify; assign a new instance to trigger updates.
|
|
107
|
+
|
|
108
|
+
## Resources
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const user = createResource(() => userId(), async (id) => {
|
|
112
|
+
const r = await fetch(`/api/users/${id}`)
|
|
113
|
+
return r.json()
|
|
26
114
|
})
|
|
27
|
-
|
|
115
|
+
|
|
116
|
+
user.data() // T | undefined
|
|
117
|
+
user.loading() // boolean
|
|
118
|
+
user.error() // Error | undefined
|
|
119
|
+
user.refetch()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`createResource(source, fetcher)` re-runs the fetcher whenever `source` changes; stale responses are dropped via an internal request-id guard. Resources created **outside** an `EffectScope` must be `dispose()`-d explicitly to avoid leaks.
|
|
123
|
+
|
|
124
|
+
## EffectScope
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const scope = effectScope()
|
|
128
|
+
scope.runInScope(() => {
|
|
129
|
+
effect(() => console.log(count()))
|
|
130
|
+
onScopeDispose(() => console.log('scope ended'))
|
|
131
|
+
})
|
|
132
|
+
scope.stop() // disposes every effect inside
|
|
28
133
|
```
|
|
29
134
|
|
|
30
|
-
|
|
135
|
+
Groups effects for bulk disposal — used internally by `@pyreon/runtime-dom`'s mount pipeline. `getCurrentScope()` returns the active scope; `setCurrentScope(scope)` is the escape hatch for advanced cross-tree integrations.
|
|
31
136
|
|
|
32
|
-
|
|
137
|
+
Internal arrays (`_effects`, `_updateHooks`) are lazy-allocated — scopes with no effects cost only the object itself.
|
|
33
138
|
|
|
34
|
-
|
|
35
|
-
- **`computed<T>(fn, options?): Computed<T>`** -- Derived signal that recomputes lazily when dependencies change.
|
|
36
|
-
- **`cell<T>(initial: T): Cell<T>`** -- Lightweight reactive cell.
|
|
139
|
+
## Selectors
|
|
37
140
|
|
|
38
|
-
|
|
141
|
+
```ts
|
|
142
|
+
const selected = signal<string | null>(null)
|
|
143
|
+
const isSelected = createSelector(() => selected())
|
|
39
144
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **`setErrorHandler(handler)`** -- Sets a global error handler for effect errors.
|
|
145
|
+
<For each={items} by={i => i.id}>
|
|
146
|
+
{(item) => <li class={() => isSelected(item.id) ? 'active' : ''}>{item.name}</li>}
|
|
147
|
+
</For>
|
|
148
|
+
```
|
|
45
149
|
|
|
46
|
-
|
|
150
|
+
`createSelector(source)` returns a function that, when called with a key, only notifies subscribers when the key transitions in or out of the selected state. O(1) instead of N effect runs on selection change.
|
|
47
151
|
|
|
48
|
-
|
|
49
|
-
- **`nextTick(): Promise<void>`** -- Resolves after the current batch of updates flushes.
|
|
152
|
+
## Cell — minimal alternative to signal
|
|
50
153
|
|
|
51
|
-
|
|
154
|
+
```ts
|
|
155
|
+
import { cell } from '@pyreon/reactivity'
|
|
156
|
+
const c = cell(0)
|
|
157
|
+
c.get(); c.set(1); c.subscribe(listener)
|
|
158
|
+
```
|
|
52
159
|
|
|
53
|
-
|
|
54
|
-
- **`untrack(fn)`** -- Alias for `runUntracked`.
|
|
160
|
+
`cell()` is a class-based primitive with a single-listener fast path and one allocation per cell. It is **not** callable and **does not** participate in effect tracking — use it only for cross-cutting state where the signal-tracking overhead would be wasteful.
|
|
55
161
|
|
|
56
|
-
|
|
162
|
+
## Debugging
|
|
57
163
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
164
|
+
```ts
|
|
165
|
+
import { setErrorHandler, inspectSignal, onSignalUpdate, why, getReactiveTrace } from '@pyreon/reactivity'
|
|
166
|
+
|
|
167
|
+
setErrorHandler((err, source) => reportToSentry(err, { tag: source }))
|
|
62
168
|
|
|
63
|
-
|
|
169
|
+
const count = signal(0, { name: 'count' })
|
|
170
|
+
onSignalUpdate(count, (next, prev) => console.log('count', prev, '→', next))
|
|
171
|
+
inspectSignal(count) // { name, value, subscribers: number }
|
|
172
|
+
why(count) // print dependency graph for this signal
|
|
173
|
+
```
|
|
64
174
|
|
|
65
|
-
-
|
|
66
|
-
- **`createResource(fetcher): Resource<T>`** -- Wraps an async data source in a reactive resource.
|
|
175
|
+
`activate/deactivate/getReactiveGraph/getReactiveFires` form the **opt-in** bridge consumed by the Pyreon devtools — zero cost until activated, gated by `process.env.NODE_ENV !== 'production'`, tree-shaken in production.
|
|
67
176
|
|
|
68
|
-
|
|
177
|
+
## Documentation
|
|
69
178
|
|
|
70
|
-
|
|
71
|
-
- **`isStore(value): boolean`** -- Checks whether a value is a reactive store.
|
|
72
|
-
- **`reconcile(target, source)`** -- Efficiently patches a store to match a new value.
|
|
73
|
-
- **`shallowReactive<T>(initial): T`** -- Creates a SHALLOWLY reactive store: top-level property writes notify, but nested object mutations don't (Vue 3 parity). Use for large object graphs where deep proxying would be wasteful.
|
|
74
|
-
- **`markRaw<T>(value): T`** -- Mark an object as RAW so `createStore` and `shallowReactive` return it unwrapped (Vue 3 parity). Useful for class instances, third-party objects, DOM nodes, or any shape that shouldn't be deeply proxied. Marking is one-way (no `unmarkRaw`); mark BEFORE the object enters a store.
|
|
179
|
+
Full docs: [docs.pyreon.dev/docs/reactivity](https://docs.pyreon.dev/docs/reactivity) (or `docs/docs/reactivity.md` in this repo).
|
|
75
180
|
|
|
76
181
|
## License
|
|
77
182
|
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
//#region src/reactive-devtools.ts
|
|
2
|
+
/**
|
|
3
|
+
* Time constant for the rate1s EWMA (milliseconds). Tuned for the "hot
|
|
4
|
+
* path debugging" use case: a 1-second time constant means a burst of
|
|
5
|
+
* fires shows up immediately, then decays to 1/e (~0.37×) after one
|
|
6
|
+
* second of silence, ~5% after 3 seconds, ~0.7% after 5 seconds.
|
|
7
|
+
*
|
|
8
|
+
* @internal — exported for tests + tunability.
|
|
9
|
+
*/
|
|
10
|
+
const LPIH_RATE_TAU_MS = 1e3;
|
|
11
|
+
let _active = false;
|
|
12
|
+
let _nextId = 1;
|
|
13
|
+
const _byId = /* @__PURE__ */ new Map();
|
|
14
|
+
const _subId = /* @__PURE__ */ new WeakMap();
|
|
15
|
+
/** @internal — finalizer callback; prunes the record when a node is GC'd. */
|
|
16
|
+
function _rdPrune(id) {
|
|
17
|
+
_byId.delete(id);
|
|
18
|
+
}
|
|
19
|
+
const _finalizer = new FinalizationRegistry(_rdPrune);
|
|
20
|
+
const FIRE_CAP = 512;
|
|
21
|
+
let _fireBuf = null;
|
|
22
|
+
let _fireCount = 0;
|
|
23
|
+
const PREVIEW_MAX = 60;
|
|
24
|
+
function preview(v) {
|
|
25
|
+
let s;
|
|
26
|
+
try {
|
|
27
|
+
if (v === null) return "null";
|
|
28
|
+
if (v === void 0) return "undefined";
|
|
29
|
+
const t = typeof v;
|
|
30
|
+
if (t === "string") s = JSON.stringify(v);
|
|
31
|
+
else if (t === "number" || t === "boolean" || t === "bigint") s = String(v);
|
|
32
|
+
else if (t === "function") s = `[Function ${v.name || "anonymous"}]`;
|
|
33
|
+
else if (t === "symbol") s = v.toString();
|
|
34
|
+
else if (Array.isArray(v)) s = `Array(${v.length})`;
|
|
35
|
+
else {
|
|
36
|
+
const ctor = v.constructor?.name;
|
|
37
|
+
let keys = [];
|
|
38
|
+
try {
|
|
39
|
+
keys = Object.keys(v).slice(0, 3);
|
|
40
|
+
} catch {
|
|
41
|
+
keys = [];
|
|
42
|
+
}
|
|
43
|
+
s = `${ctor && ctor !== "Object" ? `${ctor} ` : ""}{${keys.join(", ")}${keys.length === 3 ? ", …" : ""}}`;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
s = "[unstringifiable]";
|
|
47
|
+
}
|
|
48
|
+
return s.length > PREVIEW_MAX ? `${s.slice(0, PREVIEW_MAX)}…` : s;
|
|
49
|
+
}
|
|
50
|
+
/** Activate the bridge. Idempotent. Called when a devtools client attaches. */
|
|
51
|
+
function activateReactiveDevtools() {
|
|
52
|
+
_active = true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Deactivate + drop all retained state. Called when the devtools client
|
|
56
|
+
* disconnects so a closed panel leaves zero residue.
|
|
57
|
+
*/
|
|
58
|
+
function deactivateReactiveDevtools() {
|
|
59
|
+
_active = false;
|
|
60
|
+
_byId.clear();
|
|
61
|
+
_fireBuf = null;
|
|
62
|
+
_fireCount = 0;
|
|
63
|
+
}
|
|
64
|
+
function isReactiveDevtoolsActive() {
|
|
65
|
+
return _active;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse the user's call site from `new Error().stack`. Returns undefined
|
|
69
|
+
* when devtools isn't active (zero-cost early-return — no Error allocated)
|
|
70
|
+
* OR when the stack format isn't recognized.
|
|
71
|
+
*
|
|
72
|
+
* `skipFrames` is the number of caller-frames to skip past _captureCallerLocation
|
|
73
|
+
* itself. The framework's hot-path callers (signal / computedLazy / effect)
|
|
74
|
+
* pass their own depth so the captured frame is the USER's call to
|
|
75
|
+
* `signal()` / `computed()` / `effect()`, not the framework's internals.
|
|
76
|
+
*
|
|
77
|
+
* Recognized stack formats:
|
|
78
|
+
* - V8 (Chrome / Node / Bun): ` at fn (file:line:col)`
|
|
79
|
+
* - V8 (anonymous): ` at file:line:col`
|
|
80
|
+
* - JSC (Safari) + SpiderMonkey: `fn@file:line:col`
|
|
81
|
+
*
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
function _captureCallerLocation(skipFrames) {
|
|
85
|
+
if (!_active) return void 0;
|
|
86
|
+
const raw = (/* @__PURE__ */ new Error()).stack;
|
|
87
|
+
if (!raw) return void 0;
|
|
88
|
+
const lines = raw.split("\n");
|
|
89
|
+
const target = lines[(lines[0] && lines[0].trim().startsWith("Error") ? 1 : 0) + 1 + skipFrames];
|
|
90
|
+
if (!target) return void 0;
|
|
91
|
+
return parseStackLine(target);
|
|
92
|
+
}
|
|
93
|
+
function parseStackLine(line) {
|
|
94
|
+
const v8Paren = line.match(/\(([^()]+):(\d+):(\d+)\)\s*$/);
|
|
95
|
+
if (v8Paren && v8Paren[1] && v8Paren[2] && v8Paren[3]) {
|
|
96
|
+
const file = v8Paren[1];
|
|
97
|
+
const lineN = Number.parseInt(v8Paren[2], 10);
|
|
98
|
+
const col = Number.parseInt(v8Paren[3], 10);
|
|
99
|
+
if (Number.isFinite(lineN) && Number.isFinite(col)) return {
|
|
100
|
+
file,
|
|
101
|
+
line: lineN,
|
|
102
|
+
col
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const v8Bare = line.match(/at\s+([^\s()]+):(\d+):(\d+)\s*$/);
|
|
106
|
+
if (v8Bare && v8Bare[1] && v8Bare[2] && v8Bare[3]) {
|
|
107
|
+
const file = v8Bare[1];
|
|
108
|
+
const lineN = Number.parseInt(v8Bare[2], 10);
|
|
109
|
+
const col = Number.parseInt(v8Bare[3], 10);
|
|
110
|
+
if (Number.isFinite(lineN) && Number.isFinite(col)) return {
|
|
111
|
+
file,
|
|
112
|
+
line: lineN,
|
|
113
|
+
col
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const jsc = line.match(/@([^@\s]+):(\d+):(\d+)\s*$/);
|
|
117
|
+
if (jsc && jsc[1] && jsc[2] && jsc[3]) {
|
|
118
|
+
const file = jsc[1];
|
|
119
|
+
const lineN = Number.parseInt(jsc[2], 10);
|
|
120
|
+
const col = Number.parseInt(jsc[3], 10);
|
|
121
|
+
if (Number.isFinite(lineN) && Number.isFinite(col)) return {
|
|
122
|
+
file,
|
|
123
|
+
line: lineN,
|
|
124
|
+
col
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Register a signal/computed/effect node. `host` is the object carrying
|
|
130
|
+
* the `_s` subscriber Set (the signal read fn itself, or a computed's
|
|
131
|
+
* internal host). `sub` is the notify closure (`recompute`/`run`) whose
|
|
132
|
+
* identity appears in upstream `_s` Sets — used to resolve edges.
|
|
133
|
+
*
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
function _rdRegister(node, kind, host, sub, label, loc) {
|
|
137
|
+
if (!_active) return void 0;
|
|
138
|
+
const id = _nextId++;
|
|
139
|
+
_byId.set(id, {
|
|
140
|
+
id,
|
|
141
|
+
kind,
|
|
142
|
+
name: label ?? `${kind === "signal" ? "signal" : kind}#${id}`,
|
|
143
|
+
ref: new WeakRef(node),
|
|
144
|
+
hostRef: host ? new WeakRef(host) : null,
|
|
145
|
+
fires: 0,
|
|
146
|
+
lastFire: null,
|
|
147
|
+
loc,
|
|
148
|
+
rate1s: 0
|
|
149
|
+
});
|
|
150
|
+
if (sub) _subId.set(sub, id);
|
|
151
|
+
_finalizer.register(node, id);
|
|
152
|
+
Object.defineProperty(node, "__pxRdId", {
|
|
153
|
+
value: id,
|
|
154
|
+
enumerable: false,
|
|
155
|
+
configurable: true
|
|
156
|
+
});
|
|
157
|
+
return id;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Record that a node fired (signal write / computed recompute / effect
|
|
161
|
+
* run). Bumps counters + appends to the bounded fire buffer.
|
|
162
|
+
*
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
function _rdRecordFire(node) {
|
|
166
|
+
if (!_active) return;
|
|
167
|
+
const id = node.__pxRdId;
|
|
168
|
+
if (id === void 0) return;
|
|
169
|
+
const rec = _byId.get(id);
|
|
170
|
+
const ts = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
171
|
+
if (rec) {
|
|
172
|
+
rec.fires++;
|
|
173
|
+
if (rec.lastFire !== null) {
|
|
174
|
+
const dt = ts - rec.lastFire;
|
|
175
|
+
const decay = Math.exp(-dt / LPIH_RATE_TAU_MS);
|
|
176
|
+
rec.rate1s = rec.rate1s * decay + 1;
|
|
177
|
+
} else rec.rate1s = 1;
|
|
178
|
+
rec.lastFire = ts;
|
|
179
|
+
}
|
|
180
|
+
if (_fireBuf === null) _fireBuf = new Array(FIRE_CAP);
|
|
181
|
+
_fireBuf[_fireCount % FIRE_CAP] = {
|
|
182
|
+
id,
|
|
183
|
+
ts
|
|
184
|
+
};
|
|
185
|
+
_fireCount++;
|
|
186
|
+
}
|
|
187
|
+
function resolveSubId(sub) {
|
|
188
|
+
const direct = sub.__pxRdId;
|
|
189
|
+
if (direct !== void 0) return direct;
|
|
190
|
+
return _subId.get(sub);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Fresh snapshot of the live reactive graph. Edges are recomputed from
|
|
194
|
+
* each live node's current subscriber Set — always consistent with the
|
|
195
|
+
* framework's real subscription state, no incremental drift.
|
|
196
|
+
*/
|
|
197
|
+
function getReactiveGraph() {
|
|
198
|
+
const nodes = [];
|
|
199
|
+
const edges = [];
|
|
200
|
+
for (const rec of _byId.values()) {
|
|
201
|
+
const node = rec.ref.deref();
|
|
202
|
+
if (!node) continue;
|
|
203
|
+
const subs = (rec.hostRef?.deref() ?? null)?._s ?? null;
|
|
204
|
+
const valueStr = rec.kind === "effect" ? "" : preview(node._v);
|
|
205
|
+
nodes.push({
|
|
206
|
+
id: rec.id,
|
|
207
|
+
kind: rec.kind,
|
|
208
|
+
name: rec.name,
|
|
209
|
+
value: valueStr,
|
|
210
|
+
subscribers: subs?.size ?? 0,
|
|
211
|
+
fires: rec.fires,
|
|
212
|
+
lastFire: rec.lastFire,
|
|
213
|
+
...rec.loc ? { loc: rec.loc } : {}
|
|
214
|
+
});
|
|
215
|
+
if (subs) for (const cb of subs) {
|
|
216
|
+
const to = resolveSubId(cb);
|
|
217
|
+
if (to !== void 0) edges.push({
|
|
218
|
+
from: rec.id,
|
|
219
|
+
to
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
nodes,
|
|
225
|
+
edges
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Aggregate fire counts by source-location — powers Live Program Inlay
|
|
230
|
+
* Hints. Walks the live node registry, keys each node by its captured
|
|
231
|
+
* `loc`, and returns one summary per unique `file:line:col`. Nodes
|
|
232
|
+
* without a captured location are skipped (their fires are still
|
|
233
|
+
* visible via `getReactiveGraph()` and `getReactiveFires()` for the
|
|
234
|
+
* existing graph / timeline surfaces).
|
|
235
|
+
*
|
|
236
|
+
* Returns a fresh array, JSON-serializable, safe to ship across the
|
|
237
|
+
* devtools-host bridge or to write into an LSP cache file.
|
|
238
|
+
*/
|
|
239
|
+
function getFireSummaries() {
|
|
240
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
241
|
+
const nowTs = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
242
|
+
for (const rec of _byId.values()) {
|
|
243
|
+
if (!rec.loc) continue;
|
|
244
|
+
if (!rec.ref.deref()) continue;
|
|
245
|
+
const k = `${rec.loc.file}:${rec.loc.line}:${rec.loc.col}`;
|
|
246
|
+
const decayedRate = rec.lastFire !== null ? rec.rate1s * Math.exp(-(nowTs - rec.lastFire) / LPIH_RATE_TAU_MS) : 0;
|
|
247
|
+
const existing = byKey.get(k);
|
|
248
|
+
if (existing) {
|
|
249
|
+
existing.count += rec.fires;
|
|
250
|
+
existing.rate1s += decayedRate;
|
|
251
|
+
if (rec.lastFire !== null && (existing.lastFire === null || rec.lastFire > existing.lastFire)) {
|
|
252
|
+
existing.lastFire = rec.lastFire;
|
|
253
|
+
existing.kind = rec.kind;
|
|
254
|
+
}
|
|
255
|
+
} else byKey.set(k, {
|
|
256
|
+
loc: rec.loc,
|
|
257
|
+
count: rec.fires,
|
|
258
|
+
lastFire: rec.lastFire,
|
|
259
|
+
kind: rec.kind,
|
|
260
|
+
rate1s: decayedRate
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return [...byKey.values()];
|
|
264
|
+
}
|
|
265
|
+
/** Bounded recent-fire timeline (oldest → newest). Fresh copy. */
|
|
266
|
+
function getReactiveFires() {
|
|
267
|
+
if (_fireBuf === null || _fireCount === 0) return [];
|
|
268
|
+
if (_fireCount <= FIRE_CAP) return _fireBuf.slice(0, _fireCount);
|
|
269
|
+
const start = _fireCount % FIRE_CAP;
|
|
270
|
+
const out = [];
|
|
271
|
+
for (let i = 0; i < FIRE_CAP; i++) {
|
|
272
|
+
const e = _fireBuf[(start + i) % FIRE_CAP];
|
|
273
|
+
if (e) out.push(e);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
export { deactivateReactiveDevtools as a, getReactiveGraph as c, activateReactiveDevtools as i, isReactiveDevtoolsActive as l, _rdRecordFire as n, getFireSummaries as o, _rdRegister as r, getReactiveFires as s, _captureCallerLocation as t };
|
|
280
|
+
//# sourceMappingURL=reactive-devtools-BCpGoGZ5.js.map
|
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"8f0a5281-1","name":"batch.ts"},{"uid":"8f0a5281-3","name":"cell.ts"},{"uid":"8f0a5281-5","name":"scope.ts"},{"uid":"8f0a5281-7","name":"tracking.ts"},{"uid":"8f0a5281-9","name":"effect.ts"},{"uid":"8f0a5281-11","name":"computed.ts"},{"uid":"8f0a5281-13","name":"createSelector.ts"},{"uid":"8f0a5281-15","name":"debug.ts"},{"uid":"8f0a5281-17","name":"reactive-trace.ts"},{"uid":"8f0a5281-19","name":"signal.ts"},{"uid":"8f0a5281-21","name":"store.ts"},{"uid":"8f0a5281-23","name":"reconcile.ts"},{"uid":"8f0a5281-25","name":"resource.ts"},{"uid":"8f0a5281-27","name":"watch.ts"},{"uid":"8f0a5281-29","name":"index.ts"}]}]},{"name":"lpih.js","children":[{"name":"src/lpih.ts","uid":"8f0a5281-31"}]},{"name":"_chunks/reactive-devtools-BCpGoGZ5.js","children":[{"name":"src/reactive-devtools.ts","uid":"8f0a5281-33"}]}],"isRoot":true},"nodeParts":{"8f0a5281-1":{"renderedLength":3016,"gzipLength":1167,"brotliLength":0,"metaUid":"8f0a5281-0"},"8f0a5281-3":{"renderedLength":1636,"gzipLength":786,"brotliLength":0,"metaUid":"8f0a5281-2"},"8f0a5281-5":{"renderedLength":3026,"gzipLength":1226,"brotliLength":0,"metaUid":"8f0a5281-4"},"8f0a5281-7":{"renderedLength":2227,"gzipLength":858,"brotliLength":0,"metaUid":"8f0a5281-6"},"8f0a5281-9":{"renderedLength":7697,"gzipLength":2469,"brotliLength":0,"metaUid":"8f0a5281-8"},"8f0a5281-11":{"renderedLength":5143,"gzipLength":1570,"brotliLength":0,"metaUid":"8f0a5281-10"},"8f0a5281-13":{"renderedLength":2244,"gzipLength":981,"brotliLength":0,"metaUid":"8f0a5281-12"},"8f0a5281-15":{"renderedLength":2469,"gzipLength":1092,"brotliLength":0,"metaUid":"8f0a5281-14"},"8f0a5281-17":{"renderedLength":2721,"gzipLength":1363,"brotliLength":0,"metaUid":"8f0a5281-16"},"8f0a5281-19":{"renderedLength":3643,"gzipLength":1554,"brotliLength":0,"metaUid":"8f0a5281-18"},"8f0a5281-21":{"renderedLength":5232,"gzipLength":1867,"brotliLength":0,"metaUid":"8f0a5281-20"},"8f0a5281-23":{"renderedLength":2278,"gzipLength":940,"brotliLength":0,"metaUid":"8f0a5281-22"},"8f0a5281-25":{"renderedLength":1205,"gzipLength":524,"brotliLength":0,"metaUid":"8f0a5281-24"},"8f0a5281-27":{"renderedLength":1249,"gzipLength":582,"brotliLength":0,"metaUid":"8f0a5281-26"},"8f0a5281-29":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"8f0a5281-28"},"8f0a5281-31":{"renderedLength":6380,"gzipLength":2579,"brotliLength":0,"metaUid":"8f0a5281-30"},"8f0a5281-33":{"renderedLength":8588,"gzipLength":3409,"brotliLength":0,"metaUid":"8f0a5281-32"}},"nodeMetas":{"8f0a5281-0":{"id":"/src/batch.ts","moduleParts":{"index.js":"8f0a5281-1"},"imported":[],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-10"},{"uid":"8f0a5281-18"},{"uid":"8f0a5281-6"}]},"8f0a5281-2":{"id":"/src/cell.ts","moduleParts":{"index.js":"8f0a5281-3"},"imported":[],"importedBy":[{"uid":"8f0a5281-28"}]},"8f0a5281-4":{"id":"/src/scope.ts","moduleParts":{"index.js":"8f0a5281-5"},"imported":[],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-10"},{"uid":"8f0a5281-8"}]},"8f0a5281-6":{"id":"/src/tracking.ts","moduleParts":{"index.js":"8f0a5281-7"},"imported":[{"uid":"8f0a5281-0"}],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-10"},{"uid":"8f0a5281-12"},{"uid":"8f0a5281-8"},{"uid":"8f0a5281-24"},{"uid":"8f0a5281-18"}]},"8f0a5281-8":{"id":"/src/effect.ts","moduleParts":{"index.js":"8f0a5281-9"},"imported":[{"uid":"8f0a5281-32"},{"uid":"8f0a5281-4"},{"uid":"8f0a5281-6"}],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-10"},{"uid":"8f0a5281-12"},{"uid":"8f0a5281-24"},{"uid":"8f0a5281-26"}]},"8f0a5281-10":{"id":"/src/computed.ts","moduleParts":{"index.js":"8f0a5281-11"},"imported":[{"uid":"8f0a5281-0"},{"uid":"8f0a5281-8"},{"uid":"8f0a5281-32"},{"uid":"8f0a5281-4"},{"uid":"8f0a5281-6"}],"importedBy":[{"uid":"8f0a5281-28"}]},"8f0a5281-12":{"id":"/src/createSelector.ts","moduleParts":{"index.js":"8f0a5281-13"},"imported":[{"uid":"8f0a5281-8"},{"uid":"8f0a5281-6"}],"importedBy":[{"uid":"8f0a5281-28"}]},"8f0a5281-14":{"id":"/src/debug.ts","moduleParts":{"index.js":"8f0a5281-15"},"imported":[],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-18"}]},"8f0a5281-16":{"id":"/src/reactive-trace.ts","moduleParts":{"index.js":"8f0a5281-17"},"imported":[],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-18"}]},"8f0a5281-18":{"id":"/src/signal.ts","moduleParts":{"index.js":"8f0a5281-19"},"imported":[{"uid":"8f0a5281-0"},{"uid":"8f0a5281-14"},{"uid":"8f0a5281-32"},{"uid":"8f0a5281-16"},{"uid":"8f0a5281-6"}],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-24"},{"uid":"8f0a5281-20"}]},"8f0a5281-20":{"id":"/src/store.ts","moduleParts":{"index.js":"8f0a5281-21"},"imported":[{"uid":"8f0a5281-18"}],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-22"}]},"8f0a5281-22":{"id":"/src/reconcile.ts","moduleParts":{"index.js":"8f0a5281-23"},"imported":[{"uid":"8f0a5281-20"}],"importedBy":[{"uid":"8f0a5281-28"}]},"8f0a5281-24":{"id":"/src/resource.ts","moduleParts":{"index.js":"8f0a5281-25"},"imported":[{"uid":"8f0a5281-8"},{"uid":"8f0a5281-18"},{"uid":"8f0a5281-6"}],"importedBy":[{"uid":"8f0a5281-28"}]},"8f0a5281-26":{"id":"/src/watch.ts","moduleParts":{"index.js":"8f0a5281-27"},"imported":[{"uid":"8f0a5281-8"}],"importedBy":[{"uid":"8f0a5281-28"}]},"8f0a5281-28":{"id":"/src/index.ts","moduleParts":{"index.js":"8f0a5281-29"},"imported":[{"uid":"8f0a5281-0"},{"uid":"8f0a5281-2"},{"uid":"8f0a5281-10"},{"uid":"8f0a5281-12"},{"uid":"8f0a5281-14"},{"uid":"8f0a5281-32"},{"uid":"8f0a5281-16"},{"uid":"8f0a5281-8"},{"uid":"8f0a5281-22"},{"uid":"8f0a5281-24"},{"uid":"8f0a5281-4"},{"uid":"8f0a5281-18"},{"uid":"8f0a5281-20"},{"uid":"8f0a5281-6"},{"uid":"8f0a5281-26"}],"importedBy":[],"isEntry":true},"8f0a5281-30":{"id":"/src/lpih.ts","moduleParts":{"lpih.js":"8f0a5281-31"},"imported":[{"uid":"8f0a5281-32"},{"uid":"8f0a5281-34","dynamic":true}],"importedBy":[],"isEntry":true},"8f0a5281-32":{"id":"/src/reactive-devtools.ts","moduleParts":{"_chunks/reactive-devtools-BCpGoGZ5.js":"8f0a5281-33"},"imported":[],"importedBy":[{"uid":"8f0a5281-28"},{"uid":"8f0a5281-10"},{"uid":"8f0a5281-8"},{"uid":"8f0a5281-18"},{"uid":"8f0a5281-30"}]},"8f0a5281-34":{"id":"node:fs/promises","moduleParts":{},"imported":[],"importedBy":[{"uid":"8f0a5281-30"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|