rerender-lens 0.5.0 → 0.7.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/CHANGELOG.md +50 -1
- package/README.md +23 -4
- package/dist/{budget-CRswn-nC.d.cts → budget-CA1MS7lB.d.cts} +1 -1
- package/dist/{budget-BnJlYwrV.d.ts → budget-ok1VDJ4t.d.ts} +1 -1
- package/dist/cli.cjs +78 -26
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +78 -26
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-BYWat7nQ.d.ts → devtools-Cl5n_52v.d.ts} +11 -2
- package/dist/{devtools-DJi_6AfJ.d.cts → devtools-UwQx2VYS.d.cts} +11 -2
- package/dist/index.cjs +99 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +99 -10
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +20 -3
- package/dist/jest-setup.cjs.map +1 -1
- package/dist/jest-setup.d.cts +4 -4
- package/dist/jest-setup.d.ts +4 -4
- package/dist/jest-setup.js +20 -3
- package/dist/jest-setup.js.map +1 -1
- package/dist/jest.cjs +20 -3
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +4 -4
- package/dist/jest.d.ts +4 -4
- package/dist/jest.js +20 -3
- package/dist/jest.js.map +1 -1
- package/dist/{notifiers-DwKlvv4H.d.cts → notifiers-B5poAEvE.d.cts} +2 -2
- package/dist/{notifiers-FNzKHs0a.d.ts → notifiers-CqQW0Eum.d.ts} +2 -2
- package/dist/playwright.d.cts +4 -4
- package/dist/playwright.d.ts +4 -4
- package/dist/relay.cjs +65 -21
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.d.cts +25 -3
- package/dist/relay.d.ts +25 -3
- package/dist/relay.js +65 -21
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +105 -12
- package/dist/{runner-report-CW8X5RwK.d.ts → runner-report-Bvqoe89M.d.ts} +2 -2
- package/dist/{runner-report-B8Sc3FZO.d.cts → runner-report-CvpDg2zt.d.cts} +2 -2
- package/dist/setup.cjs +98 -9
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +98 -9
- package/dist/setup.js.map +1 -1
- package/dist/{types-DW5-N2XH.d.cts → types-DwNN5cWe.d.cts} +28 -1
- package/dist/{types-DW5-N2XH.d.ts → types-DwNN5cWe.d.ts} +28 -1
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +20 -3
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.d.cts +4 -4
- package/dist/vitest-setup.d.ts +4 -4
- package/dist/vitest-setup.js +20 -3
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +20 -3
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +5 -5
- package/dist/vitest.d.ts +5 -5
- package/dist/vitest.js +20 -3
- package/dist/vitest.js.map +1 -1
- package/package.json +3 -2
- package/panel/panel.css +48 -0
- package/panel/panel.js +348 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rerender-lens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Find avoidable React re-renders and what caused them. Reads the fiber tree like React DevTools, so nothing is patched: works with Fast Refresh, memo, forwardRef and classes. Structured reports, test assertions, and a DevTools bridge.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -149,7 +149,8 @@
|
|
|
149
149
|
"test:jest": "node scripts/jest-check.mjs",
|
|
150
150
|
"test:react18": "npm install --prefix .react18 --no-save --no-package-lock --no-audit --no-fund react@18 react-dom@18 && vitest run --config test/react-compat.config.ts",
|
|
151
151
|
"store:media": "node scripts/store-media.mjs",
|
|
152
|
-
"test:react17": "npm install --prefix .react17 --no-save --no-package-lock --no-audit --no-fund react@17 react-dom@17 && REACT_COMPAT_DIR=.react17/node_modules vitest run --config test/react-compat.config.ts"
|
|
152
|
+
"test:react17": "npm install --prefix .react17 --no-save --no-package-lock --no-audit --no-fund react@17 react-dom@17 && REACT_COMPAT_DIR=.react17/node_modules vitest run --config test/react-compat.config.ts",
|
|
153
|
+
"e2e:perf": "playwright test -c e2e/perf.config.ts"
|
|
153
154
|
},
|
|
154
155
|
"peerDependencies": {
|
|
155
156
|
"react": ">=16.8.0",
|
package/panel/panel.css
CHANGED
|
@@ -105,6 +105,11 @@ button.ib.active .glyph { color: var(--accent); }
|
|
|
105
105
|
.status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 18%, transparent); }
|
|
106
106
|
.status.connected .dot { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 18%, transparent); }
|
|
107
107
|
.status.partial .dot { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 18%, transparent); }
|
|
108
|
+
.app-picker {
|
|
109
|
+
height: 22px; max-width: 200px; padding: 0 4px; font-size: 11px; color: var(--text-dim);
|
|
110
|
+
background: var(--bg-inset); border: 1px solid var(--border); border-radius: 10px;
|
|
111
|
+
}
|
|
112
|
+
.app-picker:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
108
113
|
.tab-chip {
|
|
109
114
|
max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
110
115
|
padding: 2px 8px; border-radius: 10px; background: var(--bg-inset); color: var(--text-dim); font-size: 11px;
|
|
@@ -123,6 +128,43 @@ button.ib.active .glyph { color: var(--accent); }
|
|
|
123
128
|
.summary button.stat { height: 22px; padding: 0 6px; margin: 0 -6px; }
|
|
124
129
|
.summary button.stat:hover { background: var(--hover); }
|
|
125
130
|
|
|
131
|
+
/* ---------- commit timeline strip ---------- */
|
|
132
|
+
.timeline { flex: none; background: var(--bg-alt); border-bottom: 1px solid var(--border); }
|
|
133
|
+
.timeline-header {
|
|
134
|
+
display: flex; align-items: center; gap: 6px; padding: 1px 12px; height: 17px;
|
|
135
|
+
color: var(--text-dim); cursor: pointer; user-select: none; font-size: 10px;
|
|
136
|
+
}
|
|
137
|
+
.timeline-header:hover { color: var(--text); }
|
|
138
|
+
.timeline-header .chevron { width: 9px; flex: none; }
|
|
139
|
+
.timeline-header .count { margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
140
|
+
.timeline-header .chip {
|
|
141
|
+
height: 15px; padding: 0 7px; font-size: 10px; border-radius: 8px;
|
|
142
|
+
border: 1px solid var(--accent); background: var(--accent-bg); color: var(--accent);
|
|
143
|
+
}
|
|
144
|
+
.timeline-header .chip:hover { background: var(--accent); color: var(--bg); }
|
|
145
|
+
.timeline-body { padding: 0 12px 2px; }
|
|
146
|
+
.timeline.collapsed .timeline-body { display: none; }
|
|
147
|
+
.timeline .bars {
|
|
148
|
+
display: flex; align-items: flex-end; gap: 1px; height: 26px; padding: 0 2px;
|
|
149
|
+
background: var(--bg-inset); border-radius: 3px; overflow: hidden; cursor: crosshair;
|
|
150
|
+
}
|
|
151
|
+
.timeline .bar {
|
|
152
|
+
flex: 1 1 0; min-width: 2px; height: 100%; padding: 0; margin: 0; display: block;
|
|
153
|
+
position: relative; align-self: flex-end; border: 0; border-radius: 1px 1px 0 0;
|
|
154
|
+
background: var(--ok); opacity: 0.65; cursor: pointer;
|
|
155
|
+
}
|
|
156
|
+
.timeline .bar:hover { background: var(--ok); opacity: 1; }
|
|
157
|
+
.timeline .bar .avoid { position: absolute; left: 0; right: 0; bottom: 0; background: var(--danger); }
|
|
158
|
+
.timeline .bar.in-window, .timeline .bar.brushing { opacity: 1; }
|
|
159
|
+
.timeline .bar.selected { opacity: 1; outline: 1px solid var(--accent); outline-offset: -1px; }
|
|
160
|
+
.timeline .bar:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; opacity: 1; }
|
|
161
|
+
.timeline-axis {
|
|
162
|
+
display: flex; justify-content: space-between; height: 10px; line-height: 10px;
|
|
163
|
+
pointer-events: none; font-size: 9px; color: var(--text-faint); font-variant-numeric: tabular-nums;
|
|
164
|
+
}
|
|
165
|
+
.compact .timeline-header, .compact .timeline-body { padding-left: 8px; padding-right: 8px; }
|
|
166
|
+
.compact .timeline .bars { height: 22px; }
|
|
167
|
+
|
|
126
168
|
/* ---------- banner, toast ---------- */
|
|
127
169
|
.banner { padding: 6px 12px; background: var(--warn-bg); color: var(--text); border-bottom: 1px solid var(--border); font-size: 11px; flex: none; }
|
|
128
170
|
.banner div { padding-left: 16px; position: relative; }
|
|
@@ -175,6 +217,12 @@ button.ib.active .glyph { color: var(--accent); }
|
|
|
175
217
|
@keyframes flash { from { box-shadow: inset 3px 0 0 var(--warn); background: var(--warn-bg); } to { box-shadow: none; background: transparent; } }
|
|
176
218
|
.empty { padding: 28px 18px; color: var(--text-dim); text-align: center; line-height: 1.6; }
|
|
177
219
|
.empty code { font-family: var(--mono); font-size: 11px; background: var(--bg-inset); padding: 1px 5px; border-radius: 4px; }
|
|
220
|
+
.empty .empty-head { color: var(--text); font-weight: 600; margin-bottom: 6px; }
|
|
221
|
+
.empty .empty-tracking { color: var(--text); background: var(--bg-inset); border-radius: 6px; padding: 6px 10px; margin: 0 auto 8px; display: inline-block; max-width: 46em; }
|
|
222
|
+
.empty-actions { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin-top: 10px; }
|
|
223
|
+
.empty-actions button { border: 1px solid var(--border); }
|
|
224
|
+
.empty-actions button.primary { background: var(--accent-bg); color: var(--accent); font-weight: 600; }
|
|
225
|
+
.empty-actions input.empty-match { border: 1px solid var(--border); border-radius: 4px; padding: 0 6px; height: 24px; background: var(--bg-inset); width: 12em; }
|
|
178
226
|
|
|
179
227
|
/* ---------- details ---------- */
|
|
180
228
|
.details { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--bg); }
|
package/panel/panel.js
CHANGED
|
@@ -577,6 +577,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
577
577
|
var ITEM_H = 20;
|
|
578
578
|
var OVERSCAN = 8;
|
|
579
579
|
var FALLBACK_VIEWPORT = 800;
|
|
580
|
+
var MAX_STRIP_BARS = 200;
|
|
581
|
+
var MIN_BAR_PCT = 8;
|
|
580
582
|
function el(tag, attrs, children) {
|
|
581
583
|
const node = document.createElement(tag);
|
|
582
584
|
if (attrs) {
|
|
@@ -602,6 +604,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
602
604
|
}
|
|
603
605
|
var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
|
|
604
606
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
607
|
+
var fmtSpan = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`;
|
|
608
|
+
var spokenAgo = (ms) => ms < 1e3 ? "just now" : ms < 6e4 ? `${Math.round(ms / 1e3)} seconds ago` : `${Math.round(ms / 6e4)} minutes ago`;
|
|
605
609
|
var componentList = (m) => (m instanceof Map ? [...m] : Object.entries(m)).map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
|
|
606
610
|
function changesOf(report) {
|
|
607
611
|
return [].concat(report.propChanges || [], report.stateChanges || [], report.hookChanges || []);
|
|
@@ -999,6 +1003,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
999
1003
|
reports: [],
|
|
1000
1004
|
commits: /* @__PURE__ */ new Map(),
|
|
1001
1005
|
commitOrder: [],
|
|
1006
|
+
apps: [],
|
|
1007
|
+
appId: null,
|
|
1002
1008
|
selectedKey: null,
|
|
1003
1009
|
selectedReport: null,
|
|
1004
1010
|
selectedCommit: null,
|
|
@@ -1024,7 +1030,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1024
1030
|
recording: null,
|
|
1025
1031
|
selectedSession: null,
|
|
1026
1032
|
compareWith: null,
|
|
1027
|
-
byInstance: false
|
|
1033
|
+
byInstance: false,
|
|
1034
|
+
timeWindow: null,
|
|
1035
|
+
stripCollapsed: false
|
|
1028
1036
|
};
|
|
1029
1037
|
let persistTimer = null;
|
|
1030
1038
|
let queue = [];
|
|
@@ -1084,6 +1092,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1084
1092
|
const settingsBtn = iconButton("\u2699", "Settings", "Settings", () => toggleSettings());
|
|
1085
1093
|
const status = el("span", { class: "status", title: "" }, [el("span", { class: "dot" }), el("span", { class: "status-text", text: "no page" })]);
|
|
1086
1094
|
const tabChip = el("span", { class: "tab-chip", hidden: true, title: "The tab this panel follows" });
|
|
1095
|
+
const appPicker = el("select", { class: "app-picker", hidden: true, title: "Which connected app this panel watches", "aria-label": "App" });
|
|
1096
|
+
appPicker.addEventListener("change", () => transport.selectApp?.(appPicker.value));
|
|
1087
1097
|
const undock = transport.undock ? [
|
|
1088
1098
|
el("span", { class: "sep" }),
|
|
1089
1099
|
iconButton("\u2AFF", "Side panel", "Show this panel next to the page (Chrome side panel)", () => void transport.undock("sidepanel").catch((e) => toast(String(e.message || e)))),
|
|
@@ -1105,11 +1115,31 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1105
1115
|
...undock,
|
|
1106
1116
|
el("span", { class: "spacer" }),
|
|
1107
1117
|
tabChip,
|
|
1118
|
+
appPicker,
|
|
1108
1119
|
status,
|
|
1109
1120
|
settingsBtn
|
|
1110
1121
|
]);
|
|
1111
1122
|
const summary = el("div", { class: "summary" });
|
|
1112
1123
|
const banner = el("div", { class: "banner", hidden: true });
|
|
1124
|
+
const stripChevron = el("span", { class: "chevron", text: "\u25BE" });
|
|
1125
|
+
const stripCount = el("span", { class: "count", text: "0 commits" });
|
|
1126
|
+
const stripChip = el("button", {
|
|
1127
|
+
class: "chip",
|
|
1128
|
+
hidden: true,
|
|
1129
|
+
title: "Clear the time window (Esc)",
|
|
1130
|
+
onclick: (e) => {
|
|
1131
|
+
e.stopPropagation();
|
|
1132
|
+
clearWindow();
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
const stripBars = el("div", { class: "bars" });
|
|
1136
|
+
const stripFrom = el("span", { class: "from", text: "" });
|
|
1137
|
+
const stripTo = el("span", { class: "to", text: "now" });
|
|
1138
|
+
const stripHeader = el("div", { class: "timeline-header", onclick: () => toggleStrip() }, [stripChevron, el("span", { class: "title", text: "Timeline" }), stripChip, stripCount]);
|
|
1139
|
+
const timeline = el("div", { class: "timeline", hidden: true, role: "group", "aria-label": "Commit timeline" }, [
|
|
1140
|
+
stripHeader,
|
|
1141
|
+
el("div", { class: "timeline-body" }, [stripBars, el("div", { class: "timeline-axis" }, [stripFrom, stripTo])])
|
|
1142
|
+
]);
|
|
1113
1143
|
const viewsBar = el("div", { class: "views" });
|
|
1114
1144
|
const VIEWS = [
|
|
1115
1145
|
["tree", "Tree"],
|
|
@@ -1172,7 +1202,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1172
1202
|
details.setAttribute("aria-label", "Details");
|
|
1173
1203
|
settings.setAttribute("role", "dialog");
|
|
1174
1204
|
settings.setAttribute("aria-label", "Settings");
|
|
1175
|
-
|
|
1205
|
+
stripBars.setAttribute("role", "group");
|
|
1206
|
+
stripBars.setAttribute("aria-label", "Commits over time; arrow keys move between commits, Enter selects, drag to filter every view to a time window");
|
|
1207
|
+
root.append(toolbar, summary, banner, timeline, main, stream, toastEl);
|
|
1176
1208
|
root.addEventListener("keydown", onGlobalKey);
|
|
1177
1209
|
function setCompact(on) {
|
|
1178
1210
|
if (state.compact === on) return;
|
|
@@ -1291,7 +1323,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1291
1323
|
collapsed: [...state.collapsed],
|
|
1292
1324
|
treeWidth: state.treeWidth,
|
|
1293
1325
|
flashOn: state.flashOn,
|
|
1294
|
-
byInstance: state.byInstance
|
|
1326
|
+
byInstance: state.byInstance,
|
|
1327
|
+
stripCollapsed: state.stripCollapsed
|
|
1295
1328
|
};
|
|
1296
1329
|
transport.storage.set("panel", saved);
|
|
1297
1330
|
}, 150);
|
|
@@ -1343,6 +1376,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1343
1376
|
instancesBtn.classList.toggle("active", state.byInstance);
|
|
1344
1377
|
rebuildTree();
|
|
1345
1378
|
}
|
|
1379
|
+
if (typeof saved.stripCollapsed === "boolean" && saved.stripCollapsed !== state.stripCollapsed) toggleStrip(saved.stripCollapsed);
|
|
1346
1380
|
if (saved.tab === "history" || saved.tab === "fix") state.tab = saved.tab;
|
|
1347
1381
|
if (saved.view && viewButtons.has(saved.view)) state.view = saved.view;
|
|
1348
1382
|
for (const n of state.nodesByKey.values()) n.expanded = !state.collapsed.has(n.key);
|
|
@@ -1498,6 +1532,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1498
1532
|
function renderHeavy(details2) {
|
|
1499
1533
|
heavyAt = Date.now();
|
|
1500
1534
|
if (state.view !== "tree") renderLeft();
|
|
1535
|
+
renderStrip();
|
|
1501
1536
|
if (details2) renderDetails();
|
|
1502
1537
|
}
|
|
1503
1538
|
function scheduleHeavy(details2) {
|
|
@@ -1544,12 +1579,14 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1544
1579
|
state.selectedCommit = null;
|
|
1545
1580
|
state.selectedFix = null;
|
|
1546
1581
|
state.selectedRoot = null;
|
|
1582
|
+
state.timeWindow = null;
|
|
1547
1583
|
if (state.tab === "commit" || state.tab === "fixlist" || state.tab === "root") state.tab = "latest";
|
|
1548
1584
|
queue = [];
|
|
1549
1585
|
renderLeft();
|
|
1550
1586
|
renderDetails();
|
|
1551
1587
|
renderStream();
|
|
1552
1588
|
renderSummary();
|
|
1589
|
+
renderStrip();
|
|
1553
1590
|
}
|
|
1554
1591
|
let filterCache = { filter: "", value: null, regex: null, text: "" };
|
|
1555
1592
|
function parsedFilter() {
|
|
@@ -1590,17 +1627,45 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1590
1627
|
}
|
|
1591
1628
|
return text.includes(q);
|
|
1592
1629
|
}
|
|
1630
|
+
const inWindow = (r) => {
|
|
1631
|
+
const w = state.timeWindow;
|
|
1632
|
+
return !w || r.receivedAt >= w.from && r.receivedAt <= w.to;
|
|
1633
|
+
};
|
|
1634
|
+
const windowKey = () => {
|
|
1635
|
+
const w = state.timeWindow;
|
|
1636
|
+
return w ? `${w.from}-${w.to}` : "";
|
|
1637
|
+
};
|
|
1638
|
+
const NO_COUNTS = { total: 0, avoidable: 0 };
|
|
1639
|
+
let windowNodes = null;
|
|
1640
|
+
function nodeCounts(node) {
|
|
1641
|
+
if (!state.timeWindow) return node;
|
|
1642
|
+
const key = `${dataGen}|${windowKey()}|${state.byInstance}`;
|
|
1643
|
+
if (!windowNodes || windowNodes.key !== key) {
|
|
1644
|
+
const map = /* @__PURE__ */ new Map();
|
|
1645
|
+
for (const r of state.reports) {
|
|
1646
|
+
if (!inWindow(r)) continue;
|
|
1647
|
+
const k = nodeOfReport(r).key;
|
|
1648
|
+
let c = map.get(k);
|
|
1649
|
+
if (!c) map.set(k, c = { total: 0, avoidable: 0 });
|
|
1650
|
+
c.total++;
|
|
1651
|
+
if (r.avoidable) c.avoidable++;
|
|
1652
|
+
}
|
|
1653
|
+
windowNodes = { key, map };
|
|
1654
|
+
}
|
|
1655
|
+
return windowNodes.map.get(node.key) || NO_COUNTS;
|
|
1656
|
+
}
|
|
1593
1657
|
function visible(node) {
|
|
1594
|
-
const
|
|
1658
|
+
const c = nodeCounts(node);
|
|
1659
|
+
const own = (!state.avoidableOnly || c.avoidable > 0) && matchesFilter(node.name) && c.total > 0 && (valueQuery() === null || node.reports.some(matchesValues));
|
|
1595
1660
|
if (own) return true;
|
|
1596
|
-
for (const
|
|
1661
|
+
for (const child of node.children.values()) if (visible(child)) return true;
|
|
1597
1662
|
return false;
|
|
1598
1663
|
}
|
|
1599
|
-
const passes = (r) => (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
|
|
1664
|
+
const passes = (r) => inWindow(r) && (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
|
|
1600
1665
|
let dataGen = 0;
|
|
1601
1666
|
let filtered = { key: "", reports: [], fixes: null };
|
|
1602
1667
|
function filteredReports() {
|
|
1603
|
-
const key = `${dataGen}|${state.avoidableOnly}|${state.filter}`;
|
|
1668
|
+
const key = `${dataGen}|${state.avoidableOnly}|${state.filter}|${windowKey()}`;
|
|
1604
1669
|
if (filtered.key !== key) filtered = { key, reports: state.reports.filter(passes), fixes: null };
|
|
1605
1670
|
return filtered.reports;
|
|
1606
1671
|
}
|
|
@@ -1618,6 +1683,154 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1618
1683
|
renderDetails();
|
|
1619
1684
|
persist();
|
|
1620
1685
|
}
|
|
1686
|
+
let bars = [];
|
|
1687
|
+
const barEls = /* @__PURE__ */ new Map();
|
|
1688
|
+
const barNodes = () => [...stripBars.children];
|
|
1689
|
+
function renderStrip() {
|
|
1690
|
+
const order = state.commitOrder;
|
|
1691
|
+
const next = [];
|
|
1692
|
+
let max = 1;
|
|
1693
|
+
for (let i = Math.max(0, order.length - MAX_STRIP_BARS); i < order.length; i++) {
|
|
1694
|
+
const key = order[i];
|
|
1695
|
+
const reports = state.commits.get(key);
|
|
1696
|
+
if (!reports || !reports.length) continue;
|
|
1697
|
+
let avoidable = 0;
|
|
1698
|
+
let from = Infinity;
|
|
1699
|
+
let to = 0;
|
|
1700
|
+
for (const r of reports) {
|
|
1701
|
+
if (r.avoidable) avoidable++;
|
|
1702
|
+
if (r.receivedAt < from) from = r.receivedAt;
|
|
1703
|
+
if (r.receivedAt > to) to = r.receivedAt;
|
|
1704
|
+
}
|
|
1705
|
+
next.push({ key, total: reports.length, avoidable, from, to });
|
|
1706
|
+
if (reports.length > max) max = reports.length;
|
|
1707
|
+
}
|
|
1708
|
+
bars = next;
|
|
1709
|
+
if (!bars.length) {
|
|
1710
|
+
timeline.hidden = true;
|
|
1711
|
+
stripBars.textContent = "";
|
|
1712
|
+
barEls.clear();
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
timeline.hidden = false;
|
|
1716
|
+
const now = bars[bars.length - 1].to;
|
|
1717
|
+
const win = state.timeWindow;
|
|
1718
|
+
const live = /* @__PURE__ */ new Set();
|
|
1719
|
+
const frag = document.createDocumentFragment();
|
|
1720
|
+
for (let i = 0; i < bars.length; i++) {
|
|
1721
|
+
const b = bars[i];
|
|
1722
|
+
live.add(b.key);
|
|
1723
|
+
let node = barEls.get(b.key);
|
|
1724
|
+
if (!node) {
|
|
1725
|
+
node = el("button", { class: "bar", type: "button", "data-commit": String(b.key) }, [el("span", { class: "avoid" })]);
|
|
1726
|
+
barEls.set(b.key, node);
|
|
1727
|
+
}
|
|
1728
|
+
node.setAttribute("data-index", String(i));
|
|
1729
|
+
node.style.height = `${Math.max(MIN_BAR_PCT, Math.round(100 * Math.sqrt(b.total / max)))}%`;
|
|
1730
|
+
node.firstElementChild.style.height = `${Math.round(100 * b.avoidable / b.total)}%`;
|
|
1731
|
+
node.classList.toggle("has-avoid", b.avoidable > 0);
|
|
1732
|
+
node.classList.toggle("selected", state.selectedCommit === b.key && state.tab === "commit");
|
|
1733
|
+
node.classList.toggle("in-window", !!win && b.to >= win.from && b.from <= win.to);
|
|
1734
|
+
node.setAttribute("aria-label", `commit ${b.key}, ${plural(b.total, "render")}, ${b.avoidable} avoidable, ${spokenAgo(Math.max(0, now - b.to))}`);
|
|
1735
|
+
node.title = `#${b.key} \xB7 ${plural(b.total, "render")} \xB7 ${b.avoidable} avoidable`;
|
|
1736
|
+
frag.append(node);
|
|
1737
|
+
}
|
|
1738
|
+
stripBars.textContent = "";
|
|
1739
|
+
stripBars.append(frag);
|
|
1740
|
+
for (const key of [...barEls.keys()]) if (!live.has(key)) barEls.delete(key);
|
|
1741
|
+
const span = now - bars[0].from;
|
|
1742
|
+
stripFrom.textContent = span > 0 ? `-${fmtSpan(span)}` : "0ms";
|
|
1743
|
+
stripTo.textContent = "now";
|
|
1744
|
+
stripCount.textContent = plural(bars.length, "commit") + (order.length > bars.length ? ` of ${order.length}` : "");
|
|
1745
|
+
if (win) {
|
|
1746
|
+
let n = 0;
|
|
1747
|
+
for (const r of state.reports) if (inWindow(r)) n++;
|
|
1748
|
+
stripChip.textContent = `${fmtSpan(win.to - win.from)} window, ${plural(n, "report")} \xB7 Clear`;
|
|
1749
|
+
stripChip.hidden = false;
|
|
1750
|
+
} else stripChip.hidden = true;
|
|
1751
|
+
}
|
|
1752
|
+
function toggleStrip(value) {
|
|
1753
|
+
state.stripCollapsed = value === void 0 ? !state.stripCollapsed : value;
|
|
1754
|
+
timeline.classList.toggle("collapsed", state.stripCollapsed);
|
|
1755
|
+
stripChevron.textContent = state.stripCollapsed ? "\u25B8" : "\u25BE";
|
|
1756
|
+
if (value === void 0) persist();
|
|
1757
|
+
}
|
|
1758
|
+
function setWindow(from, to) {
|
|
1759
|
+
state.timeWindow = from <= to ? { from, to } : { from: to, to: from };
|
|
1760
|
+
afterWindowChange();
|
|
1761
|
+
}
|
|
1762
|
+
function clearWindow() {
|
|
1763
|
+
if (!state.timeWindow) return;
|
|
1764
|
+
state.timeWindow = null;
|
|
1765
|
+
afterWindowChange();
|
|
1766
|
+
}
|
|
1767
|
+
function afterWindowChange() {
|
|
1768
|
+
renderLeft();
|
|
1769
|
+
renderStream();
|
|
1770
|
+
renderDetails();
|
|
1771
|
+
renderStrip();
|
|
1772
|
+
}
|
|
1773
|
+
const barAt = (target) => {
|
|
1774
|
+
const node = target && typeof target.closest === "function" ? target.closest(".bar") : null;
|
|
1775
|
+
const i = node ? Number(node.getAttribute("data-index")) : -1;
|
|
1776
|
+
return Number.isInteger(i) && i >= 0 && i < bars.length ? i : -1;
|
|
1777
|
+
};
|
|
1778
|
+
let brush = null;
|
|
1779
|
+
let swallowClick = false;
|
|
1780
|
+
const paintBrush = () => {
|
|
1781
|
+
const lo = brush ? Math.min(brush.a, brush.b) : -1;
|
|
1782
|
+
const hi = brush ? Math.max(brush.a, brush.b) : -2;
|
|
1783
|
+
const nodes = barNodes();
|
|
1784
|
+
for (let i = 0; i < nodes.length; i++) nodes[i].classList.toggle("brushing", i >= lo && i <= hi);
|
|
1785
|
+
};
|
|
1786
|
+
stripBars.addEventListener("mousedown", (e) => {
|
|
1787
|
+
swallowClick = false;
|
|
1788
|
+
const i = barAt(e.target);
|
|
1789
|
+
if (i < 0) return;
|
|
1790
|
+
brush = { a: i, b: i };
|
|
1791
|
+
});
|
|
1792
|
+
stripBars.addEventListener("mousemove", (e) => {
|
|
1793
|
+
if (!brush) return;
|
|
1794
|
+
const i = barAt(e.target);
|
|
1795
|
+
if (i < 0 || i === brush.b) return;
|
|
1796
|
+
brush.b = i;
|
|
1797
|
+
paintBrush();
|
|
1798
|
+
});
|
|
1799
|
+
window.addEventListener("mouseup", (e) => {
|
|
1800
|
+
if (!brush) return;
|
|
1801
|
+
const i = barAt(e.target);
|
|
1802
|
+
if (i >= 0) brush.b = i;
|
|
1803
|
+
const { a, b } = brush;
|
|
1804
|
+
brush = null;
|
|
1805
|
+
paintBrush();
|
|
1806
|
+
if (a === b) return;
|
|
1807
|
+
swallowClick = true;
|
|
1808
|
+
setWindow(bars[Math.min(a, b)].from, bars[Math.max(a, b)].to);
|
|
1809
|
+
});
|
|
1810
|
+
stripBars.addEventListener("click", (e) => {
|
|
1811
|
+
if (swallowClick) {
|
|
1812
|
+
swallowClick = false;
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const i = barAt(e.target);
|
|
1816
|
+
if (i >= 0) selectCommitBar(bars[i].key);
|
|
1817
|
+
});
|
|
1818
|
+
stripBars.addEventListener("keydown", (e) => {
|
|
1819
|
+
swallowClick = false;
|
|
1820
|
+
const key = e.key;
|
|
1821
|
+
if (key !== "ArrowLeft" && key !== "ArrowRight" && key !== "Home" && key !== "End") return;
|
|
1822
|
+
const nodes = barNodes();
|
|
1823
|
+
const i = nodes.indexOf(document.activeElement);
|
|
1824
|
+
if (i < 0) return;
|
|
1825
|
+
e.preventDefault();
|
|
1826
|
+
const to = key === "ArrowLeft" ? Math.max(0, i - 1) : key === "ArrowRight" ? Math.min(nodes.length - 1, i + 1) : key === "Home" ? 0 : nodes.length - 1;
|
|
1827
|
+
nodes[to].focus();
|
|
1828
|
+
});
|
|
1829
|
+
function selectCommitBar(key) {
|
|
1830
|
+
showCommit(key);
|
|
1831
|
+
if (state.view !== "commits") setView("commits");
|
|
1832
|
+
renderStrip();
|
|
1833
|
+
}
|
|
1621
1834
|
function renderLeft() {
|
|
1622
1835
|
if (state.view === "tree") renderTree();
|
|
1623
1836
|
else if (state.view === "offenders") renderOffenders();
|
|
@@ -1627,10 +1840,73 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1627
1840
|
}
|
|
1628
1841
|
const rowEls = /* @__PURE__ */ new Map();
|
|
1629
1842
|
const treeList = virtualList(tree, ROW_H, ({ node, depth }) => rowFor(node, depth));
|
|
1630
|
-
const emptyEl = el("div", { class: "empty" }
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1843
|
+
const emptyEl = el("div", { class: "empty" });
|
|
1844
|
+
function trackingSentence(t) {
|
|
1845
|
+
const what = t.mode === "all" ? "Tracking every component" : t.mode === "memoized" ? "Tracking every React.memo and PureComponent" : "Tracking only components marked with track()";
|
|
1846
|
+
const parts = [what];
|
|
1847
|
+
if (t.include.length) parts.push(`${t.mode === "marked" ? "and" : "plus"} names matching ${t.include.join(", ")}`);
|
|
1848
|
+
if (t.exclude.length) parts.push(`except ${t.exclude.join(", ")}`);
|
|
1849
|
+
const rendered = `${t.renderedCount}${t.overflow ? "+" : ""} component${t.renderedCount === 1 ? "" : "s"} rendered`;
|
|
1850
|
+
return `${parts.join(", ")}; ${rendered}, ${t.trackedCount} of them tracked.`;
|
|
1851
|
+
}
|
|
1852
|
+
function renderEmpty() {
|
|
1853
|
+
emptyEl.textContent = "";
|
|
1854
|
+
emptyEl.append(el("div", { class: "empty-head", text: "No re-renders reported yet." }));
|
|
1855
|
+
const t = state.library?.tracking;
|
|
1856
|
+
if (!t) {
|
|
1857
|
+
emptyEl.append(
|
|
1858
|
+
el("div", null, ["Call ", el("code", { text: "init({ notifier: createDevtoolsNotifier() })" }), " in the page, or enable injection in Settings, then interact with it."])
|
|
1859
|
+
);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
emptyEl.append(el("div", { class: "empty-tracking", text: trackingSentence(t) }));
|
|
1863
|
+
if (t.mode === "all") {
|
|
1864
|
+
emptyEl.append(el("div", { text: "Every component is tracked, so this is waiting for a re-render: interact with the page. The first render of a component is never reported." }));
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const untracked = t.renderedCount - t.trackedCount;
|
|
1868
|
+
emptyEl.append(
|
|
1869
|
+
el("div", {
|
|
1870
|
+
text: untracked > 0 ? `${untracked} component${untracked === 1 ? "" : "s"} rendered without being tracked. Widen what is tracked, or interact with the page if the tracked ones simply have not re-rendered yet.` : "Interact with the page: the first render of a component is never reported, only re-renders."
|
|
1871
|
+
})
|
|
1872
|
+
);
|
|
1873
|
+
if (!transport.configure) return;
|
|
1874
|
+
const trackAll = el("button", { class: "primary", onclick: () => void applyTracking({ trackAllComponents: true }) }, "Track every component");
|
|
1875
|
+
const match = el("input", { class: "empty-match", type: "text", placeholder: "Name or /regex/", "aria-label": "Track components matching" });
|
|
1876
|
+
const addMatch = () => {
|
|
1877
|
+
const value = match.value.trim();
|
|
1878
|
+
if (!value) return;
|
|
1879
|
+
const include = (state.library?.tracking?.include || []).slice();
|
|
1880
|
+
if (!include.includes(value)) include.push(value);
|
|
1881
|
+
match.value = "";
|
|
1882
|
+
void applyTracking({ include });
|
|
1883
|
+
};
|
|
1884
|
+
match.addEventListener("keydown", (e) => {
|
|
1885
|
+
if (e.key === "Enter") addMatch();
|
|
1886
|
+
});
|
|
1887
|
+
emptyEl.append(
|
|
1888
|
+
el("div", { class: "empty-actions" }, [trackAll, match, el("button", { onclick: addMatch }, "Track components matching\u2026")])
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
async function applyTracking(patch) {
|
|
1892
|
+
if (!transport.configure) return;
|
|
1893
|
+
try {
|
|
1894
|
+
const applied = await transport.configure(patch);
|
|
1895
|
+
if (state.library) {
|
|
1896
|
+
if (applied) state.library.options = applied;
|
|
1897
|
+
const t = state.library.tracking;
|
|
1898
|
+
if (t) {
|
|
1899
|
+
if (patch.trackAllComponents) t.mode = "all";
|
|
1900
|
+
if (patch.include) t.include = patch.include;
|
|
1901
|
+
}
|
|
1902
|
+
transport.storage?.set("settings", Object.assign({}, state.library.options));
|
|
1903
|
+
}
|
|
1904
|
+
toast("Applied");
|
|
1905
|
+
renderTree();
|
|
1906
|
+
} catch (e) {
|
|
1907
|
+
toast(`Failed: ${e.message}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1634
1910
|
function renderTree() {
|
|
1635
1911
|
const flat = [];
|
|
1636
1912
|
const walk = (node, depth) => {
|
|
@@ -1642,6 +1918,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1642
1918
|
};
|
|
1643
1919
|
walk(state.tree, 0);
|
|
1644
1920
|
if (flat.length === 0) {
|
|
1921
|
+
renderEmpty();
|
|
1645
1922
|
if (!emptyEl.parentNode) tree.append(emptyEl);
|
|
1646
1923
|
} else emptyEl.remove();
|
|
1647
1924
|
if (rowEls.size > flat.length * 2 + 64) {
|
|
@@ -1698,8 +1975,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1698
1975
|
chevron.textContent = node.expanded ? "\u25BE" : "\u25B8";
|
|
1699
1976
|
const badges = row.querySelector(".badges");
|
|
1700
1977
|
badges.textContent = "";
|
|
1701
|
-
|
|
1702
|
-
if (
|
|
1978
|
+
const counts = nodeCounts(node);
|
|
1979
|
+
if (counts.avoidable) badges.append(el("span", { class: "badge avoid", title: "avoidable re-renders", text: String(counts.avoidable) }));
|
|
1980
|
+
if (counts.total) badges.append(el("span", { class: "badge", title: "re-renders", text: String(counts.total) }));
|
|
1703
1981
|
if (node.flash) {
|
|
1704
1982
|
node.flash = false;
|
|
1705
1983
|
if (Date.now() - (node.flashAt || 0) < 1e3) {
|
|
@@ -1754,7 +2032,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1754
2032
|
toggleSettings(false);
|
|
1755
2033
|
return;
|
|
1756
2034
|
}
|
|
1757
|
-
if (inField)
|
|
2035
|
+
if (inField) {
|
|
2036
|
+
target.blur();
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
clearWindow();
|
|
1758
2040
|
return;
|
|
1759
2041
|
}
|
|
1760
2042
|
if (inField || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
@@ -1862,6 +2144,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
1862
2144
|
state.tab = "commit";
|
|
1863
2145
|
renderLeft();
|
|
1864
2146
|
renderDetails();
|
|
2147
|
+
renderStrip();
|
|
1865
2148
|
}
|
|
1866
2149
|
function showRoot(name) {
|
|
1867
2150
|
state.selectedRoot = name;
|
|
@@ -2438,6 +2721,16 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2438
2721
|
status.querySelector(".status-text").textContent = text;
|
|
2439
2722
|
tabChip.hidden = !state.tabLabel;
|
|
2440
2723
|
tabChip.textContent = state.tabLabel || "";
|
|
2724
|
+
appPicker.hidden = state.apps.length < 2;
|
|
2725
|
+
if (!appPicker.hidden) {
|
|
2726
|
+
const want = state.apps.map((a) => `${a.id}:${a.label}`).join("|");
|
|
2727
|
+
if (appPicker.dataset.apps !== want) {
|
|
2728
|
+
appPicker.dataset.apps = want;
|
|
2729
|
+
appPicker.textContent = "";
|
|
2730
|
+
for (const a of state.apps) appPicker.append(el("option", { value: a.id, text: a.label || a.id }));
|
|
2731
|
+
}
|
|
2732
|
+
if (state.appId) appPicker.value = state.appId;
|
|
2733
|
+
}
|
|
2441
2734
|
banner.textContent = "";
|
|
2442
2735
|
const warnings = [];
|
|
2443
2736
|
if (lib && typeof lib.protocol === "number" && lib.protocol > PROTOCOL) warnings.push(`The page runs a newer rerender-lens (protocol ${lib.protocol}) than this panel (${PROTOCOL}). Update the extension.`);
|
|
@@ -2457,6 +2750,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2457
2750
|
function setLibrary(info) {
|
|
2458
2751
|
state.library = info;
|
|
2459
2752
|
renderStatus();
|
|
2753
|
+
if (emptyEl.parentNode) renderEmpty();
|
|
2460
2754
|
if (state.settingsOpen) void renderSettings();
|
|
2461
2755
|
if (state.flashOn) transport.flashAvoidable?.(true);
|
|
2462
2756
|
}
|
|
@@ -2634,6 +2928,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
2634
2928
|
state.library = null;
|
|
2635
2929
|
renderStatus();
|
|
2636
2930
|
break;
|
|
2931
|
+
case "apps": {
|
|
2932
|
+
const p = isRecord(message.payload) ? message.payload : {};
|
|
2933
|
+
state.apps = Array.isArray(p.list) ? p.list : [];
|
|
2934
|
+
state.appId = typeof p.selected === "string" ? p.selected : null;
|
|
2935
|
+
renderStatus();
|
|
2936
|
+
break;
|
|
2937
|
+
}
|
|
2637
2938
|
case "hello":
|
|
2638
2939
|
if (isRecord(message.payload)) setLibrary(message.payload);
|
|
2639
2940
|
break;
|
|
@@ -3251,6 +3552,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3251
3552
|
function floodReports(n) {
|
|
3252
3553
|
const out = [];
|
|
3253
3554
|
const components = Math.max(10, Math.floor(n / 10));
|
|
3555
|
+
const spread = 9e4;
|
|
3556
|
+
const t0 = Date.now() - spread;
|
|
3254
3557
|
for (let i = 0; i < n; i++) {
|
|
3255
3558
|
const id = i % components;
|
|
3256
3559
|
const depth = 1 + id % 6;
|
|
@@ -3258,6 +3561,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3258
3561
|
for (let d = 1; d < depth; d++) path.push(`Section${(id * 7 + d) % 40}`);
|
|
3259
3562
|
const avoidable = id % 3 !== 0;
|
|
3260
3563
|
out.push({
|
|
3564
|
+
receivedAt: t0 + Math.round(i / Math.max(1, n - 1) * spread),
|
|
3261
3565
|
component: `Item${id}`,
|
|
3262
3566
|
path,
|
|
3263
3567
|
trigger: avoidable ? "parent" : "props",
|
|
@@ -3327,14 +3631,22 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3327
3631
|
return transport;
|
|
3328
3632
|
}
|
|
3329
3633
|
function createRelayClientTransport(relayUrl, ES = EventSource) {
|
|
3330
|
-
const
|
|
3634
|
+
const [rawBase, relayQuery = ""] = relayUrl.split("?");
|
|
3635
|
+
const base = (rawBase ?? "").replace(/\/$/, "");
|
|
3636
|
+
const relayToken = new URLSearchParams(relayQuery).get("token");
|
|
3637
|
+
const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
|
|
3331
3638
|
let listener = null;
|
|
3332
3639
|
let stream = null;
|
|
3333
3640
|
let appsOnline = null;
|
|
3641
|
+
let roster = [];
|
|
3642
|
+
let selected = null;
|
|
3334
3643
|
const emit = (m) => {
|
|
3335
3644
|
if (listener) listener(m);
|
|
3336
3645
|
};
|
|
3337
|
-
const post = (message) =>
|
|
3646
|
+
const post = (message) => {
|
|
3647
|
+
const body = selected && isRecord(message) && message.__rerenderLensCmd === true ? { ...message, app: selected } : message;
|
|
3648
|
+
return fetch(`${base}/message${auth ? `?${auth}` : ""}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }).then(() => void 0);
|
|
3649
|
+
};
|
|
3338
3650
|
const { bridge, reply } = commandBridge(post, 2e3);
|
|
3339
3651
|
const handleData = (data) => {
|
|
3340
3652
|
let parsed;
|
|
@@ -3346,12 +3658,21 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3346
3658
|
for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
3347
3659
|
if (!isRecord(m) || reply(m)) continue;
|
|
3348
3660
|
if (m.__rerenderLens === true && m.type === "relay") {
|
|
3349
|
-
const
|
|
3661
|
+
const p = isRecord(m.payload) ? m.payload : {};
|
|
3662
|
+
const apps = typeof p.apps === "number" ? p.apps : 0;
|
|
3663
|
+
roster = Array.isArray(p.list) ? p.list : [];
|
|
3350
3664
|
const wasOnline = appsOnline;
|
|
3665
|
+
const wasSelected = selected;
|
|
3351
3666
|
appsOnline = apps;
|
|
3352
|
-
if (
|
|
3667
|
+
if (!roster.length) selected = null;
|
|
3668
|
+
else if (!selected || !roster.some((a) => a.id === selected)) selected = roster[0].id;
|
|
3669
|
+
emit({ type: "apps", payload: { list: roster, selected } });
|
|
3670
|
+
if (apps > 0 && (wasOnline === null || wasOnline === 0 || selected !== wasSelected)) void attach();
|
|
3353
3671
|
else if (apps === 0) emit({ type: "disconnected" });
|
|
3354
|
-
} else if (m.__rerenderLens === true && typeof m.type === "string")
|
|
3672
|
+
} else if (m.__rerenderLens === true && typeof m.type === "string") {
|
|
3673
|
+
if (typeof m.app === "string" && selected && m.app !== selected) continue;
|
|
3674
|
+
emit({ type: m.type, version: typeof m.version === "number" ? m.version : void 0, payload: m.payload });
|
|
3675
|
+
}
|
|
3355
3676
|
}
|
|
3356
3677
|
};
|
|
3357
3678
|
async function attach() {
|
|
@@ -3368,10 +3689,17 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
|
3368
3689
|
const transport = {
|
|
3369
3690
|
origin: base,
|
|
3370
3691
|
tabLabel: `relay ${base.replace(/^https?:\/\//, "")}`,
|
|
3692
|
+
selectApp(id) {
|
|
3693
|
+
if (id === selected || !roster.some((a) => a.id === id)) return;
|
|
3694
|
+
selected = id;
|
|
3695
|
+
emit({ type: "apps", payload: { list: roster, selected } });
|
|
3696
|
+
emit({ type: "navigated" });
|
|
3697
|
+
void attach();
|
|
3698
|
+
},
|
|
3371
3699
|
subscribe(fn) {
|
|
3372
3700
|
listener = fn;
|
|
3373
3701
|
const open = () => {
|
|
3374
|
-
stream = new ES(`${base}/events?role=panel`);
|
|
3702
|
+
stream = new ES(`${base}/events?role=panel${auth ? `&${auth}` : ""}`);
|
|
3375
3703
|
stream.onmessage = (e) => handleData(e.data);
|
|
3376
3704
|
stream.onerror = () => {
|
|
3377
3705
|
emit({ type: "disconnected" });
|