rerender-lens 0.2.0 → 0.3.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +146 -5
  3. package/dist/budget-COBu7jBU.d.cts +64 -0
  4. package/dist/budget-LkNjGtRc.d.ts +64 -0
  5. package/dist/cli.cjs +458 -0
  6. package/dist/cli.cjs.map +1 -0
  7. package/dist/cli.d.cts +4 -0
  8. package/dist/cli.d.ts +4 -0
  9. package/dist/cli.js +452 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/devtools-BHGUUo-p.d.ts +244 -0
  12. package/dist/devtools-DmhWiWcN.d.cts +244 -0
  13. package/dist/index.cjs +652 -23
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +40 -336
  16. package/dist/index.d.ts +40 -336
  17. package/dist/index.js +636 -24
  18. package/dist/index.js.map +1 -1
  19. package/dist/notifiers-BGQtWKfX.d.cts +90 -0
  20. package/dist/notifiers-BjjGSHpp.d.ts +90 -0
  21. package/dist/playwright.cjs +196 -0
  22. package/dist/playwright.cjs.map +1 -0
  23. package/dist/playwright.d.cts +35 -0
  24. package/dist/playwright.d.ts +35 -0
  25. package/dist/playwright.js +184 -0
  26. package/dist/playwright.js.map +1 -0
  27. package/dist/relay.cjs +166 -0
  28. package/dist/relay.cjs.map +1 -0
  29. package/dist/relay.d.cts +41 -0
  30. package/dist/relay.d.ts +41 -0
  31. package/dist/relay.js +161 -0
  32. package/dist/relay.js.map +1 -0
  33. package/dist/rerender-lens.iife.js +1717 -0
  34. package/dist/setup.cjs +1510 -0
  35. package/dist/setup.cjs.map +1 -0
  36. package/dist/setup.d.cts +2 -0
  37. package/dist/setup.d.ts +2 -0
  38. package/dist/setup.js +1508 -0
  39. package/dist/setup.js.map +1 -0
  40. package/dist/types-BzUEVkxJ.d.cts +177 -0
  41. package/dist/types-BzUEVkxJ.d.ts +177 -0
  42. package/dist/vite.cjs +113 -0
  43. package/dist/vite.cjs.map +1 -0
  44. package/dist/vite.d.cts +84 -0
  45. package/dist/vite.d.ts +84 -0
  46. package/dist/vite.js +103 -0
  47. package/dist/vite.js.map +1 -0
  48. package/dist/vitest-setup.cjs +1299 -0
  49. package/dist/vitest-setup.cjs.map +1 -0
  50. package/dist/vitest-setup.d.cts +8 -0
  51. package/dist/vitest-setup.d.ts +8 -0
  52. package/dist/vitest-setup.js +1297 -0
  53. package/dist/vitest-setup.js.map +1 -0
  54. package/dist/vitest.cjs +1360 -0
  55. package/dist/vitest.cjs.map +1 -0
  56. package/dist/vitest.d.cts +52 -0
  57. package/dist/vitest.d.ts +52 -0
  58. package/dist/vitest.js +1351 -0
  59. package/dist/vitest.js.map +1 -0
  60. package/package.json +84 -4
  61. package/panel/panel.css +418 -0
  62. package/panel/panel.html +12 -0
  63. package/panel/panel.js +3325 -0
package/dist/index.js CHANGED
@@ -145,6 +145,17 @@ function firstDifferentPath(a, b, path, depth = 0) {
145
145
 
146
146
  // src/report.ts
147
147
  var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
148
+ var hookLabel = (c) => c.custom && c.custom.length ? `${c.custom.join(" \u203A ")} \u203A ${c.hook}#${c.index}` : `${c.hook} #${c.index}`;
149
+ function storeAdvice(custom) {
150
+ const chain = custom ?? [];
151
+ if (chain.some((n) => /^useSelector$/.test(n)))
152
+ return "the Redux selector returns a new object on every call: return a stored slice, pass shallowEqual as the equality function, or memoize it with createSelector";
153
+ if (chain.some((n) => /^useAppSelector$/.test(n)))
154
+ return "the selector returns a new object on every call: return a stored slice, pass shallowEqual, or memoize it with createSelector";
155
+ if (chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore"))
156
+ return "the store selector returns a new object on every call: select a primitive, or wrap the selector with useShallow (Zustand) / an equality function";
157
+ return "getSnapshot returns a new reference with the same contents: cache the snapshot in the store and return the same object while the data is unchanged";
158
+ }
148
159
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
149
160
  var isChildren = (change) => change.path === "children";
150
161
  function fixFor(change) {
@@ -220,10 +231,17 @@ function buildReport(input) {
220
231
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
221
232
  const keys = c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys ? `: only ${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read` : "";
222
233
  reasons.push(`${c.path} changed${where}${keys}.`);
223
- } else if (isGenuine(c)) reasons.push(`${c.hook} #${c.index} changed.`);
234
+ } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
224
235
  else if (isStateHook(c))
225
- reasons.push(`${c.hook} #${c.index} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
226
- else reasons.push(`${c.hook} #${c.index} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);
236
+ reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
237
+ else if (c.hook === "useSyncExternalStore") reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: ${storeAdvice(c.custom)}.`);
238
+ else reasons.push(`${hookLabel(c)} returned a new reference that is deep-equal to the previous value: memoize the context/store value where it is produced.`);
239
+ }
240
+ if (input.commitCause === "effect-after-commit") {
241
+ const who = input.updaters && input.updaters.length ? input.updaters.map((u) => `<${u}>`).join(", ") : "a component that rendered in it";
242
+ reasons.push(`this commit was scheduled right after commit #${input.afterCommit ?? "?"} by ${who}: an effect there set state (effect \u2192 setState loop). Derive the value during render or compute it before setting state.`);
243
+ } else if (input.commitCause === "suspense-resolved") {
244
+ reasons.push("this commit shows content that was suspended (a Suspense boundary resolved).");
227
245
  }
228
246
  const report = {
229
247
  component: input.component,
@@ -247,6 +265,13 @@ function buildReport(input) {
247
265
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
248
266
  if (input.commitPriority) report.commitPriority = input.commitPriority;
249
267
  if (input.source) report.source = input.source;
268
+ if (input.hookState) report.hookState = input.hookState;
269
+ if (input.contexts) report.contexts = input.contexts;
270
+ if (input.state) report.state = input.state;
271
+ if (input.updaters && input.updaters.length) report.updaters = input.updaters;
272
+ if (input.commitCause) report.commitCause = input.commitCause;
273
+ if (input.afterCommit !== void 0) report.afterCommit = input.afterCommit;
274
+ if (input.key !== void 0) report.key = input.key;
250
275
  return report;
251
276
  }
252
277
  function describeTrigger(t) {
@@ -291,6 +316,9 @@ function printReport(report, options) {
291
316
  c.log(`${ch.path} (${KIND_LABEL[ch.kind]})`, { prev: ch.prev, next: ch.next });
292
317
  }
293
318
  c.log("props", report.props);
319
+ if (report.hookState && report.hookState.length) c.log("hooks", Object.fromEntries(report.hookState.map((h) => [h.path, h.value])));
320
+ if (report.contexts && report.contexts.length) c.log("contexts", Object.fromEntries(report.contexts.map((x) => [x.name, x.value])));
321
+ if (report.state) c.log("state", report.state);
294
322
  c.groupEnd();
295
323
  }
296
324
 
@@ -300,7 +328,7 @@ function getState() {
300
328
  const g = globalThis;
301
329
  let s = g[KEY];
302
330
  if (!s) {
303
- s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0 };
331
+ s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0 };
304
332
  g[KEY] = s;
305
333
  }
306
334
  return s;
@@ -335,6 +363,171 @@ function dispatch(report, override) {
335
363
  }
336
364
  }
337
365
 
366
+ // src/hookNames.ts
367
+ var noop = () => {
368
+ };
369
+ function customHooksFromStack(stack, componentName, marker) {
370
+ const names = [];
371
+ let past = false;
372
+ for (const line of stack.split("\n")) {
373
+ const m = /^\s*at\s+(?:async\s+)?([^\s(]+)|^([^@\s]+)@/.exec(line);
374
+ const name = m ? (m[1] || m[2] || "").replace(/^Object\./, "").replace(/^Proxy\./, "") : "";
375
+ if (!name) continue;
376
+ if (!past) {
377
+ if (name.includes(marker)) past = true;
378
+ continue;
379
+ }
380
+ if (name === componentName || name.endsWith("." + componentName)) break;
381
+ if (/^use[A-Z0-9_]/.test(name)) names.push(name);
382
+ if (names.length > 8) break;
383
+ }
384
+ return names;
385
+ }
386
+ var cache = /* @__PURE__ */ new WeakMap();
387
+ function makeDispatcher(fiber, componentName, out, marker) {
388
+ let node = fiber.memoizedState;
389
+ let index = 0;
390
+ const take = (count) => {
391
+ const first = node;
392
+ for (let i = 0; i < count && node; i++) node = node.next;
393
+ index += count;
394
+ return first;
395
+ };
396
+ const record = (primitive, count) => {
397
+ const stack = new Error().stack || "";
398
+ const chain = customHooksFromStack(stack, componentName, marker);
399
+ const start = index;
400
+ const first = take(count);
401
+ if (chain.length) for (let i = 0; i < count; i++) out.set(start + i, chain);
402
+ return first;
403
+ };
404
+ const memoized = (n) => n ? n.memoizedState : void 0;
405
+ const dispatcher = {};
406
+ const define = (name, impl) => {
407
+ const fn = { [marker + name]: (...a) => impl(...a) }[marker + name];
408
+ dispatcher[name] = fn;
409
+ };
410
+ define("useState", () => {
411
+ const n = record("useState", 1);
412
+ return [memoized(n), noop];
413
+ });
414
+ define("useReducer", () => {
415
+ const n = record("useReducer", 1);
416
+ return [memoized(n), noop];
417
+ });
418
+ define("useRef", (init2) => {
419
+ const n = record("useRef", 1);
420
+ return n ? n.memoizedState : { current: init2 };
421
+ });
422
+ define("useMemo", (fn) => {
423
+ const n = record("useMemo", 1);
424
+ const s = memoized(n);
425
+ return Array.isArray(s) ? s[0] : typeof fn === "function" ? fn() : void 0;
426
+ });
427
+ define("useCallback", (fn) => {
428
+ const n = record("useCallback", 1);
429
+ const s = memoized(n);
430
+ return Array.isArray(s) ? s[0] : fn;
431
+ });
432
+ for (const effect of ["useEffect", "useLayoutEffect", "useInsertionEffect", "useImperativeHandle"]) define(effect, () => void record(effect, 1));
433
+ define("useContext", (ctx) => {
434
+ record("useContext", 0);
435
+ const c = ctx;
436
+ return c ? c._currentValue : void 0;
437
+ });
438
+ define("useDebugValue", () => void record("useDebugValue", 0));
439
+ define("useSyncExternalStore", (_subscribe, getSnapshot) => {
440
+ const n = record("useSyncExternalStore", 2);
441
+ return n ? n.memoizedState : typeof getSnapshot === "function" ? getSnapshot() : void 0;
442
+ });
443
+ define("useTransition", () => {
444
+ record("useTransition", 2);
445
+ return [false, noop];
446
+ });
447
+ define("useDeferredValue", (v) => {
448
+ const n = record("useDeferredValue", 1);
449
+ return n ? n.memoizedState : v;
450
+ });
451
+ define("useId", () => {
452
+ const n = record("useId", 1);
453
+ return n ? n.memoizedState : "id";
454
+ });
455
+ define("useOptimistic", (v) => {
456
+ const n = record("useOptimistic", 1);
457
+ return [n ? n.memoizedState : v, noop];
458
+ });
459
+ for (const action of ["useActionState", "useFormState"]) {
460
+ define(action, (_fn, init2) => {
461
+ const n = record(action, 3);
462
+ return [n ? n.memoizedState : init2, noop, false];
463
+ });
464
+ }
465
+ define("useEffectEvent", (fn) => {
466
+ record("useEffectEvent", 1);
467
+ return fn;
468
+ });
469
+ define("useCacheRefresh", () => {
470
+ record("useCacheRefresh", 1);
471
+ return noop;
472
+ });
473
+ define("useHostTransitionStatus", () => ({ pending: false, data: null, method: null, action: null }));
474
+ define("useMemoCache", (size) => new Array(Number(size) || 0).fill(/* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")));
475
+ define("use", (usable) => {
476
+ record("use", 0);
477
+ const u = usable;
478
+ if (u && typeof u.then === "function") {
479
+ if (u.status === "fulfilled") return u.value;
480
+ throw new Error("rerender-lens: cannot replay use() on a pending promise");
481
+ }
482
+ return u ? u._currentValue : void 0;
483
+ });
484
+ define("readContext", (ctx) => ctx?._currentValue);
485
+ return { dispatcher, nodesConsumed: () => index };
486
+ }
487
+ function renderFunctionOf(type) {
488
+ let t = type;
489
+ while (t && typeof t === "object" && t.type) t = t.type;
490
+ if (t && typeof t === "object" && typeof t.render === "function") return { fn: t.render, name: t.render.name || t.displayName || "Component" };
491
+ if (typeof t === "function") return { fn: t, name: t.name || "Component" };
492
+ return { fn: null, name: "Component" };
493
+ }
494
+ function resolveHookNames(fiber, ref) {
495
+ const type = fiber.type;
496
+ if (!type || !ref) return /* @__PURE__ */ new Map();
497
+ const cached = cache.get(type);
498
+ if (cached) return cached.byNode;
499
+ const result = { byNode: /* @__PURE__ */ new Map(), ok: false };
500
+ cache.set(type, result);
501
+ const { fn, name } = renderFunctionOf(type);
502
+ if (!fn) return result.byNode;
503
+ const useH = "H" in ref;
504
+ const marker = "__rl_";
505
+ const previous = useH ? ref.H : ref.current;
506
+ const { dispatcher, nodesConsumed } = makeDispatcher(fiber, name, result.byNode, marker);
507
+ const proxy = new Proxy(dispatcher, {
508
+ get(target, key) {
509
+ if (key in target) return target[key];
510
+ return () => void 0;
511
+ }
512
+ });
513
+ let total = 0;
514
+ for (let n = fiber.memoizedState; n && typeof n === "object" && "next" in n; n = n.next) total++;
515
+ try {
516
+ if (useH) ref.H = proxy;
517
+ else ref.current = proxy;
518
+ const secondArg = fiber.tag === 11 ? fiber.stateNode?.ref ?? null : void 0;
519
+ fn(fiber.memoizedProps ?? {}, secondArg);
520
+ result.ok = nodesConsumed() === total;
521
+ if (!result.ok) result.byNode.clear();
522
+ } catch {
523
+ result.byNode.clear();
524
+ } finally {
525
+ if (useH) ref.H = previous;
526
+ else ref.current = previous;
527
+ }
528
+ return result.byNode;
529
+ }
530
+
338
531
  // src/fiber.ts
339
532
  var FunctionComponent = 0;
340
533
  var ClassComponent = 1;
@@ -342,10 +535,12 @@ var HostRoot = 3;
342
535
  var HostComponent = 5;
343
536
  var ContextProvider = 10;
344
537
  var ForwardRef = 11;
538
+ var SuspenseComponent = 13;
345
539
  var MemoComponent = 14;
346
540
  var SimpleMemoComponent = 15;
347
541
  var PerformedWork = 1;
348
542
  var HOOK = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
543
+ var nowMs = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
349
544
  function ensureDevtoolsHook() {
350
545
  const g = globalThis;
351
546
  let hook = g[HOOK];
@@ -374,7 +569,9 @@ function ensureDevtoolsHook() {
374
569
  return hook;
375
570
  }
376
571
  var capturedRenderers = /* @__PURE__ */ new Map();
572
+ var dispatcherRefs = /* @__PURE__ */ new Map();
377
573
  var INJECT_WRAPPED = /* @__PURE__ */ Symbol.for("rerender-lens.injectWrapped");
574
+ var ATTACHED = /* @__PURE__ */ Symbol.for("rerender-lens.attached");
378
575
  function pickRenderer(r) {
379
576
  const info = r ?? {};
380
577
  return { version: info.version, bundleType: info.bundleType, rendererPackageName: info.rendererPackageName };
@@ -385,20 +582,45 @@ function wrapInject(hook) {
385
582
  const original = hook.inject;
386
583
  hook.inject = function(renderer) {
387
584
  const id = original.call(this, renderer);
388
- capturedRenderers.set(typeof id === "number" ? id : capturedRenderers.size + 1, pickRenderer(renderer));
585
+ const key = typeof id === "number" ? id : capturedRenderers.size + 1;
586
+ capturedRenderers.set(key, pickRenderer(renderer));
587
+ const ref = renderer?.currentDispatcherRef;
588
+ if (ref && typeof ref === "object") dispatcherRefs.set(key, ref);
389
589
  return id;
390
590
  };
391
591
  h[INJECT_WRAPPED] = true;
392
592
  }
593
+ function getDispatcherRef() {
594
+ for (const ref of dispatcherRefs.values()) return ref;
595
+ const g = globalThis;
596
+ const hook = g[HOOK];
597
+ let found = null;
598
+ if (hook && hook.renderers && typeof hook.renderers.forEach === "function") {
599
+ hook.renderers.forEach((r) => {
600
+ const ref = r?.currentDispatcherRef;
601
+ if (!found && ref && typeof ref === "object") found = ref;
602
+ });
603
+ }
604
+ return found;
605
+ }
393
606
  function attach() {
394
607
  const hook = ensureDevtoolsHook();
395
608
  wrapInject(hook);
609
+ if (hook[ATTACHED]) return () => {
610
+ };
611
+ hook[ATTACHED] = true;
396
612
  const previous = hook.onCommitFiberRoot;
397
613
  const patched = function(id, root, priority, didError) {
614
+ const started = nowMs();
398
615
  try {
399
616
  onCommit(root, priorityLabel(priority));
400
617
  } catch (err) {
401
618
  warnOnce("commit", `failed to inspect a commit: ${String(err)}`);
619
+ } finally {
620
+ const spent = nowMs() - started;
621
+ const s = getState();
622
+ s.overheadMs += spent;
623
+ if (spent > s.maxCommitMs) s.maxCommitMs = spent;
402
624
  }
403
625
  if (typeof previous === "function") return previous.call(this, id, root, priority, didError);
404
626
  };
@@ -410,6 +632,7 @@ function attach() {
410
632
  };
411
633
  hook.onScheduleFiberRoot = patchedSchedule;
412
634
  return () => {
635
+ hook[ATTACHED] = false;
413
636
  if (hook.onCommitFiberRoot === patched) hook.onCommitFiberRoot = previous;
414
637
  if (hook.onScheduleFiberRoot === patchedSchedule) hook.onScheduleFiberRoot = previousSchedule;
415
638
  };
@@ -579,7 +802,7 @@ function ownerName(fiber) {
579
802
  }
580
803
  var isStateNode = (n) => !!n.queue && typeof n.queue.lastRenderedReducer === "function";
581
804
  var isStoreNode = (n) => !!n.queue && typeof n.queue.getSnapshot === "function";
582
- function hookLabel(node) {
805
+ function hookLabel2(node) {
583
806
  if (isStoreNode(node)) return "useSyncExternalStore";
584
807
  if (isStateNode(node)) {
585
808
  const r = node.queue.lastRenderedReducer;
@@ -588,20 +811,43 @@ function hookLabel(node) {
588
811
  return "state";
589
812
  }
590
813
  var NODELESS_HOOKS = /* @__PURE__ */ new Set(["useContext", "useDebugValue", "use"]);
814
+ function hookLabelsFor(fiber, first) {
815
+ const types = fiber._debugHookTypes?.filter((t) => !NODELESS_HOOKS.has(t));
816
+ let nodeCount = 0;
817
+ for (let n = first; n; n = n.next) nodeCount++;
818
+ return types && types.length === nodeCount ? types : null;
819
+ }
820
+ var isHookList = (v) => !!v && typeof v === "object" && "next" in v;
821
+ function snapshotHooks(fiber) {
822
+ const out = [];
823
+ if (fiber.tag === ClassComponent) return out;
824
+ const first = fiber.memoizedState;
825
+ if (!isHookList(first)) return out;
826
+ const labels = hookLabelsFor(fiber, first);
827
+ let i = 0;
828
+ for (let n = first; n; n = n.next, i++) {
829
+ if (!isStateNode(n) && !isStoreNode(n)) continue;
830
+ const hook = labels?.[i] ?? hookLabel2(n);
831
+ out.push({ path: `${hook}#${i}`, hook, index: i, value: n.memoizedState });
832
+ }
833
+ return out;
834
+ }
835
+ function snapshotContexts(fiber) {
836
+ const out = [];
837
+ for (let d = fiber.dependencies?.firstContext ?? null; d; d = d.next) out.push({ name: d.context.displayName ?? "Context", value: d.memoizedValue });
838
+ return out;
839
+ }
591
840
  function diffHooks(fiber, alt) {
592
841
  const out = [];
593
842
  if (fiber.tag === ClassComponent) return out;
594
843
  let a = alt.memoizedState;
595
844
  let b = fiber.memoizedState;
596
845
  if (!a || !b || typeof b !== "object" || !("next" in b)) return out;
597
- const types = fiber._debugHookTypes?.filter((t) => !NODELESS_HOOKS.has(t));
598
- let nodeCount = 0;
599
- for (let n = b; n; n = n.next) nodeCount++;
600
- const labels = types && types.length === nodeCount ? types : null;
846
+ const labels = hookLabelsFor(fiber, b);
601
847
  let i = 0;
602
848
  while (a && b) {
603
849
  if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
604
- const hook = labels?.[i] ?? hookLabel(b);
850
+ const hook = labels?.[i] ?? hookLabel2(b);
605
851
  out.push({
606
852
  path: `${hook}#${i}`,
607
853
  hook,
@@ -690,14 +936,14 @@ function analyze(fiber, alt, trackHooks) {
690
936
  const trigger = causes.length === 0 ? "parent" : causes.length === 1 ? causes[0] : "mixed";
691
937
  return { propChanges, stateChanges, hookChanges, trigger };
692
938
  }
693
- function nearestRenderedAncestor(fiber, cache, trackHooks) {
939
+ function nearestRenderedAncestor(fiber, cache2, trackHooks) {
694
940
  let f = fiber.return;
695
941
  while (f && f.tag !== HostRoot) {
696
942
  if (isComponentTag(f.tag) && f.alternate && didRender(f)) {
697
- let info = cache.get(f);
943
+ let info = cache2.get(f);
698
944
  if (info === void 0) {
699
945
  info = { name: fiberName(f), trigger: analyze(f, f.alternate, trackHooks).trigger };
700
- cache.set(f, info);
946
+ cache2.set(f, info);
701
947
  }
702
948
  return info;
703
949
  }
@@ -705,6 +951,21 @@ function nearestRenderedAncestor(fiber, cache, trackHooks) {
705
951
  }
706
952
  return null;
707
953
  }
954
+ function updatersOf(root) {
955
+ const set = root.memoizedUpdaters;
956
+ if (!set || typeof set.forEach !== "function") return [];
957
+ const names = [];
958
+ set.forEach((f) => {
959
+ const comp = nearestComponent(f);
960
+ if (comp) {
961
+ const name = fiberName(comp);
962
+ if (!names.includes(name)) names.push(name);
963
+ }
964
+ });
965
+ return names;
966
+ }
967
+ var lastCommit = null;
968
+ var EFFECT_LOOP_WINDOW_MS = 50;
708
969
  function onCommit(root, commitPriority) {
709
970
  const s = getState();
710
971
  if (!s.enabled) return;
@@ -712,28 +973,53 @@ function onCommit(root, commitPriority) {
712
973
  const o = s.options;
713
974
  const trackHooks = o.trackHooks !== false;
714
975
  const rendered = [];
976
+ const renderedNames = /* @__PURE__ */ new Set();
715
977
  let hot = false;
978
+ let suspenseResolved = false;
716
979
  const stack = [root.current];
717
980
  while (stack.length) {
718
981
  const fiber = stack.pop();
719
982
  const alt = fiber.alternate;
720
983
  if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
721
984
  if (isHotSwapped(fiber, alt)) hot = true;
985
+ renderedNames.add(fiberName(fiber));
722
986
  if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
723
987
  }
988
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
724
989
  if (!alt || fiber.child !== alt.child) {
725
990
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
726
991
  }
727
992
  }
728
993
  if (hot && o.ignoreHotReload !== false) return;
729
994
  rendered.reverse();
730
- if (rendered.length === 0) return;
995
+ const at = nowMs();
996
+ const updaters = updatersOf(root);
997
+ let commitCause;
998
+ let afterCommit;
999
+ if (lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1000
+ commitCause = "effect-after-commit";
1001
+ afterCommit = lastCommit.id;
1002
+ } else if (suspenseResolved) commitCause = "suspense-resolved";
1003
+ if (rendered.length === 0) {
1004
+ lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1005
+ return;
1006
+ }
731
1007
  const commitId = s.nextCommitId++;
1008
+ lastCommit = { id: commitId, at, rendered: renderedNames };
1009
+ const dispatcherRef = o.resolveHookNames ? getDispatcherRef() : null;
732
1010
  const parentCache = /* @__PURE__ */ new Map();
733
1011
  for (const fiber of rendered) {
734
1012
  const alt = fiber.alternate;
735
1013
  const a = analyze(fiber, alt, trackHooks);
736
1014
  const durations = durationsOf(fiber);
1015
+ let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
1016
+ if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
1017
+ const names = resolveHookNames(fiber, dispatcherRef);
1018
+ if (names.size) {
1019
+ for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
1020
+ if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
1021
+ }
1022
+ }
737
1023
  const report = buildReport({
738
1024
  component: fiberName(fiber),
739
1025
  instanceId: instanceId(fiber),
@@ -743,6 +1029,15 @@ function onCommit(root, commitPriority) {
743
1029
  propChanges: a.propChanges,
744
1030
  stateChanges: a.stateChanges,
745
1031
  hookChanges: a.hookChanges,
1032
+ ...o.includeState !== false ? {
1033
+ hookState,
1034
+ contexts: trackHooks ? snapshotContexts(fiber) : void 0,
1035
+ state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
1036
+ } : {},
1037
+ updaters,
1038
+ commitCause,
1039
+ afterCommit,
1040
+ key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
746
1041
  parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
747
1042
  owner: ownerName(fiber),
748
1043
  path: componentPath(fiber),
@@ -855,6 +1150,201 @@ function useWhyRerender(name, values, options) {
855
1150
  });
856
1151
  }
857
1152
 
1153
+ // src/fixes.ts
1154
+ var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
1155
+ var rootOf = (path) => path.split(/[.[]/)[0] || path;
1156
+ function fixesFor(r) {
1157
+ const out = [];
1158
+ const owner = r.owner || r.parent && r.parent.name || "?";
1159
+ const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
1160
+ if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
1161
+ out.push({
1162
+ kind: "memo",
1163
+ owner: r.component,
1164
+ target: r.component,
1165
+ prop: null,
1166
+ label: `Wrap <${r.component}> in React.memo`,
1167
+ detail: changes.length === 0 ? `<${r.component}> re-rendered with identical props because <${r.parent && r.parent.name || "its parent"}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`
1168
+ });
1169
+ }
1170
+ for (const c of r.propChanges) {
1171
+ if (!AVOIDABLE.has(c.kind)) continue;
1172
+ const root = rootOf(c.path);
1173
+ if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
1174
+ out.push({ kind: "children", owner, target: r.component, prop: "children", label: `memoize children of <${r.component}> in <${owner}>`, detail: `<${owner}> re-creates the children of <${r.component}> on every render.` });
1175
+ } else if (c.kind === "function") {
1176
+ out.push({ kind: "useCallback", owner, target: r.component, prop: root, label: `useCallback(${root}) in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.` });
1177
+ } else if (c.kind === "element") {
1178
+ out.push({ kind: "useMemoElement", owner, target: r.component, prop: root, label: `memoize element prop ${root} in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.` });
1179
+ } else {
1180
+ out.push({ kind: "useMemo", owner, target: r.component, prop: root, label: `useMemo(${root}) in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new ${Array.isArray(c.next) ? "array" : "object"} with the same contents on every render of <${owner}>.` });
1181
+ }
1182
+ }
1183
+ for (const c of [...r.stateChanges, ...r.hookChanges]) {
1184
+ const isContext = c.hook === "useContext";
1185
+ const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
1186
+ const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
1187
+ if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
1188
+ out.push({
1189
+ kind: "splitContext",
1190
+ owner: providerOwner || `${ctxName}.Provider`,
1191
+ target: r.component,
1192
+ prop: ctxName,
1193
+ label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
1194
+ detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`
1195
+ });
1196
+ continue;
1197
+ }
1198
+ if (!AVOIDABLE.has(c.kind)) continue;
1199
+ if (isContext) {
1200
+ out.push({ kind: "contextValue", owner: providerOwner || `${ctxName}.Provider`, target: r.component, prop: ctxName, label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`, detail: `${ctxName} produced a new value that is deep-equal to the previous one.` });
1201
+ } else if (c.hook === "useSyncExternalStore") {
1202
+ const chain = c.custom || [];
1203
+ const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
1204
+ const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
1205
+ out.push({ kind: "storeSnapshot", owner: r.component, target: r.component, prop: c.path, label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`, detail: storeAdvice(c.custom) });
1206
+ } else {
1207
+ out.push({ kind: "bailout", owner: r.component, target: r.component, prop: c.path, label: `bail out before setting ${c.path} in <${r.component}>`, detail: `${c.path} was set to a value deep-equal to the current one (new reference).` });
1208
+ }
1209
+ }
1210
+ return out;
1211
+ }
1212
+ var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
1213
+ function rankFixes(reports) {
1214
+ const byKey = /* @__PURE__ */ new Map();
1215
+ for (const r of reports) {
1216
+ if (!r.avoidable) continue;
1217
+ for (const f of fixesFor(r)) {
1218
+ const key = fixKey(f);
1219
+ let agg = byKey.get(key);
1220
+ if (!agg) {
1221
+ agg = { ...f, key, count: 0, components: {} };
1222
+ byKey.set(key, agg);
1223
+ }
1224
+ agg.count++;
1225
+ agg.components[r.component] = (agg.components[r.component] || 0) + 1;
1226
+ }
1227
+ }
1228
+ return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
1229
+ }
1230
+ function formatFixes(reports, limit = 10) {
1231
+ const ranked = rankFixes(reports);
1232
+ if (!ranked.length) return "No avoidable re-renders.";
1233
+ const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
1234
+ if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1235
+ return lines.join("\n");
1236
+ }
1237
+
1238
+ // src/sessions.ts
1239
+ function summarizeReports(reports, meta = {}) {
1240
+ var _a;
1241
+ const byComponent = {};
1242
+ let avoidable = 0;
1243
+ let wasted = 0;
1244
+ for (const r of reports) {
1245
+ const c = byComponent[_a = r.component] || (byComponent[_a] = { total: 0, avoidable: 0, wasted: 0 });
1246
+ c.total++;
1247
+ if (r.avoidable) {
1248
+ c.avoidable++;
1249
+ avoidable++;
1250
+ if (typeof r.selfDuration === "number") {
1251
+ c.wasted += r.selfDuration;
1252
+ wasted += r.selfDuration;
1253
+ }
1254
+ }
1255
+ }
1256
+ return {
1257
+ id: meta.id ?? `s${Date.now().toString(36)}`,
1258
+ name: meta.name ?? "session",
1259
+ startedAt: meta.startedAt ?? (reports[0]?.time ?? 0),
1260
+ endedAt: meta.endedAt ?? (reports[reports.length - 1]?.time ?? null),
1261
+ total: reports.length,
1262
+ avoidable,
1263
+ wasted,
1264
+ byComponent,
1265
+ fixes: rankFixes(reports).map((f) => ({ key: f.key, label: f.label, count: f.count }))
1266
+ };
1267
+ }
1268
+ function compareSummaries(before, after) {
1269
+ const names = /* @__PURE__ */ new Set([...Object.keys(before.byComponent), ...Object.keys(after.byComponent)]);
1270
+ const rows = [];
1271
+ for (const component of names) {
1272
+ const b = before.byComponent[component]?.avoidable || 0;
1273
+ const a = after.byComponent[component]?.avoidable || 0;
1274
+ if (b || a) rows.push({ component, before: b, after: a, delta: a - b });
1275
+ }
1276
+ rows.sort((x, y) => x.delta - y.delta || y.before - x.before || x.component.localeCompare(y.component));
1277
+ const afterKeys = new Set(after.fixes.map((f) => f.key));
1278
+ const beforeKeys = new Set(before.fixes.map((f) => f.key));
1279
+ return {
1280
+ before,
1281
+ after,
1282
+ rows,
1283
+ total: { before: before.total, after: after.total, delta: after.total - before.total },
1284
+ avoidable: { before: before.avoidable, after: after.avoidable, delta: after.avoidable - before.avoidable },
1285
+ wasted: { before: before.wasted, after: after.wasted, delta: after.wasted - before.wasted },
1286
+ resolvedFixes: before.fixes.filter((f) => !afterKeys.has(f.key)),
1287
+ newFixes: after.fixes.filter((f) => !beforeKeys.has(f.key)),
1288
+ regressions: rows.filter((r) => r.delta > 0)
1289
+ };
1290
+ }
1291
+ function formatComparison(c) {
1292
+ const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
1293
+ const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
1294
+ const delta = (n, suffix = "") => n === 0 ? "\xB10" : `${n > 0 ? "+" : ""}${Number.isInteger(n) ? n : n.toFixed(1)}${suffix}`;
1295
+ const out = [line("avoidable", c.before.name.slice(0, 8), c.after.name.slice(0, 8), "\u0394"), line("all components", c.avoidable.before, c.avoidable.after, delta(c.avoidable.delta))];
1296
+ if (c.wasted.before || c.wasted.after) out.push(line("wasted ms", c.wasted.before.toFixed(1), c.wasted.after.toFixed(1), delta(Number(c.wasted.delta.toFixed(1)))));
1297
+ for (const r of c.rows) out.push(line(`<${r.component}>`, r.before, r.after, delta(r.delta)));
1298
+ if (c.resolvedFixes.length) out.push("", "no longer needed:", ...c.resolvedFixes.map((f) => ` - ${f.label}`));
1299
+ if (c.newFixes.length) out.push("", "new:", ...c.newFixes.map((f) => ` - ${f.label}`));
1300
+ return out.join("\n");
1301
+ }
1302
+ function parseExport(data) {
1303
+ if (Array.isArray(data)) return { reports: data, sessions: [], summary: null };
1304
+ if (data && typeof data === "object") {
1305
+ const d = data;
1306
+ if (Array.isArray(d.reports)) return { reports: d.reports, sessions: Array.isArray(d.sessions) ? d.sessions : [], summary: null };
1307
+ if (d.byComponent && typeof d.byComponent === "object") return { reports: [], sessions: [], summary: data };
1308
+ }
1309
+ throw new Error("not a rerender-lens export or session summary");
1310
+ }
1311
+
1312
+ // src/budget.ts
1313
+ function avoidableCounts(reports) {
1314
+ const counts2 = {};
1315
+ for (const r of reports) if (r.avoidable) counts2[r.component] = (counts2[r.component] || 0) + 1;
1316
+ return counts2;
1317
+ }
1318
+ function checkBudget(reports, budget) {
1319
+ const b = typeof budget === "number" ? { "*": budget } : budget;
1320
+ const fallback = typeof b["*"] === "number" ? b["*"] : 0;
1321
+ const counts2 = avoidableCounts(reports);
1322
+ const violations = [];
1323
+ const slack = [];
1324
+ const names = /* @__PURE__ */ new Set([...Object.keys(counts2), ...Object.keys(b).filter((k) => k !== "*")]);
1325
+ for (const component of names) {
1326
+ const allowed = typeof b[component] === "number" ? b[component] : fallback;
1327
+ const avoidable = counts2[component] || 0;
1328
+ if (avoidable > allowed) violations.push({ component, avoidable, allowed });
1329
+ else if (avoidable < allowed) slack.push({ component, avoidable, allowed });
1330
+ }
1331
+ violations.sort((x, y) => y.avoidable - y.allowed - (x.avoidable - x.allowed));
1332
+ return { ok: violations.length === 0, violations, slack, counts: counts2 };
1333
+ }
1334
+ function toBudget(reports) {
1335
+ return { "*": 0, ...avoidableCounts(reports) };
1336
+ }
1337
+ function assertWithinBudget(reports, budget) {
1338
+ const result = checkBudget(reports, budget);
1339
+ if (result.ok) return result;
1340
+ const lines = result.violations.map((v) => ` <${v.component}>: ${v.avoidable} avoidable re-render${v.avoidable === 1 ? "" : "s"} (budget ${v.allowed})`);
1341
+ throw new Error(`Re-render budget exceeded:
1342
+ ${lines.join("\n")}
1343
+
1344
+ Fixes, most impact first:
1345
+ ${formatFixes(reports)}`);
1346
+ }
1347
+
858
1348
  // src/notifiers.ts
859
1349
  function createCollector() {
860
1350
  const reports = [];
@@ -875,8 +1365,14 @@ function createCollector() {
875
1365
  const lines = bad.map((r) => ` <${r.component}> render #${r.renderCount}:
876
1366
  ${r.reasons.map((x) => ` - ${x}`).join("\n")}`);
877
1367
  throw new Error(`${bad.length} avoidable re-render${bad.length === 1 ? "" : "s"} detected:
878
- ${lines.join("\n")}`);
879
- }
1368
+ ${lines.join("\n")}
1369
+
1370
+ Fixes, most impact first:
1371
+ ${formatFixes(reports)}`);
1372
+ },
1373
+ fixes: () => rankFixes(reports),
1374
+ summary: (name) => summarizeReports(reports, name ? { name } : {}),
1375
+ assertWithinBudget: (budget) => assertWithinBudget(reports, budget)
880
1376
  };
881
1377
  }
882
1378
  function combineNotifiers(...notifiers) {
@@ -974,11 +1470,12 @@ function flash(target, duration = 500) {
974
1470
  }
975
1471
 
976
1472
  // src/version.ts
977
- var VERSION = "0.2.0" ;
1473
+ var VERSION = "0.3.0" ;
978
1474
 
979
1475
  // src/devtools.ts
980
1476
  var DEVTOOLS_MARKER = "__rerenderLens";
981
1477
  var PROTOCOL_VERSION = 2;
1478
+ var DEFAULT_CHANNEL = "rerender-lens";
982
1479
  function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
983
1480
  if (value === null || value === void 0) return value;
984
1481
  const t = typeof value;
@@ -1029,7 +1526,7 @@ var stringToMatcher = (s) => {
1029
1526
  };
1030
1527
  function serializeOptions(o) {
1031
1528
  const out = {};
1032
- const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "logAll", "silent", "collapse", "ignoreHotReload"];
1529
+ const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "includeState", "resolveHookNames", "logAll", "silent", "collapse", "ignoreHotReload"];
1033
1530
  for (const k of bools) if (typeof o[k] === "boolean") out[k] = o[k];
1034
1531
  if (typeof o.maxReportsPerComponent === "number") out.maxReportsPerComponent = o.maxReportsPerComponent;
1035
1532
  if (o.include) out.include = o.include.map(matcherToString).filter((x) => x !== null);
@@ -1038,7 +1535,7 @@ function serializeOptions(o) {
1038
1535
  }
1039
1536
  function deserializeOptions(o) {
1040
1537
  const out = {};
1041
- const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "logAll", "silent", "collapse", "ignoreHotReload"];
1538
+ const bools = ["trackAllMemoized", "trackAllComponents", "trackHooks", "includeState", "resolveHookNames", "logAll", "silent", "collapse", "ignoreHotReload"];
1042
1539
  for (const k of bools) if (typeof o[k] === "boolean") out[k] = o[k];
1043
1540
  if (typeof o.maxReportsPerComponent === "number") out.maxReportsPerComponent = o.maxReportsPerComponent;
1044
1541
  if (Array.isArray(o.include)) out.include = o.include.filter((s) => typeof s === "string" && s).map(stringToMatcher);
@@ -1069,7 +1566,8 @@ function createDevtoolsNotifier(options = {}) {
1069
1566
  source,
1070
1567
  injected: typeof window !== "undefined" && typeof window.__RERENDER_LENS_INJECTED__ === "string",
1071
1568
  scheduled: getState().scheduled,
1072
- commits: getState().commits
1569
+ commits: getState().commits,
1570
+ overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs }
1073
1571
  });
1074
1572
  const bridge = {
1075
1573
  replay: () => {
@@ -1134,12 +1632,126 @@ function createDevtoolsNotifier(options = {}) {
1134
1632
  }
1135
1633
  };
1136
1634
  if (typeof window !== "undefined") window.__RERENDER_LENS_DEVTOOLS__ = bridge;
1137
- post("hello", info());
1635
+ const runCommand = (data) => {
1636
+ const reply = { __rerenderLensReply: true, id: data.id };
1637
+ try {
1638
+ switch (data.cmd) {
1639
+ case "info":
1640
+ reply.result = info();
1641
+ break;
1642
+ case "pull":
1643
+ reply.result = bridge.pull(typeof data.arg === "number" ? data.arg : 0);
1644
+ break;
1645
+ case "replay":
1646
+ bridge.replay();
1647
+ reply.result = true;
1648
+ break;
1649
+ case "clear":
1650
+ bridge.clear();
1651
+ reply.result = true;
1652
+ break;
1653
+ case "configure":
1654
+ reply.result = bridge.configure(data.arg ?? {});
1655
+ break;
1656
+ case "highlight":
1657
+ reply.result = bridge.highlight(typeof data.arg === "number" ? data.arg : null);
1658
+ break;
1659
+ case "flash":
1660
+ bridge.flashAvoidable(!!data.arg);
1661
+ reply.result = true;
1662
+ break;
1663
+ default:
1664
+ reply.error = `unknown command ${String(data.cmd)}`;
1665
+ }
1666
+ } catch (e) {
1667
+ reply.error = String(e.message || e);
1668
+ }
1669
+ return reply;
1670
+ };
1671
+ const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
1672
+ const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
1673
+ const sinks = [];
1674
+ if (options.channel && typeof BroadcastChannel === "function") {
1675
+ const name = typeof options.channel === "string" ? options.channel : DEFAULT_CHANNEL;
1676
+ try {
1677
+ const channel = new BroadcastChannel(name);
1678
+ channel.onmessage = (event) => {
1679
+ if (isCommand(event.data)) channel.postMessage(runCommand(event.data));
1680
+ };
1681
+ sinks.push((message) => {
1682
+ try {
1683
+ channel.postMessage(message);
1684
+ } catch {
1685
+ }
1686
+ });
1687
+ } catch {
1688
+ }
1689
+ }
1690
+ const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
1691
+ const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
1692
+ if (relayUrl && ES && typeof fetch === "function") {
1693
+ const base = relayUrl.replace(/\/$/, "");
1694
+ let queue = [];
1695
+ let scheduled = false;
1696
+ const flush = () => {
1697
+ scheduled = false;
1698
+ const batch = queue;
1699
+ queue = [];
1700
+ fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
1701
+ });
1702
+ };
1703
+ const relaySend = (message) => {
1704
+ queue.push(message);
1705
+ if (!scheduled) {
1706
+ scheduled = true;
1707
+ setTimeout(flush, 0);
1708
+ }
1709
+ };
1710
+ try {
1711
+ const stream = new ES(`${base}/events?role=app`);
1712
+ stream.onmessage = (event) => {
1713
+ let parsed;
1714
+ try {
1715
+ parsed = JSON.parse(event.data);
1716
+ } catch {
1717
+ return;
1718
+ }
1719
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) if (isCommand(m)) relaySend(runCommand(m));
1720
+ };
1721
+ stream.onerror = () => {
1722
+ };
1723
+ sinks.push(relaySend);
1724
+ } catch {
1725
+ }
1726
+ }
1727
+ const postAll = (type, payload) => {
1728
+ post(type, payload);
1729
+ if (sinks.length) {
1730
+ const message = envelope(type, payload);
1731
+ for (const sink of sinks) sink(message);
1732
+ }
1733
+ };
1734
+ if (sinks.length) {
1735
+ const replay = bridge.replay;
1736
+ const clear = bridge.clear;
1737
+ bridge.replay = () => {
1738
+ for (const sink of sinks) {
1739
+ sink(envelope("hello", info()));
1740
+ for (const e of buffer) sink(envelope("report", e.payload));
1741
+ }
1742
+ replay();
1743
+ };
1744
+ bridge.clear = () => {
1745
+ clear();
1746
+ for (const sink of sinks) sink(envelope("clear"));
1747
+ };
1748
+ }
1749
+ postAll("hello", info());
1138
1750
  return (report) => {
1139
1751
  const payload = serialize(report, maxDepth);
1140
1752
  buffer.push({ seq: ++seq, instanceId: report.instanceId, payload });
1141
1753
  if (buffer.length > bufferSize) buffer.splice(0, buffer.length - bufferSize);
1142
- post("report", payload);
1754
+ postAll("report", payload);
1143
1755
  if (flashOn && report.avoidable && report.instanceId) {
1144
1756
  const fiber = fiberById(report.instanceId);
1145
1757
  if (fiber) flash({ nodes: hostNodesOf(fiber), label: report.component });
@@ -1147,6 +1759,6 @@ function createDevtoolsNotifier(options = {}) {
1147
1759
  };
1148
1760
  }
1149
1761
 
1150
- export { DEVTOOLS_MARKER, MARKER, PROTOCOL_VERSION, VERSION, buildReport, classify, combineNotifiers, configure, createCollector, createDevtoolsNotifier, deepEqual, deserializeOptions, diffRecords, disable, ensureDevtoolsHook, getDisplayName, getRenderers, init, isEnabled, isProductionReact, printReport, serialize, serializeOptions, shouldTrack, summarize, track, useWhyRerender };
1762
+ export { DEFAULT_CHANNEL, DEVTOOLS_MARKER, MARKER, PROTOCOL_VERSION, VERSION, assertWithinBudget, avoidableCounts, buildReport, checkBudget, classify, combineNotifiers, compareSummaries, configure, createCollector, createDevtoolsNotifier, customHooksFromStack, deepEqual, deserializeOptions, diffRecords, disable, ensureDevtoolsHook, fixKey, fixesFor, formatComparison, formatFixes, getDisplayName, getRenderers, hookLabel, init, isEnabled, isProductionReact, parseExport, printReport, rankFixes, resolveHookNames, serialize, serializeOptions, shouldTrack, storeAdvice, summarize, summarizeReports, toBudget, track, useWhyRerender };
1151
1763
  //# sourceMappingURL=index.js.map
1152
1764
  //# sourceMappingURL=index.js.map