@reticlehq/browser 2.10.0 → 2.11.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/dist/observers/network.js +59 -0
- package/dist/observers/subresources.d.ts +10 -0
- package/dist/observers/subresources.js +63 -0
- package/dist/presenter/presenter-controls.js +21 -8
- package/dist/presenter/presenter-drag.js +17 -14
- package/dist/presenter/presenter-shell.js +29 -11
- package/package.json +2 -2
|
@@ -256,6 +256,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
256
256
|
const contentType = res.headers.get('content-type');
|
|
257
257
|
// The request is done; the BODY may not be. Watch it so settle cannot pass mid-stream.
|
|
258
258
|
watchStreamedBody(emit, res, id, url, contentType, res.headers.get('content-length'));
|
|
259
|
+
reportedNetUrls.add(rawUrl);
|
|
259
260
|
const emitRequest = (responseBodyFields) => {
|
|
260
261
|
emit(EventType.NET_REQUEST, {
|
|
261
262
|
id,
|
|
@@ -385,6 +386,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
385
386
|
const cur = meta.get(this);
|
|
386
387
|
if (cur === undefined)
|
|
387
388
|
return;
|
|
389
|
+
reportedNetUrls.add(cur.rawUrl);
|
|
388
390
|
const xhrContentType = this.getResponseHeader('content-type');
|
|
389
391
|
let responseBodyFields = {};
|
|
390
392
|
// responseText throws unless responseType is '' or 'text' — guard before reading.
|
|
@@ -523,7 +525,64 @@ export function installNetwork(emit, opts = {}) {
|
|
|
523
525
|
};
|
|
524
526
|
navProto.sendBeacon = patchedBeacon;
|
|
525
527
|
}
|
|
528
|
+
// Document-initiated subresources (link/css/img/script/manifest): fetch and XHR patches never see
|
|
529
|
+
// these, so a `{net}` predicate over a favicon or manifest used to read `assertion_failed` — "your
|
|
530
|
+
// change is broken" — when the truthful answer is "not observable". A PerformanceObserver over
|
|
531
|
+
// `resource` entries reports them with no CDP. Status is the known gap: entries carry one only on
|
|
532
|
+
// newer Chromium, so the field is emitted ONLY when readable and the evaluation seam downgrades
|
|
533
|
+
// status assertions it cannot verify instead of guessing.
|
|
534
|
+
//
|
|
535
|
+
// Dedup: any URL the patched transports already reported (or later report) is skipped — resource
|
|
536
|
+
// timing also records fetch/XHR loads. The seen-set is filled eagerly here AND at every
|
|
537
|
+
// NET_REQUEST emit below, so both orders of arrival stay single-reported.
|
|
538
|
+
const reportedNetUrls = new Set();
|
|
539
|
+
let subresourceEvents = 0;
|
|
540
|
+
const SUBRESOURCE_CAP = 200;
|
|
541
|
+
let subresourceObserver;
|
|
542
|
+
if (typeof PerformanceObserver !== 'undefined') {
|
|
543
|
+
try {
|
|
544
|
+
const observer = new PerformanceObserver((list) => {
|
|
545
|
+
for (const raw of list.getEntries()) {
|
|
546
|
+
const entry = raw;
|
|
547
|
+
if (subresourceEvents >= SUBRESOURCE_CAP)
|
|
548
|
+
return;
|
|
549
|
+
const type = entry.initiatorType || 'other';
|
|
550
|
+
// Patched transports own their records; only document-initiated types are new here.
|
|
551
|
+
if ('fetch' === type || 'xmlhttprequest' === type)
|
|
552
|
+
continue;
|
|
553
|
+
const rawUrl = entry.name;
|
|
554
|
+
if (reportedNetUrls.has(rawUrl))
|
|
555
|
+
continue;
|
|
556
|
+
reportedNetUrls.add(rawUrl);
|
|
557
|
+
subresourceEvents += 1;
|
|
558
|
+
emit(EventType.NET_REQUEST, {
|
|
559
|
+
id: nextId(),
|
|
560
|
+
method: 'GET',
|
|
561
|
+
url: redactUrl(rawUrl),
|
|
562
|
+
durationMs: Math.round(entry.duration),
|
|
563
|
+
initiator: type,
|
|
564
|
+
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
|
565
|
+
// responseStatus is Chromium-only; omitted entirely when unreadable so the wire never
|
|
566
|
+
// carries a guessed status. Its absence is what the server-side seam reads as unknown.
|
|
567
|
+
...((entry.responseStatus ?? 0) > 0
|
|
568
|
+
? {
|
|
569
|
+
status: entry.responseStatus,
|
|
570
|
+
ok: (entry.responseStatus ?? 0) >= 200 && (entry.responseStatus ?? 0) < 400,
|
|
571
|
+
}
|
|
572
|
+
: {}),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
subresourceObserver = observer;
|
|
577
|
+
observer.observe({ type: 'resource', buffered: true });
|
|
578
|
+
// Disconnected in the disposer below.
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// No observer support: document-initiated loads stay unobserved, stated rather than guessed.
|
|
582
|
+
}
|
|
583
|
+
}
|
|
526
584
|
return () => {
|
|
585
|
+
subresourceObserver?.disconnect();
|
|
527
586
|
// Restore each slot ONLY if it still holds OUR wrapper. Between connect() and disconnect() the app
|
|
528
587
|
// (or Sentry/analytics/a router) may have wrapped fetch/XHR/EventSource/WebSocket/sendBeacon ON TOP
|
|
529
588
|
// of ours; blindly writing the original back would silently uninstall their instrumentation — the
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SubresourceObservation {
|
|
2
|
+
url: string;
|
|
3
|
+
method: string;
|
|
4
|
+
status?: number;
|
|
5
|
+
ok?: boolean;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
transferSize?: number;
|
|
8
|
+
initiatorType: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function collectSubresources(seenUrls: ReadonlySet<string>, nowMs: number, sinceMs: number): SubresourceObservation[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { redactUrl } from './network-redact.js';
|
|
2
|
+
/**
|
|
3
|
+
* Document-initiated subresource observation.
|
|
4
|
+
*
|
|
5
|
+
* fetch/XHR patches never see requests the DOCUMENT makes on its own: `<link rel=icon>`,
|
|
6
|
+
* `<link rel=manifest>`, stylesheets, `<img src>`, `<script src>`. Those are exactly the loads a
|
|
7
|
+
* branding change touches, so a `{net}` predicate over them used to return `assertion_failed`
|
|
8
|
+
* ("your change is broken") when the truthful answer was "not observable" — and an agent trusting
|
|
9
|
+
* that verdict goes and "fixes" working code.
|
|
10
|
+
*
|
|
11
|
+
* `performance.getEntriesByType('resource')` reports these with no CDP and no patching: URL (name),
|
|
12
|
+
* initiator type (`link`, `css`, `img`, `script`, …), duration, transfer size. The known limitation
|
|
13
|
+
* is the status code — resource timing does not expose one directly (`responseStatus` exists only
|
|
14
|
+
* in newer Chromium). Entries carry `status` ONLY when it could be read; predicates asserting a
|
|
15
|
+
* status over unreadable data are handled at the evaluation seam, which downgrades rather than
|
|
16
|
+
* guesses.
|
|
17
|
+
*
|
|
18
|
+
* Deduplication matters: a fetch/XHR-initiated request ALSO lands in resource timing. Every entry
|
|
19
|
+
* is checked against the URLs the patched transports already reported this page, and only genuinely
|
|
20
|
+
* document-initiated loads survive.
|
|
21
|
+
*/
|
|
22
|
+
/** Resource-timing initiator types that mean "the document itself asked", not a patched transport. */
|
|
23
|
+
const DOCUMENT_INITIATORS = new Set(['link', 'css', 'img', 'script', 'manifest', 'other']);
|
|
24
|
+
/** Cap per call — a pathological page must not blow the event budget. */
|
|
25
|
+
const MAX_SUBRESOURCE_EVENTS = 200;
|
|
26
|
+
export function collectSubresources(seenUrls, nowMs, sinceMs) {
|
|
27
|
+
const out = [];
|
|
28
|
+
try {
|
|
29
|
+
const entries = performance.getEntriesByType('resource');
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const initiatorType = entry.initiatorType || 'other';
|
|
32
|
+
// fetch/XHR already have richer records from their own patches.
|
|
33
|
+
if (!DOCUMENT_INITIATORS.has(initiatorType))
|
|
34
|
+
continue;
|
|
35
|
+
const raw = entry.name;
|
|
36
|
+
if (seenUrls.has(raw))
|
|
37
|
+
continue;
|
|
38
|
+
const startMs = nowMs - entry.duration;
|
|
39
|
+
if (startMs < sinceMs)
|
|
40
|
+
continue;
|
|
41
|
+
const obs = {
|
|
42
|
+
url: redactUrl(raw),
|
|
43
|
+
method: 'GET',
|
|
44
|
+
durationMs: Math.round(entry.duration),
|
|
45
|
+
initiatorType,
|
|
46
|
+
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
|
47
|
+
};
|
|
48
|
+
// responseStatus is Chromium-only and may be 0 when unreadable. Present ONLY when it says
|
|
49
|
+
// something real, so the wire never carries a guessed status.
|
|
50
|
+
const rs = entry.responseStatus;
|
|
51
|
+
if ('number' === typeof rs && rs > 0) {
|
|
52
|
+
obs.status = rs;
|
|
53
|
+
obs.ok = rs >= 200 && rs < 400;
|
|
54
|
+
}
|
|
55
|
+
out.push(obs);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// No resource-timing support / security error: report nothing, honestly.
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return out.slice(0, MAX_SUBRESOURCE_EVENTS);
|
|
63
|
+
}
|
|
@@ -223,6 +223,14 @@ export class ControlPanel {
|
|
|
223
223
|
/** The full replayable-flow list from the last push; re-filtered per page on route change. */
|
|
224
224
|
#flowItems = [];
|
|
225
225
|
#workspaceTeardown;
|
|
226
|
+
/**
|
|
227
|
+
* One signal for every listener this controller registers.
|
|
228
|
+
*
|
|
229
|
+
* All eight are anonymous closures over `this`, so there was no reference to hand
|
|
230
|
+
* `removeEventListener` and teardown removed none of them — each kept the controller reachable
|
|
231
|
+
* for as long as its element lived, and a second mount stacked another set on top.
|
|
232
|
+
*/
|
|
233
|
+
#listeners;
|
|
226
234
|
#host;
|
|
227
235
|
constructor(host) {
|
|
228
236
|
this.#host = host;
|
|
@@ -232,6 +240,8 @@ export class ControlPanel {
|
|
|
232
240
|
}
|
|
233
241
|
/** Query control refs out of the mounted root and bind the DOM listeners, then paint active. */
|
|
234
242
|
mount(root, glow) {
|
|
243
|
+
this.#listeners = new AbortController();
|
|
244
|
+
const { signal } = this.#listeners;
|
|
235
245
|
this.#root = root;
|
|
236
246
|
this.#glow = glow;
|
|
237
247
|
this.#refs = queryControlRefs(root);
|
|
@@ -239,17 +249,17 @@ export class ControlPanel {
|
|
|
239
249
|
if (pauseBtn !== undefined) {
|
|
240
250
|
paintPauseBtn(pauseBtn, this.#state === SessionState.PAUSED);
|
|
241
251
|
}
|
|
242
|
-
this.#refs.pauseBtn?.addEventListener('click', () => this.#onPauseToggle());
|
|
243
|
-
this.#refs.endBtn?.addEventListener('click', () => this.#onEnd());
|
|
244
|
-
this.#refs.sendBtn?.addEventListener('click', () => this.#onSend());
|
|
252
|
+
this.#refs.pauseBtn?.addEventListener('click', () => this.#onPauseToggle(), { signal });
|
|
253
|
+
this.#refs.endBtn?.addEventListener('click', () => this.#onEnd(), { signal });
|
|
254
|
+
this.#refs.sendBtn?.addEventListener('click', () => this.#onSend(), { signal });
|
|
245
255
|
this.#refs.input?.addEventListener('keydown', (e) => {
|
|
246
256
|
// Enter sends; Shift+Enter inserts a newline (falls through to the textarea's default).
|
|
247
257
|
if (e instanceof KeyboardEvent && 'Enter' === e.key && !e.shiftKey) {
|
|
248
258
|
e.preventDefault();
|
|
249
259
|
this.#onSend();
|
|
250
260
|
}
|
|
251
|
-
});
|
|
252
|
-
this.#refs.input?.addEventListener('input', () => this.#autosize());
|
|
261
|
+
}, { signal });
|
|
262
|
+
this.#refs.input?.addEventListener('input', () => this.#autosize(), { signal });
|
|
253
263
|
// Replay-a-flow: one ▶ click re-runs a saved flow (no agent). Delegated so it covers all chips.
|
|
254
264
|
this.#refs.flows?.addEventListener('click', (e) => {
|
|
255
265
|
const target = e.target;
|
|
@@ -259,9 +269,9 @@ export class ControlPanel {
|
|
|
259
269
|
if (name !== null && name !== undefined && name.length > 0) {
|
|
260
270
|
this.#host.emit(HumanControlKind.REPLAY, name);
|
|
261
271
|
}
|
|
262
|
-
});
|
|
263
|
-
this.#refs.copyBtn?.addEventListener('click', () => this.#onCopy());
|
|
264
|
-
this.#refs.exportBtn?.addEventListener('click', () => this.#onExport());
|
|
272
|
+
}, { signal });
|
|
273
|
+
this.#refs.copyBtn?.addEventListener('click', () => this.#onCopy(), { signal });
|
|
274
|
+
this.#refs.exportBtn?.addEventListener('click', () => this.#onExport(), { signal });
|
|
265
275
|
this.#workspaceTeardown = mountWorkspaceSelector(root);
|
|
266
276
|
this.setState(SessionState.ACTIVE);
|
|
267
277
|
}
|
|
@@ -297,6 +307,9 @@ export class ControlPanel {
|
|
|
297
307
|
}
|
|
298
308
|
/** Clear any pending ended-fade timer (called from Presenter.destroy). */
|
|
299
309
|
teardown() {
|
|
310
|
+
// All eight registrations, in one call that cannot drift from mount().
|
|
311
|
+
this.#listeners?.abort();
|
|
312
|
+
this.#listeners = undefined;
|
|
300
313
|
this.#workspaceTeardown?.();
|
|
301
314
|
this.#workspaceTeardown = undefined;
|
|
302
315
|
if (this.#fadeTimer !== undefined)
|
|
@@ -68,8 +68,12 @@ export function installHudPositionGuards(hud, overlay) {
|
|
|
68
68
|
const scheduleRelayout = () => {
|
|
69
69
|
scheduleSyncDockLayout(hud, overlay);
|
|
70
70
|
};
|
|
71
|
+
// One signal for all three listeners here. The observers below are NOT covered — neither
|
|
72
|
+
// ResizeObserver nor MutationObserver accepts one — so they keep their explicit disconnects.
|
|
73
|
+
const listeners = new AbortController();
|
|
74
|
+
const { signal } = listeners;
|
|
71
75
|
const onResize = () => scheduleRelayout();
|
|
72
|
-
window.addEventListener('resize', onResize);
|
|
76
|
+
window.addEventListener('resize', onResize, { signal });
|
|
73
77
|
let resizeObserver;
|
|
74
78
|
if (typeof ResizeObserver !== 'undefined') {
|
|
75
79
|
resizeObserver = new ResizeObserver(() => scheduleRelayout());
|
|
@@ -81,12 +85,10 @@ export function installHudPositionGuards(hud, overlay) {
|
|
|
81
85
|
attributeFilter: [MIN_ATTR, CHAT_ATTR, SETTINGS_ATTR],
|
|
82
86
|
});
|
|
83
87
|
const onVisualViewportResize = () => scheduleRelayout();
|
|
84
|
-
window.visualViewport?.addEventListener('resize', onVisualViewportResize);
|
|
85
|
-
window.visualViewport?.addEventListener('scroll', onVisualViewportResize);
|
|
88
|
+
window.visualViewport?.addEventListener('resize', onVisualViewportResize, { signal });
|
|
89
|
+
window.visualViewport?.addEventListener('scroll', onVisualViewportResize, { signal });
|
|
86
90
|
return () => {
|
|
87
|
-
|
|
88
|
-
window.visualViewport?.removeEventListener('resize', onVisualViewportResize);
|
|
89
|
-
window.visualViewport?.removeEventListener('scroll', onVisualViewportResize);
|
|
91
|
+
listeners.abort();
|
|
90
92
|
resizeObserver?.disconnect();
|
|
91
93
|
minObserver.disconnect();
|
|
92
94
|
};
|
|
@@ -171,15 +173,16 @@ export function installHudDrag(hud, head, callbacks = {}) {
|
|
|
171
173
|
releaseCapture(e.pointerId);
|
|
172
174
|
finishDrag();
|
|
173
175
|
};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
// All four pointer phases share one signal: they are added together and must come off
|
|
177
|
+
// together, and a partial removal would leave a half-live drag.
|
|
178
|
+
const listeners = new AbortController();
|
|
179
|
+
const { signal } = listeners;
|
|
180
|
+
head.addEventListener('pointerdown', onPointerDown, { signal });
|
|
181
|
+
head.addEventListener('pointermove', onPointerMove, { signal });
|
|
182
|
+
head.addEventListener('pointerup', onPointerUp, { signal });
|
|
183
|
+
head.addEventListener('pointercancel', onPointerCancel, { signal });
|
|
178
184
|
return () => {
|
|
179
|
-
|
|
180
|
-
head.removeEventListener('pointermove', onPointerMove);
|
|
181
|
-
head.removeEventListener('pointerup', onPointerUp);
|
|
182
|
-
head.removeEventListener('pointercancel', onPointerCancel);
|
|
185
|
+
listeners.abort();
|
|
183
186
|
if (dragging)
|
|
184
187
|
releaseCapture(activePointerId ?? 0);
|
|
185
188
|
finishDrag();
|
|
@@ -48,6 +48,15 @@ export class HudShell {
|
|
|
48
48
|
*/
|
|
49
49
|
#annotateOn = false;
|
|
50
50
|
#toggleSync;
|
|
51
|
+
/**
|
|
52
|
+
* One signal for every listener this shell registers, so teardown cannot drift from mount.
|
|
53
|
+
*
|
|
54
|
+
* The click handlers below are anonymous closures over `this`: there is no reference to hand to
|
|
55
|
+
* `removeEventListener`, so before this they were never removed at all. Mounting twice onto the
|
|
56
|
+
* same root therefore stacked a second set, and every handler kept the shell reachable for as
|
|
57
|
+
* long as its element lived.
|
|
58
|
+
*/
|
|
59
|
+
#listeners;
|
|
51
60
|
#report = new PresenterReport({
|
|
52
61
|
onBeforeOpen: () => {
|
|
53
62
|
// One panel at a time in the slot above the toolbar. The chat, the settings and the report
|
|
@@ -155,6 +164,8 @@ export class HudShell {
|
|
|
155
164
|
this.#annotateBtn?.setAttribute('data-active', on ? '1' : '0');
|
|
156
165
|
}
|
|
157
166
|
mount(root) {
|
|
167
|
+
this.#listeners = new AbortController();
|
|
168
|
+
const { signal } = this.#listeners;
|
|
158
169
|
this.#root = root;
|
|
159
170
|
this.#dock = root.querySelector(`[${DOCK_ATTR}]`) ?? undefined;
|
|
160
171
|
const fabEl = root.querySelector(`[${FAB_ATTR}]`);
|
|
@@ -170,19 +181,19 @@ export class HudShell {
|
|
|
170
181
|
e.stopPropagation();
|
|
171
182
|
this.setAnnotateOn(!this.#annotateOn);
|
|
172
183
|
this.#callbacks.onAnnotateToggle?.(this.#annotateOn);
|
|
173
|
-
});
|
|
184
|
+
}, { signal });
|
|
174
185
|
const pillEl = root.querySelector(`[${CHAT_PILL_ATTR}]`);
|
|
175
186
|
pillEl?.addEventListener('click', (e) => {
|
|
176
187
|
e.preventDefault();
|
|
177
188
|
e.stopPropagation();
|
|
178
189
|
this.showChat();
|
|
179
|
-
});
|
|
190
|
+
}, { signal });
|
|
180
191
|
const chatMinEl = root.querySelector(`[${CHAT_MIN_ATTR}]`);
|
|
181
192
|
chatMinEl?.addEventListener('click', (e) => {
|
|
182
193
|
e.preventDefault();
|
|
183
194
|
e.stopPropagation();
|
|
184
195
|
this.closeChat();
|
|
185
|
-
});
|
|
196
|
+
}, { signal });
|
|
186
197
|
const collapseEl = root.querySelector('[data-reticle-min-btn]');
|
|
187
198
|
this.#collapseBtn = collapseEl instanceof HTMLButtonElement ? collapseEl : undefined;
|
|
188
199
|
root.setAttribute(MIN_ATTR, '1');
|
|
@@ -204,7 +215,7 @@ export class HudShell {
|
|
|
204
215
|
e.preventDefault();
|
|
205
216
|
e.stopPropagation();
|
|
206
217
|
this.#report.toggle();
|
|
207
|
-
});
|
|
218
|
+
}, { signal });
|
|
208
219
|
this.#fab?.addEventListener('click', (e) => {
|
|
209
220
|
e.stopPropagation();
|
|
210
221
|
if (this.#suppressFabClick) {
|
|
@@ -212,15 +223,15 @@ export class HudShell {
|
|
|
212
223
|
return;
|
|
213
224
|
}
|
|
214
225
|
this.expand();
|
|
215
|
-
});
|
|
226
|
+
}, { signal });
|
|
216
227
|
this.#collapseBtn?.addEventListener('click', (e) => {
|
|
217
228
|
e.stopPropagation();
|
|
218
229
|
this.collapse();
|
|
219
|
-
});
|
|
230
|
+
}, { signal });
|
|
220
231
|
this.#chatToggle?.addEventListener('click', (e) => {
|
|
221
232
|
e.stopPropagation();
|
|
222
233
|
this.toggleChat();
|
|
223
|
-
});
|
|
234
|
+
}, { signal });
|
|
224
235
|
const toolbarDrag = root.querySelector(`.${DRAG_HANDLE_CLASS}`);
|
|
225
236
|
const dragHandles = [this.#fab, toolbarDrag].filter((el) => el instanceof HTMLElement);
|
|
226
237
|
if (this.#dock !== undefined && dragHandles.length > 0) {
|
|
@@ -235,12 +246,19 @@ export class HudShell {
|
|
|
235
246
|
});
|
|
236
247
|
this.#layoutTeardown = installHudPositionGuards(this.#dock, root);
|
|
237
248
|
}
|
|
238
|
-
document.addEventListener('pointerdown', this.#onDocPointerDown);
|
|
239
|
-
document.addEventListener('keydown', this.#onKeyDown);
|
|
249
|
+
document.addEventListener('pointerdown', this.#onDocPointerDown, { signal });
|
|
250
|
+
document.addEventListener('keydown', this.#onKeyDown, { signal });
|
|
240
251
|
}
|
|
241
252
|
teardown() {
|
|
242
|
-
|
|
243
|
-
|
|
253
|
+
// One call for all nine registrations. It cannot fall out of step with mount() the way a list
|
|
254
|
+
// of removeEventListener calls can, which is the whole point.
|
|
255
|
+
this.#listeners?.abort();
|
|
256
|
+
this.#listeners = undefined;
|
|
257
|
+
// NOT covered by the signal: a MutationObserver takes no `signal` option. This disconnect was
|
|
258
|
+
// missing entirely, so the observer kept `root` — and through it the shell — alive after
|
|
259
|
+
// teardown, and a re-mount left the previous one still firing.
|
|
260
|
+
this.#toggleSync?.disconnect();
|
|
261
|
+
this.#toggleSync = undefined;
|
|
244
262
|
this.#dragTeardown?.();
|
|
245
263
|
this.#dragTeardown = undefined;
|
|
246
264
|
this.#layoutTeardown?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/browser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Reticle browser SDK — installs observers, builds semantic snapshots, executes actions, talks to the bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@testing-library/dom": "^10.4.1",
|
|
17
|
-
"@reticlehq/core": "2.
|
|
17
|
+
"@reticlehq/core": "2.11.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@iconify-json/heroicons-outline": "^1.2.1",
|