@fluixi/devtools 0.2.0-alpha.6 → 1.0.0-alpha.99

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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @fluixi/devtools v0.2.0-alpha.6 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/devtools v1.0.0-alpha.99 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
 
3
3
  // src/instrument.ts
4
4
  import {
@@ -191,6 +191,8 @@ function walk(value, depth, seen) {
191
191
  const shown = keys.slice(0, MAX_ENTRIES).map((key) => {
192
192
  let read;
193
193
  try {
194
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
195
+ if (descriptor?.get) return `${key}: [getter]`;
194
196
  read = walk(object[key], depth + 1, seen);
195
197
  } catch {
196
198
  read = "[unreadable]";
@@ -542,6 +544,7 @@ function createReactor() {
542
544
 
543
545
  // src/observe.ts
544
546
  import {
547
+ createRoot as createRoot2,
545
548
  observeReactiveNodes,
546
549
  untrack as untrack2
547
550
  } from "@fluixi/reactive/signal";
@@ -600,7 +603,14 @@ function callSite(stack, origin) {
600
603
  return fallback2 ? { location: { ...fallback2, external: true }, external: true } : void 0;
601
604
  }
602
605
  function currentCallSite(origin) {
603
- return callSite(new Error().stack, origin);
606
+ const errors = Error;
607
+ const limit = errors.stackTraceLimit;
608
+ errors.stackTraceLimit = 50;
609
+ try {
610
+ return callSite(new Error().stack, origin);
611
+ } finally {
612
+ errors.stackTraceLimit = limit;
613
+ }
604
614
  }
605
615
 
606
616
  // src/history.ts
@@ -1607,6 +1617,7 @@ var PREFIX = {
1607
1617
  service: "svc"
1608
1618
  };
1609
1619
  function readProps(props) {
1620
+ if (readOnce.has(props)) return readOnce.get(props);
1610
1621
  const at3 = propsLocations(props);
1611
1622
  const out = [];
1612
1623
  for (const name of Object.keys(props)) {
@@ -1617,13 +1628,25 @@ function readProps(props) {
1617
1628
  ...where ? { at: { line: where[0], column: where[1] } } : {}
1618
1629
  };
1619
1630
  try {
1620
- report.value = formatValue(untrack2(() => props[name]), 200);
1631
+ report.value = formatValue(
1632
+ createRoot2((dispose) => {
1633
+ try {
1634
+ return untrack2(() => props[name]);
1635
+ } finally {
1636
+ dispose();
1637
+ }
1638
+ }),
1639
+ 200
1640
+ );
1621
1641
  } catch {
1622
1642
  }
1623
1643
  out.push(report);
1624
1644
  }
1625
- return out.length ? out : void 0;
1645
+ const reports = out.length ? out : void 0;
1646
+ readOnce.set(props, reports);
1647
+ return reports;
1626
1648
  }
1649
+ var readOnce = /* @__PURE__ */ new WeakMap();
1627
1650
  function observeGraph(options = {}) {
1628
1651
  installSourceMarker();
1629
1652
  const origin = options.origin ?? (typeof location === "object" ? location.origin : void 0);
@@ -1631,7 +1654,7 @@ function observeGraph(options = {}) {
1631
1654
  const claims = (node, value) => {
1632
1655
  for (const dep of node.deps) {
1633
1656
  const other = graph.get(dep);
1634
- if (other?.read && other.read() === value) return false;
1657
+ if (other?.read && readRaw(other) === value) return false;
1635
1658
  }
1636
1659
  return true;
1637
1660
  };
@@ -1649,6 +1672,7 @@ function observeGraph(options = {}) {
1649
1672
  const scopeOfTarget = /* @__PURE__ */ new WeakMap();
1650
1673
  const fieldsOf = /* @__PURE__ */ new WeakMap();
1651
1674
  let described = null;
1675
+ let descendants = null;
1652
1676
  const rendered = /* @__PURE__ */ new Map();
1653
1677
  const elementsIn = (value) => {
1654
1678
  if (value instanceof Element) return [value];
@@ -2010,6 +2034,7 @@ function observeGraph(options = {}) {
2010
2034
  graph,
2011
2035
  sample() {
2012
2036
  described = null;
2037
+ descendants = null;
2013
2038
  const changed = [];
2014
2039
  const moved = (id) => {
2015
2040
  changed.push(id);
@@ -2065,7 +2090,7 @@ function observeGraph(options = {}) {
2065
2090
  runsAtLastSample.set(node.id, node.runs);
2066
2091
  continue;
2067
2092
  }
2068
- const raw = node.read();
2093
+ const raw = readRaw(node);
2069
2094
  if (recording) raws.set(node.id, raw);
2070
2095
  edges(node.id, node);
2071
2096
  const stamped = raw?.$name;
@@ -2140,17 +2165,8 @@ function observeGraph(options = {}) {
2140
2165
  if (live.length) return live;
2141
2166
  collect(id);
2142
2167
  if (!live.length) {
2143
- const inside = (node) => {
2144
- const seen = /* @__PURE__ */ new Set();
2145
- let at3 = node.createdIn;
2146
- while (at3 !== void 0 && !seen.has(at3)) {
2147
- if (at3 === id) return true;
2148
- seen.add(at3);
2149
- at3 = graph.get(at3)?.createdIn;
2150
- }
2151
- return false;
2152
- };
2153
- for (const node of graph.values()) if (inside(node)) collect(node.id);
2168
+ descendants ??= descendantsByCreator(graph);
2169
+ for (const below of descendants.get(id) ?? []) collect(below);
2154
2170
  }
2155
2171
  return live;
2156
2172
  },
@@ -2263,6 +2279,21 @@ function observeGraph(options = {}) {
2263
2279
  }
2264
2280
  return api;
2265
2281
  }
2282
+ function descendantsByCreator(graph) {
2283
+ const out = /* @__PURE__ */ new Map();
2284
+ for (const node of graph.values()) {
2285
+ const seen = /* @__PURE__ */ new Set();
2286
+ let at3 = node.createdIn;
2287
+ while (at3 !== void 0 && !seen.has(at3)) {
2288
+ seen.add(at3);
2289
+ let list = out.get(at3);
2290
+ if (!list) out.set(at3, list = []);
2291
+ list.push(node.id);
2292
+ at3 = graph.get(at3)?.createdIn;
2293
+ }
2294
+ }
2295
+ return out;
2296
+ }
2266
2297
 
2267
2298
  // src/highlight.ts
2268
2299
  var LAYER = "fx-highlight-layer";
@@ -122,6 +122,8 @@ function walk(value, depth, seen) {
122
122
  const shown = keys.slice(0, MAX_ENTRIES).map((key) => {
123
123
  let read;
124
124
  try {
125
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
126
+ if (descriptor?.get) return `${key}: [getter]`;
125
127
  read = walk(object[key], depth + 1, seen);
126
128
  } catch {
127
129
  read = "[unreadable]";
@@ -106,6 +106,8 @@ function walk(value, depth, seen) {
106
106
  const shown = keys.slice(0, MAX_ENTRIES).map((key) => {
107
107
  let read;
108
108
  try {
109
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
110
+ if (descriptor?.get) return `${key}: [getter]`;
109
111
  read = walk(object[key], depth + 1, seen);
110
112
  } catch {
111
113
  read = "[unreadable]";
@@ -194,6 +194,8 @@ function walk(value, depth, seen) {
194
194
  const shown = keys.slice(0, MAX_ENTRIES).map((key) => {
195
195
  let read;
196
196
  try {
197
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
198
+ if (descriptor?.get) return `${key}: [getter]`;
197
199
  read = walk(object[key], depth + 1, seen);
198
200
  } catch {
199
201
  read = "[unreadable]";
@@ -176,6 +176,8 @@ function walk(value, depth, seen) {
176
176
  const shown = keys.slice(0, MAX_ENTRIES).map((key) => {
177
177
  let read;
178
178
  try {
179
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
180
+ if (descriptor?.get) return `${key}: [getter]`;
179
181
  read = walk(object[key], depth + 1, seen);
180
182
  } catch {
181
183
  read = "[unreadable]";
package/dist/observe.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/observe.ts
21
21
  var observe_exports = {};
22
22
  __export(observe_exports, {
23
+ descendantsByCreator: () => descendantsByCreator,
23
24
  observeGraph: () => observeGraph
24
25
  });
25
26
  module.exports = __toCommonJS(observe_exports);
@@ -173,7 +174,14 @@ function callSite(stack, origin) {
173
174
  return fallback ? { location: { ...fallback, external: true }, external: true } : void 0;
174
175
  }
175
176
  function currentCallSite(origin) {
176
- return callSite(new Error().stack, origin);
177
+ const errors = Error;
178
+ const limit = errors.stackTraceLimit;
179
+ errors.stackTraceLimit = 50;
180
+ try {
181
+ return callSite(new Error().stack, origin);
182
+ } finally {
183
+ errors.stackTraceLimit = limit;
184
+ }
177
185
  }
178
186
 
179
187
  // src/instrument.ts
@@ -262,6 +270,8 @@ function walk(value, depth, seen) {
262
270
  const shown = keys.slice(0, MAX_ENTRIES).map((key) => {
263
271
  let read;
264
272
  try {
273
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
274
+ if (descriptor?.get) return `${key}: [getter]`;
265
275
  read = walk(object[key], depth + 1, seen);
266
276
  } catch {
267
277
  read = "[unreadable]";
@@ -832,6 +842,7 @@ var PREFIX = {
832
842
  service: "svc"
833
843
  };
834
844
  function readProps(props) {
845
+ if (readOnce.has(props)) return readOnce.get(props);
835
846
  const at = propsLocations(props);
836
847
  const out = [];
837
848
  for (const name of Object.keys(props)) {
@@ -842,13 +853,25 @@ function readProps(props) {
842
853
  ...where ? { at: { line: where[0], column: where[1] } } : {}
843
854
  };
844
855
  try {
845
- report.value = formatValue((0, import_signal2.untrack)(() => props[name]), 200);
856
+ report.value = formatValue(
857
+ (0, import_signal2.createRoot)((dispose) => {
858
+ try {
859
+ return (0, import_signal2.untrack)(() => props[name]);
860
+ } finally {
861
+ dispose();
862
+ }
863
+ }),
864
+ 200
865
+ );
846
866
  } catch {
847
867
  }
848
868
  out.push(report);
849
869
  }
850
- return out.length ? out : void 0;
870
+ const reports = out.length ? out : void 0;
871
+ readOnce.set(props, reports);
872
+ return reports;
851
873
  }
874
+ var readOnce = /* @__PURE__ */ new WeakMap();
852
875
  function observeGraph(options = {}) {
853
876
  installSourceMarker();
854
877
  const origin = options.origin ?? (typeof location === "object" ? location.origin : void 0);
@@ -856,7 +879,7 @@ function observeGraph(options = {}) {
856
879
  const claims = (node, value) => {
857
880
  for (const dep of node.deps) {
858
881
  const other = graph.get(dep);
859
- if (other?.read && other.read() === value) return false;
882
+ if (other?.read && readRaw(other) === value) return false;
860
883
  }
861
884
  return true;
862
885
  };
@@ -874,6 +897,7 @@ function observeGraph(options = {}) {
874
897
  const scopeOfTarget = /* @__PURE__ */ new WeakMap();
875
898
  const fieldsOf = /* @__PURE__ */ new WeakMap();
876
899
  let described = null;
900
+ let descendants = null;
877
901
  const rendered = /* @__PURE__ */ new Map();
878
902
  const elementsIn = (value) => {
879
903
  if (value instanceof Element) return [value];
@@ -1235,6 +1259,7 @@ function observeGraph(options = {}) {
1235
1259
  graph,
1236
1260
  sample() {
1237
1261
  described = null;
1262
+ descendants = null;
1238
1263
  const changed = [];
1239
1264
  const moved = (id) => {
1240
1265
  changed.push(id);
@@ -1290,7 +1315,7 @@ function observeGraph(options = {}) {
1290
1315
  runsAtLastSample.set(node.id, node.runs);
1291
1316
  continue;
1292
1317
  }
1293
- const raw = node.read();
1318
+ const raw = readRaw(node);
1294
1319
  if (recording) raws.set(node.id, raw);
1295
1320
  edges(node.id, node);
1296
1321
  const stamped = raw?.$name;
@@ -1365,17 +1390,8 @@ function observeGraph(options = {}) {
1365
1390
  if (live.length) return live;
1366
1391
  collect(id);
1367
1392
  if (!live.length) {
1368
- const inside = (node) => {
1369
- const seen = /* @__PURE__ */ new Set();
1370
- let at = node.createdIn;
1371
- while (at !== void 0 && !seen.has(at)) {
1372
- if (at === id) return true;
1373
- seen.add(at);
1374
- at = graph.get(at)?.createdIn;
1375
- }
1376
- return false;
1377
- };
1378
- for (const node of graph.values()) if (inside(node)) collect(node.id);
1393
+ descendants ??= descendantsByCreator(graph);
1394
+ for (const below of descendants.get(id) ?? []) collect(below);
1379
1395
  }
1380
1396
  return live;
1381
1397
  },
@@ -1488,3 +1504,18 @@ function observeGraph(options = {}) {
1488
1504
  }
1489
1505
  return api;
1490
1506
  }
1507
+ function descendantsByCreator(graph) {
1508
+ const out = /* @__PURE__ */ new Map();
1509
+ for (const node of graph.values()) {
1510
+ const seen = /* @__PURE__ */ new Set();
1511
+ let at = node.createdIn;
1512
+ while (at !== void 0 && !seen.has(at)) {
1513
+ seen.add(at);
1514
+ let list = out.get(at);
1515
+ if (!list) out.set(at, list = []);
1516
+ list.push(node.id);
1517
+ at = graph.get(at)?.createdIn;
1518
+ }
1519
+ }
1520
+ return out;
1521
+ }
package/dist/observe.d.ts CHANGED
@@ -77,4 +77,11 @@ export interface ObserveOptions {
77
77
  mapSource?: (location: ReactiveSourceLocation) => ReactiveSourceLocation | undefined;
78
78
  }
79
79
  export declare function observeGraph(options?: ObserveOptions): ObservedGraph;
80
+ /**
81
+ * What every node created, at any depth, in graph order: the nodes whose `createdIn` chain
82
+ * passes through it. Built in one pass over the graph, so asking it for each tag of a page is
83
+ * linear; walking every node's chain once per tag made a sample quadratic, which on a page of a
84
+ * few thousand nodes took seconds on every keystroke.
85
+ */
86
+ export declare function descendantsByCreator(graph: Map<number, RNode>): Map<number, number[]>;
80
87
  //# sourceMappingURL=observe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../src/observe.ts"],"names":[],"mappings":"AAUA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAmE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAG9D,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAmDhE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnC,gFAAgF;IAChF,MAAM,IAAI,MAAM,EAAE,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,6FAA6F;IAC7F,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,4EAA4E;IAC5E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,8EAA8E;IAC9E,UAAU,IAAI,UAAU,EAAE,CAAC;IAC3B,qEAAqE;IACrE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IACnC,wEAAwE;IACxE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IACpD;;;OAGG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7C,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IACrD,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,sBAAsB,GAAG,SAAS,CAAC;CACtF;AA0ED,wBAAgB,YAAY,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CAk2BxE"}
1
+ {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../src/observe.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAmE,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAG9D,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAmDhE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnC,gFAAgF;IAChF,MAAM,IAAI,MAAM,EAAE,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,6FAA6F;IAC7F,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACpC,4EAA4E;IAC5E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,8EAA8E;IAC9E,UAAU,IAAI,UAAU,EAAE,CAAC;IAC3B,qEAAqE;IACrE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IACnC,wEAAwE;IACxE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IACpD;;;OAGG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7C,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IACrD,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,sBAAsB,GAAG,SAAS,CAAC;CACtF;AAgGD,wBAAgB,YAAY,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CA81BxE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAcrF"}
package/dist/observe.js CHANGED
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // The graph it fills is the same Map<number, RNode> createGraphView already draws, so a
10
10
  // renderer does not care which of the two produced it.
11
- import { observeReactiveNodes, untrack, } from '@fluixi/reactive/signal';
11
+ import { createRoot, observeReactiveNodes, untrack, } from '@fluixi/reactive/signal';
12
12
  import { classSource, componentSource, dropReactiveSource, holeLocation, installSourceMarker, propsLocations, takeReactiveSource } from './source.js';
13
13
  import { currentCallSite } from './callsite.js';
14
14
  import { KIND_OF_BUILTIN } from './instrument.js';
@@ -91,14 +91,23 @@ const PREFIX = {
91
91
  * app's modules load, which in practice means before the entry import.
92
92
  */
93
93
  /**
94
- * What a component was handed, read for display.
94
+ * What a component was handed, read for display, once per props object.
95
95
  *
96
96
  * Every prop is a getter over whatever the caller wrote, so reading one runs that
97
97
  * expression. Untracked, or the read would subscribe whatever is sampling to it and the
98
- * panel would drive the graph by looking at it. `children` is skipped: reading it builds
99
- * the subtree, which is a side effect nobody asked for by opening an inspector.
98
+ * panel would drive the graph by looking at it. And only once: JSX in a prop compiles to a
99
+ * getter that makes a new memo on each read, which the next sample pulls, building the
100
+ * component again, and any other prop can reach such a getter (`when={local.trailing != null}`).
101
+ * Read on every sample, each one added another copy of those components to the graph.
102
+ * `children` is skipped: reading it builds the subtree.
103
+ *
104
+ * Any other getter can build one too (`Show`'s `fallback`), and what it builds is live: its
105
+ * effects run outside the component, so a context it reads is missing, and they keep running
106
+ * and throwing on every change. Each read happens in a root disposed as soon as it returns.
100
107
  */
101
108
  function readProps(props) {
109
+ if (readOnce.has(props))
110
+ return readOnce.get(props);
102
111
  const at = propsLocations(props);
103
112
  const out = [];
104
113
  for (const name of Object.keys(props)) {
@@ -110,15 +119,26 @@ function readProps(props) {
110
119
  ...(where ? { at: { line: where[0], column: where[1] } } : {}),
111
120
  };
112
121
  try {
113
- report.value = formatValue(untrack(() => props[name]), 200);
122
+ report.value = formatValue(createRoot((dispose) => {
123
+ try {
124
+ return untrack(() => props[name]);
125
+ }
126
+ finally {
127
+ dispose();
128
+ }
129
+ }), 200);
114
130
  }
115
131
  catch {
116
132
  // A getter that throws is still a prop worth naming.
117
133
  }
118
134
  out.push(report);
119
135
  }
120
- return out.length ? out : undefined;
136
+ const reports = out.length ? out : undefined;
137
+ readOnce.set(props, reports);
138
+ return reports;
121
139
  }
140
+ /** Each props object's reports, from its first read. */
141
+ const readOnce = new WeakMap();
122
142
  export function observeGraph(options = {}) {
123
143
  installSourceMarker();
124
144
  const origin = options.origin ?? (typeof location === 'object' ? location.origin : undefined);
@@ -134,7 +154,7 @@ export function observeGraph(options = {}) {
134
154
  const claims = (node, value) => {
135
155
  for (const dep of node.deps) {
136
156
  const other = graph.get(dep);
137
- if (other?.read && other.read() === value)
157
+ if (other?.read && readRaw(other) === value)
138
158
  return false;
139
159
  }
140
160
  return true;
@@ -189,6 +209,8 @@ export function observeGraph(options = {}) {
189
209
  const fieldsOf = new WeakMap();
190
210
  /** Cleared on each sample: the page has moved on. */
191
211
  let described = null;
212
+ /** Per sample, like `described`: what each node created, at any depth. */
213
+ let descendants = null;
192
214
  /**
193
215
  * What each tag rendered, weakly.
194
216
  *
@@ -704,6 +726,7 @@ export function observeGraph(options = {}) {
704
726
  graph,
705
727
  sample() {
706
728
  described = null;
729
+ descendants = null;
707
730
  const changed = [];
708
731
  /** Recorded once here, so every consumer can be told about it at its own pace. */
709
732
  const moved = (id) => {
@@ -780,7 +803,9 @@ export function observeGraph(options = {}) {
780
803
  runsAtLastSample.set(node.id, node.runs);
781
804
  continue;
782
805
  }
783
- const raw = node.read();
806
+ // Guarded: a memo holding an error rethrows it on every read, and this runs on a
807
+ // timer, so an unguarded read threw the page's error ten times a second.
808
+ const raw = readRaw(node);
784
809
  // For the next write to diff against; without it the first change has no entries.
785
810
  if (recording)
786
811
  raws.set(node.id, raw);
@@ -897,20 +922,9 @@ export function observeGraph(options = {}) {
897
922
  // A value drives the element its binding was recorded against. A tag that returned
898
923
  // nothing usable falls back to where the values inside it reach.
899
924
  if (!live.length) {
900
- const inside = (node) => {
901
- const seen = new Set();
902
- let at = node.createdIn;
903
- while (at !== undefined && !seen.has(at)) {
904
- if (at === id)
905
- return true;
906
- seen.add(at);
907
- at = graph.get(at)?.createdIn;
908
- }
909
- return false;
910
- };
911
- for (const node of graph.values())
912
- if (inside(node))
913
- collect(node.id);
925
+ descendants ??= descendantsByCreator(graph);
926
+ for (const below of descendants.get(id) ?? [])
927
+ collect(below);
914
928
  }
915
929
  return live;
916
930
  },
@@ -1040,3 +1054,25 @@ export function observeGraph(options = {}) {
1040
1054
  }
1041
1055
  return api;
1042
1056
  }
1057
+ /**
1058
+ * What every node created, at any depth, in graph order: the nodes whose `createdIn` chain
1059
+ * passes through it. Built in one pass over the graph, so asking it for each tag of a page is
1060
+ * linear; walking every node's chain once per tag made a sample quadratic, which on a page of a
1061
+ * few thousand nodes took seconds on every keystroke.
1062
+ */
1063
+ export function descendantsByCreator(graph) {
1064
+ const out = new Map();
1065
+ for (const node of graph.values()) {
1066
+ const seen = new Set();
1067
+ let at = node.createdIn;
1068
+ while (at !== undefined && !seen.has(at)) {
1069
+ seen.add(at);
1070
+ let list = out.get(at);
1071
+ if (!list)
1072
+ out.set(at, (list = []));
1073
+ list.push(node.id);
1074
+ at = graph.get(at)?.createdIn;
1075
+ }
1076
+ }
1077
+ return out;
1078
+ }
package/dist/observe.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/observe.ts
2
2
  import {
3
+ createRoot as createRoot2,
3
4
  observeReactiveNodes,
4
5
  untrack as untrack2
5
6
  } from "@fluixi/reactive/signal";
@@ -152,7 +153,14 @@ function callSite(stack, origin) {
152
153
  return fallback ? { location: { ...fallback, external: true }, external: true } : void 0;
153
154
  }
154
155
  function currentCallSite(origin) {
155
- return callSite(new Error().stack, origin);
156
+ const errors = Error;
157
+ const limit = errors.stackTraceLimit;
158
+ errors.stackTraceLimit = 50;
159
+ try {
160
+ return callSite(new Error().stack, origin);
161
+ } finally {
162
+ errors.stackTraceLimit = limit;
163
+ }
156
164
  }
157
165
 
158
166
  // src/instrument.ts
@@ -252,6 +260,8 @@ function walk(value, depth, seen) {
252
260
  const shown = keys.slice(0, MAX_ENTRIES).map((key) => {
253
261
  let read;
254
262
  try {
263
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
264
+ if (descriptor?.get) return `${key}: [getter]`;
255
265
  read = walk(object[key], depth + 1, seen);
256
266
  } catch {
257
267
  read = "[unreadable]";
@@ -822,6 +832,7 @@ var PREFIX = {
822
832
  service: "svc"
823
833
  };
824
834
  function readProps(props) {
835
+ if (readOnce.has(props)) return readOnce.get(props);
825
836
  const at = propsLocations(props);
826
837
  const out = [];
827
838
  for (const name of Object.keys(props)) {
@@ -832,13 +843,25 @@ function readProps(props) {
832
843
  ...where ? { at: { line: where[0], column: where[1] } } : {}
833
844
  };
834
845
  try {
835
- report.value = formatValue(untrack2(() => props[name]), 200);
846
+ report.value = formatValue(
847
+ createRoot2((dispose) => {
848
+ try {
849
+ return untrack2(() => props[name]);
850
+ } finally {
851
+ dispose();
852
+ }
853
+ }),
854
+ 200
855
+ );
836
856
  } catch {
837
857
  }
838
858
  out.push(report);
839
859
  }
840
- return out.length ? out : void 0;
860
+ const reports = out.length ? out : void 0;
861
+ readOnce.set(props, reports);
862
+ return reports;
841
863
  }
864
+ var readOnce = /* @__PURE__ */ new WeakMap();
842
865
  function observeGraph(options = {}) {
843
866
  installSourceMarker();
844
867
  const origin = options.origin ?? (typeof location === "object" ? location.origin : void 0);
@@ -846,7 +869,7 @@ function observeGraph(options = {}) {
846
869
  const claims = (node, value) => {
847
870
  for (const dep of node.deps) {
848
871
  const other = graph.get(dep);
849
- if (other?.read && other.read() === value) return false;
872
+ if (other?.read && readRaw(other) === value) return false;
850
873
  }
851
874
  return true;
852
875
  };
@@ -864,6 +887,7 @@ function observeGraph(options = {}) {
864
887
  const scopeOfTarget = /* @__PURE__ */ new WeakMap();
865
888
  const fieldsOf = /* @__PURE__ */ new WeakMap();
866
889
  let described = null;
890
+ let descendants = null;
867
891
  const rendered = /* @__PURE__ */ new Map();
868
892
  const elementsIn = (value) => {
869
893
  if (value instanceof Element) return [value];
@@ -1225,6 +1249,7 @@ function observeGraph(options = {}) {
1225
1249
  graph,
1226
1250
  sample() {
1227
1251
  described = null;
1252
+ descendants = null;
1228
1253
  const changed = [];
1229
1254
  const moved = (id) => {
1230
1255
  changed.push(id);
@@ -1280,7 +1305,7 @@ function observeGraph(options = {}) {
1280
1305
  runsAtLastSample.set(node.id, node.runs);
1281
1306
  continue;
1282
1307
  }
1283
- const raw = node.read();
1308
+ const raw = readRaw(node);
1284
1309
  if (recording) raws.set(node.id, raw);
1285
1310
  edges(node.id, node);
1286
1311
  const stamped = raw?.$name;
@@ -1355,17 +1380,8 @@ function observeGraph(options = {}) {
1355
1380
  if (live.length) return live;
1356
1381
  collect(id);
1357
1382
  if (!live.length) {
1358
- const inside = (node) => {
1359
- const seen = /* @__PURE__ */ new Set();
1360
- let at = node.createdIn;
1361
- while (at !== void 0 && !seen.has(at)) {
1362
- if (at === id) return true;
1363
- seen.add(at);
1364
- at = graph.get(at)?.createdIn;
1365
- }
1366
- return false;
1367
- };
1368
- for (const node of graph.values()) if (inside(node)) collect(node.id);
1383
+ descendants ??= descendantsByCreator(graph);
1384
+ for (const below of descendants.get(id) ?? []) collect(below);
1369
1385
  }
1370
1386
  return live;
1371
1387
  },
@@ -1478,6 +1494,22 @@ function observeGraph(options = {}) {
1478
1494
  }
1479
1495
  return api;
1480
1496
  }
1497
+ function descendantsByCreator(graph) {
1498
+ const out = /* @__PURE__ */ new Map();
1499
+ for (const node of graph.values()) {
1500
+ const seen = /* @__PURE__ */ new Set();
1501
+ let at = node.createdIn;
1502
+ while (at !== void 0 && !seen.has(at)) {
1503
+ seen.add(at);
1504
+ let list = out.get(at);
1505
+ if (!list) out.set(at, list = []);
1506
+ list.push(node.id);
1507
+ at = graph.get(at)?.createdIn;
1508
+ }
1509
+ }
1510
+ return out;
1511
+ }
1481
1512
  export {
1513
+ descendantsByCreator,
1482
1514
  observeGraph
1483
1515
  };
@@ -1,4 +1,4 @@
1
- /*! @fluixi/devtools v0.2.0-alpha.6 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/devtools v1.0.0-alpha.99 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1,4 +1,4 @@
1
- /*! @fluixi/devtools v0.2.0-alpha.6 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/devtools v1.0.0-alpha.99 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
 
3
3
  // src/plugin.ts
4
4
  var plugins = [];