@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/lib/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { a as deactivateReactiveDevtools, c as getReactiveGraph, i as activateReactiveDevtools, l as isReactiveDevtoolsActive, n as _rdRecordFire, o as getFireSummaries, r as _rdRegister, s as getReactiveFires, t as _captureCallerLocation } from "./_chunks/reactive-devtools-BCpGoGZ5.js";
|
|
2
|
+
|
|
1
3
|
//#region src/batch.ts
|
|
2
4
|
const __DEV__ = process.env.NODE_ENV !== "production";
|
|
3
5
|
let batchDepth = 0;
|
|
@@ -149,169 +151,6 @@ function cell(value) {
|
|
|
149
151
|
return new Cell(value);
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
//#endregion
|
|
153
|
-
//#region src/reactive-devtools.ts
|
|
154
|
-
let _active = false;
|
|
155
|
-
let _nextId = 1;
|
|
156
|
-
const _byId = /* @__PURE__ */ new Map();
|
|
157
|
-
const _subId = /* @__PURE__ */ new WeakMap();
|
|
158
|
-
/** @internal — finalizer callback; prunes the record when a node is GC'd. */
|
|
159
|
-
function _rdPrune(id) {
|
|
160
|
-
_byId.delete(id);
|
|
161
|
-
}
|
|
162
|
-
const _finalizer = new FinalizationRegistry(_rdPrune);
|
|
163
|
-
const FIRE_CAP = 512;
|
|
164
|
-
let _fireBuf = null;
|
|
165
|
-
let _fireCount = 0;
|
|
166
|
-
const PREVIEW_MAX$1 = 60;
|
|
167
|
-
function preview$1(v) {
|
|
168
|
-
let s;
|
|
169
|
-
try {
|
|
170
|
-
if (v === null) return "null";
|
|
171
|
-
if (v === void 0) return "undefined";
|
|
172
|
-
const t = typeof v;
|
|
173
|
-
if (t === "string") s = JSON.stringify(v);
|
|
174
|
-
else if (t === "number" || t === "boolean" || t === "bigint") s = String(v);
|
|
175
|
-
else if (t === "function") s = `[Function ${v.name || "anonymous"}]`;
|
|
176
|
-
else if (t === "symbol") s = v.toString();
|
|
177
|
-
else if (Array.isArray(v)) s = `Array(${v.length})`;
|
|
178
|
-
else {
|
|
179
|
-
const ctor = v.constructor?.name;
|
|
180
|
-
let keys = [];
|
|
181
|
-
try {
|
|
182
|
-
keys = Object.keys(v).slice(0, 3);
|
|
183
|
-
} catch {
|
|
184
|
-
keys = [];
|
|
185
|
-
}
|
|
186
|
-
s = `${ctor && ctor !== "Object" ? `${ctor} ` : ""}{${keys.join(", ")}${keys.length === 3 ? ", …" : ""}}`;
|
|
187
|
-
}
|
|
188
|
-
} catch {
|
|
189
|
-
s = "[unstringifiable]";
|
|
190
|
-
}
|
|
191
|
-
return s.length > PREVIEW_MAX$1 ? `${s.slice(0, PREVIEW_MAX$1)}…` : s;
|
|
192
|
-
}
|
|
193
|
-
/** Activate the bridge. Idempotent. Called when a devtools client attaches. */
|
|
194
|
-
function activateReactiveDevtools() {
|
|
195
|
-
_active = true;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Deactivate + drop all retained state. Called when the devtools client
|
|
199
|
-
* disconnects so a closed panel leaves zero residue.
|
|
200
|
-
*/
|
|
201
|
-
function deactivateReactiveDevtools() {
|
|
202
|
-
_active = false;
|
|
203
|
-
_byId.clear();
|
|
204
|
-
_fireBuf = null;
|
|
205
|
-
_fireCount = 0;
|
|
206
|
-
}
|
|
207
|
-
function isReactiveDevtoolsActive() {
|
|
208
|
-
return _active;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Register a signal/computed/effect node. `host` is the object carrying
|
|
212
|
-
* the `_s` subscriber Set (the signal read fn itself, or a computed's
|
|
213
|
-
* internal host). `sub` is the notify closure (`recompute`/`run`) whose
|
|
214
|
-
* identity appears in upstream `_s` Sets — used to resolve edges.
|
|
215
|
-
*
|
|
216
|
-
* @internal
|
|
217
|
-
*/
|
|
218
|
-
function _rdRegister(node, kind, host, sub, label) {
|
|
219
|
-
if (!_active) return void 0;
|
|
220
|
-
const id = _nextId++;
|
|
221
|
-
_byId.set(id, {
|
|
222
|
-
id,
|
|
223
|
-
kind,
|
|
224
|
-
name: label ?? `${kind === "signal" ? "signal" : kind}#${id}`,
|
|
225
|
-
ref: new WeakRef(node),
|
|
226
|
-
hostRef: host ? new WeakRef(host) : null,
|
|
227
|
-
fires: 0,
|
|
228
|
-
lastFire: null
|
|
229
|
-
});
|
|
230
|
-
if (sub) _subId.set(sub, id);
|
|
231
|
-
_finalizer.register(node, id);
|
|
232
|
-
Object.defineProperty(node, "__pxRdId", {
|
|
233
|
-
value: id,
|
|
234
|
-
enumerable: false,
|
|
235
|
-
configurable: true
|
|
236
|
-
});
|
|
237
|
-
return id;
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Record that a node fired (signal write / computed recompute / effect
|
|
241
|
-
* run). Bumps counters + appends to the bounded fire buffer.
|
|
242
|
-
*
|
|
243
|
-
* @internal
|
|
244
|
-
*/
|
|
245
|
-
function _rdRecordFire(node) {
|
|
246
|
-
if (!_active) return;
|
|
247
|
-
const id = node.__pxRdId;
|
|
248
|
-
if (id === void 0) return;
|
|
249
|
-
const rec = _byId.get(id);
|
|
250
|
-
const ts = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
251
|
-
if (rec) {
|
|
252
|
-
rec.fires++;
|
|
253
|
-
rec.lastFire = ts;
|
|
254
|
-
}
|
|
255
|
-
if (_fireBuf === null) _fireBuf = new Array(FIRE_CAP);
|
|
256
|
-
_fireBuf[_fireCount % FIRE_CAP] = {
|
|
257
|
-
id,
|
|
258
|
-
ts
|
|
259
|
-
};
|
|
260
|
-
_fireCount++;
|
|
261
|
-
}
|
|
262
|
-
function resolveSubId(sub) {
|
|
263
|
-
const direct = sub.__pxRdId;
|
|
264
|
-
if (direct !== void 0) return direct;
|
|
265
|
-
return _subId.get(sub);
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Fresh snapshot of the live reactive graph. Edges are recomputed from
|
|
269
|
-
* each live node's current subscriber Set — always consistent with the
|
|
270
|
-
* framework's real subscription state, no incremental drift.
|
|
271
|
-
*/
|
|
272
|
-
function getReactiveGraph() {
|
|
273
|
-
const nodes = [];
|
|
274
|
-
const edges = [];
|
|
275
|
-
for (const rec of _byId.values()) {
|
|
276
|
-
const node = rec.ref.deref();
|
|
277
|
-
if (!node) continue;
|
|
278
|
-
const subs = (rec.hostRef?.deref() ?? null)?._s ?? null;
|
|
279
|
-
const valueStr = rec.kind === "effect" ? "" : preview$1(node._v);
|
|
280
|
-
nodes.push({
|
|
281
|
-
id: rec.id,
|
|
282
|
-
kind: rec.kind,
|
|
283
|
-
name: rec.name,
|
|
284
|
-
value: valueStr,
|
|
285
|
-
subscribers: subs?.size ?? 0,
|
|
286
|
-
fires: rec.fires,
|
|
287
|
-
lastFire: rec.lastFire
|
|
288
|
-
});
|
|
289
|
-
if (subs) for (const cb of subs) {
|
|
290
|
-
const to = resolveSubId(cb);
|
|
291
|
-
if (to !== void 0) edges.push({
|
|
292
|
-
from: rec.id,
|
|
293
|
-
to
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return {
|
|
298
|
-
nodes,
|
|
299
|
-
edges
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
/** Bounded recent-fire timeline (oldest → newest). Fresh copy. */
|
|
303
|
-
function getReactiveFires() {
|
|
304
|
-
if (_fireBuf === null || _fireCount === 0) return [];
|
|
305
|
-
if (_fireCount <= FIRE_CAP) return _fireBuf.slice(0, _fireCount);
|
|
306
|
-
const start = _fireCount % FIRE_CAP;
|
|
307
|
-
const out = [];
|
|
308
|
-
for (let i = 0; i < FIRE_CAP; i++) {
|
|
309
|
-
const e = _fireBuf[(start + i) % FIRE_CAP];
|
|
310
|
-
if (e) out.push(e);
|
|
311
|
-
}
|
|
312
|
-
return out;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
154
|
//#endregion
|
|
316
155
|
//#region src/scope.ts
|
|
317
156
|
var EffectScope = class {
|
|
@@ -560,7 +399,7 @@ function cleanupLocalDeps$1(deps, fn) {
|
|
|
560
399
|
deps.length = 0;
|
|
561
400
|
}
|
|
562
401
|
}
|
|
563
|
-
function effect(fn) {
|
|
402
|
+
function effect(fn, options) {
|
|
564
403
|
if (process.env.NODE_ENV !== "production") {
|
|
565
404
|
if (fn.constructor && fn.constructor.name === "AsyncFunction") console.warn("[pyreon] effect() received an async function. Signal reads after the first `await` are NOT tracked — only the synchronous prefix is. Read every tracked signal BEFORE any await, or split into separate effects, or use `watch(source, asyncCb)` for async-in-callback patterns.");
|
|
566
405
|
}
|
|
@@ -628,7 +467,7 @@ function effect(fn) {
|
|
|
628
467
|
if (!isFirstRun) scope?.notifyEffectRan();
|
|
629
468
|
isFirstRun = false;
|
|
630
469
|
};
|
|
631
|
-
if (process.env.NODE_ENV !== "production") _rdRegister(run, "effect", null, run, void 0);
|
|
470
|
+
if (process.env.NODE_ENV !== "production") _rdRegister(run, "effect", null, run, void 0, options?.__sourceLocation ?? _captureCallerLocation(1));
|
|
632
471
|
run();
|
|
633
472
|
const e = { dispose() {
|
|
634
473
|
runCleanup();
|
|
@@ -727,7 +566,7 @@ function renderEffect(fn) {
|
|
|
727
566
|
}
|
|
728
567
|
} else renderEffectFullTrack(deps, run, trackedFn);
|
|
729
568
|
};
|
|
730
|
-
if (process.env.NODE_ENV !== "production") _rdRegister(run, "effect", null, run, void 0);
|
|
569
|
+
if (process.env.NODE_ENV !== "production") _rdRegister(run, "effect", null, run, void 0, _captureCallerLocation(1));
|
|
731
570
|
run();
|
|
732
571
|
const dispose = () => {
|
|
733
572
|
if (disposed) return;
|
|
@@ -759,7 +598,8 @@ function computed(fn, options) {
|
|
|
759
598
|
if (process.env.NODE_ENV !== "production") {
|
|
760
599
|
if (fn.constructor && fn.constructor.name === "AsyncFunction") console.warn("[pyreon] computed() received an async function. The result type becomes `Computed<Promise<T>>`, and signal reads after the first `await` are NOT tracked. Use `createResource` for async-derived state, or compute synchronously over a signal that holds the awaited value.");
|
|
761
600
|
}
|
|
762
|
-
|
|
601
|
+
const loc = options?.__sourceLocation;
|
|
602
|
+
return options?.equals ? computedWithEquals(fn, options.equals, loc) : computedLazy(fn, loc);
|
|
763
603
|
}
|
|
764
604
|
/**
|
|
765
605
|
* Default computed — lazy evaluation with deferred cleanup.
|
|
@@ -771,7 +611,7 @@ function computed(fn, options) {
|
|
|
771
611
|
* in diamond patterns (a→b,c→d: b notifies d, c tries to notify d again —
|
|
772
612
|
* skipped because d is already dirty).
|
|
773
613
|
*/
|
|
774
|
-
function computedLazy(fn) {
|
|
614
|
+
function computedLazy(fn, injectedLoc) {
|
|
775
615
|
let value;
|
|
776
616
|
let dirty = true;
|
|
777
617
|
let disposed = false;
|
|
@@ -832,7 +672,7 @@ function computedLazy(fn) {
|
|
|
832
672
|
set.delete(updater);
|
|
833
673
|
};
|
|
834
674
|
};
|
|
835
|
-
if (process.env.NODE_ENV !== "production") _rdRegister(read, "derived", host, recompute, void 0);
|
|
675
|
+
if (process.env.NODE_ENV !== "production") _rdRegister(read, "derived", host, recompute, void 0, injectedLoc ?? _captureCallerLocation(2));
|
|
836
676
|
getCurrentScope()?.add({ dispose: read.dispose });
|
|
837
677
|
return read;
|
|
838
678
|
}
|
|
@@ -842,7 +682,7 @@ function computedLazy(fn) {
|
|
|
842
682
|
* Re-evaluates immediately when deps change and only notifies downstream
|
|
843
683
|
* if `equals(prev, next)` returns false.
|
|
844
684
|
*/
|
|
845
|
-
function computedWithEquals(fn, equals) {
|
|
685
|
+
function computedWithEquals(fn, equals, injectedLoc) {
|
|
846
686
|
let value;
|
|
847
687
|
let dirty = true;
|
|
848
688
|
let initialized = false;
|
|
@@ -910,7 +750,7 @@ function computedWithEquals(fn, equals) {
|
|
|
910
750
|
set.delete(updater);
|
|
911
751
|
};
|
|
912
752
|
};
|
|
913
|
-
if (process.env.NODE_ENV !== "production") _rdRegister(read, "derived", host, recompute, void 0);
|
|
753
|
+
if (process.env.NODE_ENV !== "production") _rdRegister(read, "derived", host, recompute, void 0, injectedLoc ?? _captureCallerLocation(2));
|
|
914
754
|
getCurrentScope()?.add({ dispose: read.dispose });
|
|
915
755
|
return read;
|
|
916
756
|
}
|
|
@@ -1275,7 +1115,10 @@ function signal(initialValue, options) {
|
|
|
1275
1115
|
read.direct = _directFn;
|
|
1276
1116
|
read.debug = _debug;
|
|
1277
1117
|
read.label = options?.name;
|
|
1278
|
-
if (process.env.NODE_ENV !== "production")
|
|
1118
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1119
|
+
const loc = options?.__sourceLocation ? options.__sourceLocation : _captureCallerLocation(1);
|
|
1120
|
+
_rdRegister(read, "signal", read, null, read.label, loc);
|
|
1121
|
+
}
|
|
1279
1122
|
return read;
|
|
1280
1123
|
}
|
|
1281
1124
|
|
|
@@ -1596,5 +1439,5 @@ function watch(source, callback, opts = {}) {
|
|
|
1596
1439
|
}
|
|
1597
1440
|
|
|
1598
1441
|
//#endregion
|
|
1599
|
-
export { Cell, EffectScope, _bind, activateReactiveDevtools, batch, cell, clearReactiveTrace, computed, createResource, createSelector, createStore, deactivateReactiveDevtools, effect, effectScope, getCurrentScope, getReactiveFires, getReactiveGraph, getReactiveTrace, inspectSignal, isReactiveDevtoolsActive, isStore, markRaw, nextTick, onCleanup, onScopeDispose, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, shallowReactive, signal, watch, why };
|
|
1442
|
+
export { Cell, EffectScope, _bind, activateReactiveDevtools, batch, cell, clearReactiveTrace, computed, createResource, createSelector, createStore, deactivateReactiveDevtools, effect, effectScope, getCurrentScope, getFireSummaries, getReactiveFires, getReactiveGraph, getReactiveTrace, inspectSignal, isReactiveDevtoolsActive, isStore, markRaw, nextTick, onCleanup, onScopeDispose, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, shallowReactive, signal, watch, why };
|
|
1600
1443
|
//# sourceMappingURL=index.js.map
|
package/lib/lpih.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { o as getFireSummaries } from "./_chunks/reactive-devtools-BCpGoGZ5.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lpih.ts
|
|
4
|
+
/**
|
|
5
|
+
* Live Program Inlay Hints — runtime bridge.
|
|
6
|
+
*
|
|
7
|
+
* Writes the current `getFireSummaries()` snapshot to a JSON file that
|
|
8
|
+
* the LSP server reads via the `PYREON_LPIH_CACHE` env var. This is the
|
|
9
|
+
* file-cache bridge mechanism — chosen over IPC/WebSocket because:
|
|
10
|
+
*
|
|
11
|
+
* 1. LSP servers are stdio-only — they can't easily talk to a browser.
|
|
12
|
+
* 2. Filesystem is a universal lowest-common-denominator transport.
|
|
13
|
+
* 3. The runtime side writes (atomic rename); the LSP side reads.
|
|
14
|
+
* 4. The LSP re-reads the file on every inlay-hint request, so live
|
|
15
|
+
* edits land immediately without coordination.
|
|
16
|
+
*
|
|
17
|
+
* Two consumer modes:
|
|
18
|
+
*
|
|
19
|
+
* **Dev-server polled mode**: a dev-server hook calls
|
|
20
|
+
* `writeLpihCache(path)` on every signal write or at a regular interval
|
|
21
|
+
* (e.g. 250ms throttle). The LSP picks it up on next inlay-hint request.
|
|
22
|
+
*
|
|
23
|
+
* **On-demand mode**: a test harness or devtools UI calls
|
|
24
|
+
* `writeLpihCache(path)` explicitly when it wants the LSP to see the
|
|
25
|
+
* current state.
|
|
26
|
+
*
|
|
27
|
+
* Atomic write semantics: writes to `<path>.tmp.<pid>.<seq>` then renames
|
|
28
|
+
* to `<path>`. Readers (the LSP server) never see a half-written file.
|
|
29
|
+
*
|
|
30
|
+
* Zero-cost when devtools is inactive: `getFireSummaries()` returns []
|
|
31
|
+
* unless `activateReactiveDevtools()` has been called. So calling
|
|
32
|
+
* `writeLpihCache()` against an inactive registry writes an empty
|
|
33
|
+
* `{ fires: [] }` — cheap, correct.
|
|
34
|
+
*/
|
|
35
|
+
let _seq = 0;
|
|
36
|
+
/**
|
|
37
|
+
* Canonical filename for the LPIH cache file. Co-located with the
|
|
38
|
+
* project — convention: `<cwd>/.pyreon-lpih.json`. The dot-prefix marks
|
|
39
|
+
* it as a hidden / generated file by filesystem convention; the
|
|
40
|
+
* extension makes its contents grep-able as JSON.
|
|
41
|
+
*
|
|
42
|
+
* @internal — exported for tests + symmetry with the LSP-side default.
|
|
43
|
+
*/
|
|
44
|
+
const LPIH_DEFAULT_FILENAME = ".pyreon-lpih.json";
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the default LPIH cache path for the current process. The path
|
|
47
|
+
* is **`<cwd>/.pyreon-lpih.json`** — co-located with the project so the
|
|
48
|
+
* LSP can auto-discover it by walking up from any source file.
|
|
49
|
+
*
|
|
50
|
+
* Returns null in environments without `process.cwd()` (e.g. a fresh
|
|
51
|
+
* web worker without polyfills) — callers should fall back to an
|
|
52
|
+
* explicit path argument.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* import { startLpihPolling, getDefaultLpihCachePath } from '@pyreon/reactivity/lpih'
|
|
56
|
+
* console.log(getDefaultLpihCachePath()) // → '/Users/me/proj/.pyreon-lpih.json'
|
|
57
|
+
* startLpihPolling() // writes to that path
|
|
58
|
+
*/
|
|
59
|
+
function getDefaultLpihCachePath() {
|
|
60
|
+
if (typeof process === "undefined") return null;
|
|
61
|
+
const proc = process;
|
|
62
|
+
if (typeof proc.cwd !== "function") return null;
|
|
63
|
+
try {
|
|
64
|
+
return `${proc.cwd().replace(/[/\\]+$/, "")}/${LPIH_DEFAULT_FILENAME}`;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Snapshot `getFireSummaries()` and write it to `path` atomically.
|
|
71
|
+
* Returns the number of fires written.
|
|
72
|
+
*
|
|
73
|
+
* **Path resolution**: when `path` is omitted, defaults to
|
|
74
|
+
* `<cwd>/.pyreon-lpih.json` (`getDefaultLpihCachePath()`). The LSP
|
|
75
|
+
* auto-discovers this convention by walking up from any source file to
|
|
76
|
+
* the nearest `package.json` — so projects that use the default need
|
|
77
|
+
* zero env-var configuration.
|
|
78
|
+
*
|
|
79
|
+
* Errors (filesystem permission, EACCES, etc.) are caught and re-thrown
|
|
80
|
+
* — the caller decides whether to swallow them. The runtime side wraps
|
|
81
|
+
* this in a try/catch when called from hot paths.
|
|
82
|
+
*
|
|
83
|
+
* Throws if `path` is omitted AND no default can be resolved (e.g.
|
|
84
|
+
* a web worker without `process.cwd()`).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* import { activateReactiveDevtools } from '@pyreon/reactivity'
|
|
88
|
+
* import { writeLpihCache } from '@pyreon/reactivity/lpih'
|
|
89
|
+
*
|
|
90
|
+
* activateReactiveDevtools()
|
|
91
|
+
* await writeLpihCache() // → writes to <cwd>/.pyreon-lpih.json
|
|
92
|
+
* // The LSP server auto-discovers this path; no env var needed.
|
|
93
|
+
*/
|
|
94
|
+
async function writeLpihCache(path) {
|
|
95
|
+
const resolvedPath = path ?? getDefaultLpihCachePath();
|
|
96
|
+
if (resolvedPath === null) throw new Error("[lpih] writeLpihCache: no path provided and no default could be resolved (process.cwd() unavailable). Pass an explicit path.");
|
|
97
|
+
return await _writeToPath(resolvedPath);
|
|
98
|
+
}
|
|
99
|
+
async function _writeToPath(path) {
|
|
100
|
+
const summaries = getFireSummaries();
|
|
101
|
+
const payload = { fires: summaries.map((s) => ({
|
|
102
|
+
file: s.loc.file,
|
|
103
|
+
line: s.loc.line,
|
|
104
|
+
count: s.count,
|
|
105
|
+
kind: s.kind,
|
|
106
|
+
lastFire: s.lastFire,
|
|
107
|
+
rate1s: s.rate1s
|
|
108
|
+
})) };
|
|
109
|
+
const tmp = `${path}.tmp.${typeof process !== "undefined" && "pid" in process ? process.pid ?? 0 : 0}.${++_seq}`;
|
|
110
|
+
const fs = await import("node:fs/promises");
|
|
111
|
+
try {
|
|
112
|
+
await fs.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
113
|
+
await fs.rename(tmp, path);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
try {
|
|
116
|
+
await fs.unlink(tmp);
|
|
117
|
+
} catch {}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
return summaries.length;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Polling helper: call `writeLpihCache(path)` every `intervalMs`. Returns
|
|
124
|
+
* a disposer that stops the timer.
|
|
125
|
+
*
|
|
126
|
+
* **Path resolution**: same as `writeLpihCache` — `path` defaults to
|
|
127
|
+
* `<cwd>/.pyreon-lpih.json` when omitted. The LSP auto-discovers this
|
|
128
|
+
* convention so projects need zero configuration.
|
|
129
|
+
*
|
|
130
|
+
* Useful for dev servers that want the LSP to see live updates. The
|
|
131
|
+
* interval is throttled (not debounced); a fast-firing signal won't
|
|
132
|
+
* generate one write per fire. 250-500ms is the recommended range.
|
|
133
|
+
*
|
|
134
|
+
* Throws synchronously if `path` is omitted AND no default can be
|
|
135
|
+
* resolved — the caller catches this once at startup rather than
|
|
136
|
+
* silently never writing.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* import { activateReactiveDevtools } from '@pyreon/reactivity'
|
|
140
|
+
* import { startLpihPolling } from '@pyreon/reactivity/lpih'
|
|
141
|
+
*
|
|
142
|
+
* if (import.meta.env.DEV) {
|
|
143
|
+
* activateReactiveDevtools()
|
|
144
|
+
* startLpihPolling() // writes to <cwd>/.pyreon-lpih.json every 250ms
|
|
145
|
+
* }
|
|
146
|
+
*/
|
|
147
|
+
function startLpihPolling(path, intervalMs = 250) {
|
|
148
|
+
const resolvedPath = path ?? getDefaultLpihCachePath();
|
|
149
|
+
if (resolvedPath === null) throw new Error("[lpih] startLpihPolling: no path provided and no default could be resolved (process.cwd() unavailable). Pass an explicit path.");
|
|
150
|
+
return _startPollingAt(resolvedPath, intervalMs);
|
|
151
|
+
}
|
|
152
|
+
function _startPollingAt(path, intervalMs) {
|
|
153
|
+
let active = true;
|
|
154
|
+
let timer = null;
|
|
155
|
+
const tick = async () => {
|
|
156
|
+
if (!active) return;
|
|
157
|
+
try {
|
|
158
|
+
await _writeToPath(path);
|
|
159
|
+
} catch {}
|
|
160
|
+
if (active) {
|
|
161
|
+
timer = setTimeout(tick, intervalMs);
|
|
162
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
tick();
|
|
166
|
+
return () => {
|
|
167
|
+
active = false;
|
|
168
|
+
if (timer !== null) {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
timer = null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//#endregion
|
|
176
|
+
export { LPIH_DEFAULT_FILENAME, getDefaultLpihCachePath, startLpihPolling, writeLpihCache };
|
|
177
|
+
//# sourceMappingURL=lpih.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -67,6 +67,21 @@ interface ComputedOptions<T> {
|
|
|
67
67
|
* })
|
|
68
68
|
*/
|
|
69
69
|
equals?: (prev: T, next: T) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* @internal — source location injected by `@pyreon/vite-plugin` at build
|
|
72
|
+
* time. When present, the runtime skips the `new Error().stack` capture
|
|
73
|
+
* in `_rdRegister` — saves ~2.2µs per computed creation when devtools is
|
|
74
|
+
* active. Plain user code should NOT set this; the field is opaque
|
|
75
|
+
* (no public type) so it's not part of the public API surface.
|
|
76
|
+
*
|
|
77
|
+
* Shape: `{ file: string; line: number; col: number }` matching
|
|
78
|
+
* `@pyreon/reactivity`'s `SourceLocation`.
|
|
79
|
+
*/
|
|
80
|
+
__sourceLocation?: {
|
|
81
|
+
file: string;
|
|
82
|
+
line: number;
|
|
83
|
+
col: number;
|
|
84
|
+
};
|
|
70
85
|
}
|
|
71
86
|
declare function computed<T>(fn: () => T, options?: ComputedOptions<T>): Computed<T>;
|
|
72
87
|
//#endregion
|
|
@@ -150,6 +165,21 @@ interface Signal<T> {
|
|
|
150
165
|
interface SignalOptions {
|
|
151
166
|
/** Debug name for this signal — shows up in devtools and debug() output. */
|
|
152
167
|
name?: string;
|
|
168
|
+
/**
|
|
169
|
+
* @internal — source location injected by `@pyreon/vite-plugin` at build
|
|
170
|
+
* time. When present, the runtime skips the `new Error().stack` capture
|
|
171
|
+
* in `_rdRegister` — saves ~2.2µs per signal creation when devtools is
|
|
172
|
+
* active. Plain user code should NOT set this; the field is opaque
|
|
173
|
+
* (no public type) so it's not part of the public API surface.
|
|
174
|
+
*
|
|
175
|
+
* Shape: `{ file: string; line: number; col: number }` matching
|
|
176
|
+
* `@pyreon/reactivity`'s `SourceLocation`.
|
|
177
|
+
*/
|
|
178
|
+
__sourceLocation?: {
|
|
179
|
+
file: string;
|
|
180
|
+
line: number;
|
|
181
|
+
col: number;
|
|
182
|
+
};
|
|
153
183
|
}
|
|
154
184
|
/**
|
|
155
185
|
* Create a reactive signal.
|
|
@@ -237,6 +267,25 @@ declare function inspectSignal<T>(sig: Signal<T>): SignalDebugInfo<T>;
|
|
|
237
267
|
* they get a stable synthetic label (`derived#12` / `effect#7`).
|
|
238
268
|
*/
|
|
239
269
|
type ReactiveNodeKind = 'signal' | 'derived' | 'effect';
|
|
270
|
+
/**
|
|
271
|
+
* Source location of a reactive node's creation — captured at registration
|
|
272
|
+
* time from the user's call stack. Powers "Live Program Inlay Hints" — the
|
|
273
|
+
* editor surfaces fire counts at the source line where the node was created.
|
|
274
|
+
*
|
|
275
|
+
* Captured ONLY when devtools is active (`_active === true`). Stack parsing
|
|
276
|
+
* is best-effort across V8 / JSC / SpiderMonkey; returns undefined when the
|
|
277
|
+
* stack format isn't recognized (older runtimes, minified prod, web workers
|
|
278
|
+
* without source maps). Dev gate is the existing `process.env.NODE_ENV` at
|
|
279
|
+
* each caller — production paths never run the capture.
|
|
280
|
+
*/
|
|
281
|
+
interface SourceLocation {
|
|
282
|
+
/** Absolute path or file URL parsed from the stack frame. */
|
|
283
|
+
file: string;
|
|
284
|
+
/** 1-based line number. */
|
|
285
|
+
line: number;
|
|
286
|
+
/** 1-based column number. */
|
|
287
|
+
col: number;
|
|
288
|
+
}
|
|
240
289
|
interface ReactiveNode {
|
|
241
290
|
id: number;
|
|
242
291
|
kind: ReactiveNodeKind;
|
|
@@ -250,6 +299,13 @@ interface ReactiveNode {
|
|
|
250
299
|
fires: number;
|
|
251
300
|
/** `performance.now()` of the most recent fire, or null. */
|
|
252
301
|
lastFire: number | null;
|
|
302
|
+
/**
|
|
303
|
+
* Source location of the creation call (`signal(0)` / `computed(...)` /
|
|
304
|
+
* `effect(...)`). Undefined when devtools wasn't active at creation
|
|
305
|
+
* time OR the stack format wasn't parseable. Editor inlay-hint surfaces
|
|
306
|
+
* consume this to merge live fire counts onto static spans.
|
|
307
|
+
*/
|
|
308
|
+
loc?: SourceLocation;
|
|
253
309
|
}
|
|
254
310
|
interface ReactiveEdge {
|
|
255
311
|
/** Source node id (the reactive value being read). */
|
|
@@ -266,6 +322,35 @@ interface ReactiveFire {
|
|
|
266
322
|
/** `performance.now()` at fire time. */
|
|
267
323
|
ts: number;
|
|
268
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Per-source-location fire-count summary. Aggregated from the fire ring
|
|
327
|
+
* buffer + node registry. The shape an editor / LSP inlay-hint consumer
|
|
328
|
+
* needs to merge "this signal at line N fires K times" onto static
|
|
329
|
+
* Reactivity-Lens spans. Pure data, JSON-serializable, no node refs.
|
|
330
|
+
*/
|
|
331
|
+
interface FireSummary {
|
|
332
|
+
loc: SourceLocation;
|
|
333
|
+
/** Total fires in the visible ring buffer at this location. */
|
|
334
|
+
count: number;
|
|
335
|
+
/** Most recent fire `performance.now()` at this location, or null. */
|
|
336
|
+
lastFire: number | null;
|
|
337
|
+
/** Node kind that fired most recently at this location. */
|
|
338
|
+
kind: ReactiveNodeKind;
|
|
339
|
+
/**
|
|
340
|
+
* Exponentially-weighted moving average of the fire rate at this
|
|
341
|
+
* location, in fires per second. Decayed to "now" at read time so a
|
|
342
|
+
* node that stopped firing N seconds ago shows a rate that's
|
|
343
|
+
* exponentially smaller than its steady-state value.
|
|
344
|
+
*
|
|
345
|
+
* Calculation uses a 1-second time constant (`LPIH_RATE_TAU_MS`):
|
|
346
|
+
* - On each fire: `r = r * exp(-dt/TAU) + 1`
|
|
347
|
+
* - Steady state at λ fires/sec converges to ≈ λ (when λ × TAU ≫ 1)
|
|
348
|
+
* - On read: `r_now = r * exp(-dt_since_last/TAU)`
|
|
349
|
+
*
|
|
350
|
+
* 0 when there have been no fires (or all fires were >>TAU ago).
|
|
351
|
+
*/
|
|
352
|
+
rate1s: number;
|
|
353
|
+
}
|
|
269
354
|
/** Activate the bridge. Idempotent. Called when a devtools client attaches. */
|
|
270
355
|
declare function activateReactiveDevtools(): void;
|
|
271
356
|
/**
|
|
@@ -280,6 +365,18 @@ declare function isReactiveDevtoolsActive(): boolean;
|
|
|
280
365
|
* framework's real subscription state, no incremental drift.
|
|
281
366
|
*/
|
|
282
367
|
declare function getReactiveGraph(): ReactiveGraph;
|
|
368
|
+
/**
|
|
369
|
+
* Aggregate fire counts by source-location — powers Live Program Inlay
|
|
370
|
+
* Hints. Walks the live node registry, keys each node by its captured
|
|
371
|
+
* `loc`, and returns one summary per unique `file:line:col`. Nodes
|
|
372
|
+
* without a captured location are skipped (their fires are still
|
|
373
|
+
* visible via `getReactiveGraph()` and `getReactiveFires()` for the
|
|
374
|
+
* existing graph / timeline surfaces).
|
|
375
|
+
*
|
|
376
|
+
* Returns a fresh array, JSON-serializable, safe to ship across the
|
|
377
|
+
* devtools-host bridge or to write into an LSP cache file.
|
|
378
|
+
*/
|
|
379
|
+
declare function getFireSummaries(): FireSummary[];
|
|
283
380
|
/** Bounded recent-fire timeline (oldest → newest). Fresh copy. */
|
|
284
381
|
declare function getReactiveFires(): ReactiveFire[];
|
|
285
382
|
//#endregion
|
|
@@ -337,6 +434,23 @@ declare function clearReactiveTrace(): void;
|
|
|
337
434
|
interface Effect {
|
|
338
435
|
dispose(): void;
|
|
339
436
|
}
|
|
437
|
+
interface EffectOptions {
|
|
438
|
+
/**
|
|
439
|
+
* @internal — source location injected by `@pyreon/vite-plugin` at build
|
|
440
|
+
* time. When present, the runtime skips the `new Error().stack` capture
|
|
441
|
+
* in `_rdRegister` — saves ~2.2µs per effect creation when devtools is
|
|
442
|
+
* active. Plain user code should NOT set this; the field is opaque
|
|
443
|
+
* (no public type) so it's not part of the public API surface.
|
|
444
|
+
*
|
|
445
|
+
* Shape: `{ file: string; line: number; col: number }` matching
|
|
446
|
+
* `@pyreon/reactivity`'s `SourceLocation`.
|
|
447
|
+
*/
|
|
448
|
+
__sourceLocation?: {
|
|
449
|
+
file: string;
|
|
450
|
+
line: number;
|
|
451
|
+
col: number;
|
|
452
|
+
};
|
|
453
|
+
}
|
|
340
454
|
interface ReactiveSnapshotCapture {
|
|
341
455
|
capture: () => unknown;
|
|
342
456
|
/** Run `fn` with the previously-captured snapshot active. */
|
|
@@ -369,7 +483,7 @@ declare function setSnapshotCapture(hook: ReactiveSnapshotCapture | null): void;
|
|
|
369
483
|
*/
|
|
370
484
|
declare function onCleanup(fn: () => void): void;
|
|
371
485
|
declare function setErrorHandler(fn: (err: unknown) => void): void;
|
|
372
|
-
declare function effect(fn: () => (() => void) | void): Effect;
|
|
486
|
+
declare function effect(fn: () => (() => void) | void, options?: EffectOptions): Effect;
|
|
373
487
|
/**
|
|
374
488
|
* Lightweight effect for DOM render bindings.
|
|
375
489
|
*
|
|
@@ -589,5 +703,5 @@ interface WatchOptions {
|
|
|
589
703
|
*/
|
|
590
704
|
declare function watch<T>(source: () => T, callback: (newVal: T, oldVal: T | undefined) => void | (() => void), opts?: WatchOptions): () => void;
|
|
591
705
|
//#endregion
|
|
592
|
-
export { Cell, type Computed, type ComputedOptions, type Effect, EffectScope, type ReactiveEdge, type ReactiveFire, type ReactiveGraph, type ReactiveNode, type ReactiveNodeKind, type ReactiveSnapshotCapture, type ReactiveTraceEntry, type ReadonlySignal, type Resource, type Signal, type SignalDebugInfo, type SignalOptions, type WatchOptions, _bind, activateReactiveDevtools, batch, cell, clearReactiveTrace, computed, createResource, createSelector, createStore, deactivateReactiveDevtools, effect, effectScope, getCurrentScope, getReactiveFires, getReactiveGraph, getReactiveTrace, inspectSignal, isReactiveDevtoolsActive, isStore, markRaw, nextTick, onCleanup, onScopeDispose, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, shallowReactive, signal, watch, why };
|
|
706
|
+
export { Cell, type Computed, type ComputedOptions, type Effect, EffectScope, type FireSummary, type ReactiveEdge, type ReactiveFire, type ReactiveGraph, type ReactiveNode, type ReactiveNodeKind, type ReactiveSnapshotCapture, type ReactiveTraceEntry, type ReadonlySignal, type Resource, type Signal, type SignalDebugInfo, type SignalOptions, type SourceLocation, type WatchOptions, _bind, activateReactiveDevtools, batch, cell, clearReactiveTrace, computed, createResource, createSelector, createStore, deactivateReactiveDevtools, effect, effectScope, getCurrentScope, getFireSummaries, getReactiveFires, getReactiveGraph, getReactiveTrace, inspectSignal, isReactiveDevtoolsActive, isStore, markRaw, nextTick, onCleanup, onScopeDispose, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, shallowReactive, signal, watch, why };
|
|
593
707
|
//# sourceMappingURL=index2.d.ts.map
|