@reticlehq/browser 2.12.0 → 2.13.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/actions/actions.js +6 -6
- package/dist/connect-options.d.ts +18 -1
- package/dist/dom/dom-ignore.d.ts +9 -0
- package/dist/dom/dom-ignore.js +32 -3
- package/dist/dom/query.js +55 -3
- package/dist/dom/snapshot.d.ts +8 -0
- package/dist/dom/snapshot.js +71 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/dist/observers/animation.js +6 -8
- package/dist/observers/focus.js +5 -6
- package/dist/observers/health.js +6 -9
- package/dist/observers/network-redact.d.ts +13 -0
- package/dist/observers/network-redact.js +12 -1
- package/dist/observers/network.d.ts +2 -2
- package/dist/observers/network.js +24 -17
- package/dist/observers/scroll.js +4 -2
- package/dist/presenter/presenter-config.d.ts +11 -2
- package/dist/presenter/presenter-config.js +9 -0
- package/dist/presenter/presenter-hud-chrome.d.ts +1 -1
- package/dist/presenter/presenter-report-copy.d.ts +30 -1
- package/dist/presenter/presenter-report-copy.js +31 -2
- package/dist/presenter/presenter-report-styles.js +24 -0
- package/dist/presenter/presenter-report.d.ts +2 -2
- package/dist/presenter/presenter-report.js +95 -3
- package/dist/presenter/presenter-shell-styles.js +20 -1
- package/dist/presenter/presenter-shell.js +0 -1
- package/dist/presenter/presenter.d.ts +13 -0
- package/dist/presenter/presenter.js +37 -2
- package/dist/registry/auto-testids.d.ts +21 -0
- package/dist/registry/auto-testids.js +48 -0
- package/dist/registry/capabilities.d.ts +28 -1
- package/dist/registry/capabilities.js +59 -7
- package/dist/registry/store-adapters.d.ts +1 -1
- package/dist/registry/store-adapters.js +6 -1
- package/dist/registry/stores.d.ts +4 -0
- package/dist/registry/stores.js +35 -0
- package/dist/reticle.js +8 -0
- package/dist/transport/unreachable-message.d.ts +7 -0
- package/dist/transport/unreachable-message.js +9 -0
- package/package.json +4 -4
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
/** Self-describing capability registry — the testable surface the app advertises. */
|
|
2
|
+
import { RETICLE_RENDERS_STORE } from '@reticlehq/core';
|
|
3
|
+
import { isPresenterVisible } from '../dom/dom-ignore.js';
|
|
4
|
+
import { domTestids } from './auto-testids.js';
|
|
5
|
+
import { storeNames } from './stores.js';
|
|
6
|
+
/**
|
|
7
|
+
* The two halves of the capability report the app no longer has to write down.
|
|
8
|
+
*
|
|
9
|
+
* `testids` and `stores` used to be *declarations* — strings copied into a generated file by `init`
|
|
10
|
+
* and then expected to stay true. Both are now read from the thing they describe: the testids from
|
|
11
|
+
* the DOM, the store names from the registry (which the React adapter fills by discovery). What the
|
|
12
|
+
* app declares is still merged in and still wins nothing away: `signals` and `flows` cannot be
|
|
13
|
+
* observed and remain the app's to state.
|
|
14
|
+
*/
|
|
15
|
+
function liveTestids() {
|
|
16
|
+
if ('undefined' === typeof document)
|
|
17
|
+
return [];
|
|
18
|
+
return domTestids(document);
|
|
19
|
+
}
|
|
20
|
+
/** Registered stores, minus the ones Reticle registered on the app's behalf. */
|
|
21
|
+
function liveStores() {
|
|
22
|
+
return storeNames().filter((name) => RETICLE_RENDERS_STORE !== name);
|
|
23
|
+
}
|
|
24
|
+
function union(declared, observed) {
|
|
25
|
+
const out = [...declared];
|
|
26
|
+
for (const value of observed)
|
|
27
|
+
if (!out.includes(value))
|
|
28
|
+
out.push(value);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
2
31
|
// Persist on a global so the registry survives HMR module re-evaluation (matches __reticleAdapters).
|
|
3
32
|
const globalStore = globalThis;
|
|
4
33
|
function empty() {
|
|
@@ -48,16 +77,39 @@ export function registerCapabilities(input) {
|
|
|
48
77
|
/** Snapshot copy of the registered capabilities (defensive — never hand out the live arrays). */
|
|
49
78
|
export function getCapabilities() {
|
|
50
79
|
return {
|
|
51
|
-
testids:
|
|
80
|
+
testids: union(capabilities.testids, liveTestids()),
|
|
52
81
|
signals: [...capabilities.signals],
|
|
53
|
-
stores:
|
|
82
|
+
stores: union(capabilities.stores, liveStores()),
|
|
54
83
|
flows: capabilities.flows.map((f) => ({ name: f.name, steps: [...f.steps] })),
|
|
84
|
+
// Only when open — an absent field is the ordinary case and should not cost a line in every
|
|
85
|
+
// capabilities payload ever sent.
|
|
86
|
+
...(isPresenterVisible() ? { presenterExposed: true } : {}),
|
|
55
87
|
};
|
|
56
88
|
}
|
|
57
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Testids the app DECLARED, without the ones merely observed in the DOM.
|
|
91
|
+
*
|
|
92
|
+
* The difference carries meaning the merged list cannot. `knownEmptyState` asks "did the app NAME
|
|
93
|
+
* this element as part of its testable surface?", which is how a zero-match query against
|
|
94
|
+
* `cart-empty-region` is told apart from a broken selector. Every testid on the page is observed, so
|
|
95
|
+
* answering that over the merged list makes it true whenever the page has any testid at all — the
|
|
96
|
+
* same as not asking.
|
|
97
|
+
*/
|
|
98
|
+
export function declaredTestids() {
|
|
99
|
+
return [...capabilities.testids];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether the app has a testable surface at all (used in the HELLO flag).
|
|
103
|
+
*
|
|
104
|
+
* Answered over the OBSERVED surface as well as the declared one. It used to be answerable only by
|
|
105
|
+
* declaration, so an app with two hundred testids in its DOM and a live Redux store reported
|
|
106
|
+
* `hasCapabilities: false` until somebody typed those facts into a config file — and the agent
|
|
107
|
+
* reading that flag concluded, correctly by the flag and wrongly in fact, that there was nothing to
|
|
108
|
+
* drive.
|
|
109
|
+
*/
|
|
58
110
|
export function hasCapabilities() {
|
|
59
|
-
return (capabilities.
|
|
60
|
-
capabilities.
|
|
61
|
-
capabilities.
|
|
62
|
-
capabilities.
|
|
111
|
+
return (capabilities.signals.length > 0 ||
|
|
112
|
+
capabilities.flows.length > 0 ||
|
|
113
|
+
union(capabilities.testids, liveTestids()).length > 0 ||
|
|
114
|
+
union(capabilities.stores, liveStores()).length > 0);
|
|
63
115
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { nativeWarn } from '../timers/native-console.js';
|
|
2
|
+
import { markAdapterSource } from './stores.js';
|
|
2
3
|
/**
|
|
3
4
|
* Expose a TanStack Query cache as a Reticle store, keyed by query key.
|
|
4
5
|
*
|
|
@@ -16,7 +17,7 @@ export function tanstackQueryStore(client) {
|
|
|
16
17
|
// effects, a provider remount, HMR — and an adapter holding the old cache would keep answering
|
|
17
18
|
// from a store the app no longer reads, which is a stale-data bug inside the tool whose job is
|
|
18
19
|
// catching stale data.
|
|
19
|
-
|
|
20
|
+
const store = {
|
|
20
21
|
getState: () => {
|
|
21
22
|
const out = {};
|
|
22
23
|
for (const query of client.getQueryCache().getAll()) {
|
|
@@ -34,6 +35,10 @@ export function tanstackQueryStore(client) {
|
|
|
34
35
|
},
|
|
35
36
|
subscribe: (listener) => client.getQueryCache().subscribe(listener),
|
|
36
37
|
};
|
|
38
|
+
// Registering this adapter claims the CLIENT too, so fiber-tree discovery can tell that the app
|
|
39
|
+
// already wired this exact QueryClient and must not add a second entry for it.
|
|
40
|
+
markAdapterSource(store, client);
|
|
41
|
+
return store;
|
|
37
42
|
}
|
|
38
43
|
/**
|
|
39
44
|
* Expose a chosen set of Jotai atoms as one Reticle store.
|
|
@@ -48,3 +48,7 @@ export declare function readStoresWithTruncation(only?: string): {
|
|
|
48
48
|
stores: Record<string, unknown>;
|
|
49
49
|
truncation?: Record<string, TruncationReport>;
|
|
50
50
|
};
|
|
51
|
+
/** Called by an adapter to record what it was built from. See `sourceOwner`. */
|
|
52
|
+
export declare function markAdapterSource(store: object, source: object): void;
|
|
53
|
+
/** The name this store source is registered under, or undefined if nothing has claimed it. */
|
|
54
|
+
export declare function sourceOwner(value: unknown): string | undefined;
|
package/dist/registry/stores.js
CHANGED
|
@@ -34,6 +34,7 @@ function isStoreLike(source) {
|
|
|
34
34
|
* leaves the store on pull-only reads: no change events, and an empty `stateDiffs` in every act summary.
|
|
35
35
|
*/
|
|
36
36
|
export function registerStore(name, source, subscribe) {
|
|
37
|
+
claimSource(name, source);
|
|
37
38
|
if (isStoreLike(source)) {
|
|
38
39
|
const getter = () => source.getState();
|
|
39
40
|
const sub = (listener) => source.subscribe(listener);
|
|
@@ -149,3 +150,37 @@ function warnSilentStoreOnce(name) {
|
|
|
149
150
|
`changes are invisible: no STATE_CHANGE events, no state diffs in causal summaries, and a state ` +
|
|
150
151
|
`predicate will never see it update. Pass the store object (or a subscribe callback) to fix.`);
|
|
151
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Who owns a given store SOURCE — the object the app (or an adapter) was built from.
|
|
155
|
+
*
|
|
156
|
+
* Registration is keyed by name, which cannot answer "is this same store already readable?". The
|
|
157
|
+
* question matters because the React adapter discovers stores from the fiber tree, and an app that
|
|
158
|
+
* has already registered the very same store by hand must not get a second entry for it under a
|
|
159
|
+
* second name. Comparing the READ VALUE cannot answer it either: `tanstackQueryStore` builds a fresh
|
|
160
|
+
* snapshot object on every `getState`, so identity never matches. Measured on bench-app, which
|
|
161
|
+
* registers `queries` itself and got an auto-discovered `query` beside it — one cache, listed twice,
|
|
162
|
+
* in every state read.
|
|
163
|
+
*
|
|
164
|
+
* A WeakMap so nothing here keeps an app's store alive.
|
|
165
|
+
*/
|
|
166
|
+
const sourceOwners = new WeakMap();
|
|
167
|
+
/** Adapter instance → the object it wraps, so registering an adapter also claims its source. */
|
|
168
|
+
const adapterSources = new WeakMap();
|
|
169
|
+
/** Called by an adapter to record what it was built from. See `sourceOwner`. */
|
|
170
|
+
export function markAdapterSource(store, source) {
|
|
171
|
+
adapterSources.set(store, source);
|
|
172
|
+
}
|
|
173
|
+
function claimSource(name, source) {
|
|
174
|
+
if (null === source || ('object' !== typeof source && 'function' !== typeof source))
|
|
175
|
+
return;
|
|
176
|
+
sourceOwners.set(source, name);
|
|
177
|
+
const wrapped = adapterSources.get(source);
|
|
178
|
+
if (wrapped !== undefined)
|
|
179
|
+
sourceOwners.set(wrapped, name);
|
|
180
|
+
}
|
|
181
|
+
/** The name this store source is registered under, or undefined if nothing has claimed it. */
|
|
182
|
+
export function sourceOwner(value) {
|
|
183
|
+
if (null === value || ('object' !== typeof value && 'function' !== typeof value))
|
|
184
|
+
return undefined;
|
|
185
|
+
return sourceOwners.get(value);
|
|
186
|
+
}
|
package/dist/reticle.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setPresenterVisible } from './dom/dom-ignore.js';
|
|
1
2
|
import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, buildRedactionPolicy, isLoopbackHostname, isLocalPage, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, CONTRACT_FINGERPRINT, newDocumentId, NO_EDITS_OBSERVED, } from '@reticlehq/core';
|
|
2
3
|
import { rememberSessionLabel } from './session-continuity.js';
|
|
3
4
|
import { editEpoch } from './edit-epoch.js';
|
|
@@ -251,11 +252,18 @@ export class Reticle {
|
|
|
251
252
|
// page observed rather than guessing at the daemon, which it cannot see from in here.
|
|
252
253
|
onUnreachable: ({ url: tried, attempts }) => {
|
|
253
254
|
nativeWarn(unreachableMessage(tried, attempts));
|
|
255
|
+
// And on screen, not only in a console nobody has open. An instrumented page with a dead
|
|
256
|
+
// bridge looked exactly like a page with no Reticle in it, so the user could not tell a
|
|
257
|
+
// daemon they forgot to start from an install that did not work.
|
|
258
|
+
this.#presenter?.showUnreachable(tried, attempts);
|
|
254
259
|
},
|
|
255
260
|
});
|
|
256
261
|
// Capabilities registered AFTER connect (which is when they are registered, by design) must
|
|
257
262
|
// reach the bridge, or an app that declared its whole testable surface reads as having none.
|
|
258
263
|
setCapabilitiesListener(() => this.#transport?.reannounce());
|
|
264
|
+
// Contributors only, and reported in capabilities so a verdict drawn with it open is never
|
|
265
|
+
// mistaken for an ordinary one. See connect-options.ts.
|
|
266
|
+
setPresenterVisible(true === options.exposePresenter);
|
|
259
267
|
const emit = this.#emit;
|
|
260
268
|
this.#teardowns = installAllObservers(emit, {
|
|
261
269
|
captureBodies: true === options.captureNetworkBodies,
|
|
@@ -10,3 +10,10 @@
|
|
|
10
10
|
* diagnose the daemon, and lists the checks in the order that actually resolves the ambiguity.
|
|
11
11
|
*/
|
|
12
12
|
export declare function unreachableMessage(url: string, attempts: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* The same fact, sized for the HUD's one-line status row.
|
|
15
|
+
*
|
|
16
|
+
* The console warning explains; this one only has room to name the URL, which is the answer in
|
|
17
|
+
* most cases anyway — a bridge on a port the page is not dialling.
|
|
18
|
+
*/
|
|
19
|
+
export declare function unreachableStripText(url: string, attempts: number): string;
|
|
@@ -19,3 +19,12 @@ export function unreachableMessage(url, attempts) {
|
|
|
19
19
|
`different host, so set the URL explicitly (Vite: VITE_RETICLE_WS_URL, or ` +
|
|
20
20
|
`reticle.connect({ url })). An https page cannot open a ws:// socket at all. Still retrying…`);
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* The same fact, sized for the HUD's one-line status row.
|
|
24
|
+
*
|
|
25
|
+
* The console warning explains; this one only has room to name the URL, which is the answer in
|
|
26
|
+
* most cases anyway — a bridge on a port the page is not dialling.
|
|
27
|
+
*/
|
|
28
|
+
export function unreachableStripText(url, attempts) {
|
|
29
|
+
return `no bridge at ${url} — ${String(attempts)} attempt${1 === attempts ? '' : 's'}`;
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/browser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@reticlehq/core": "2.
|
|
16
|
+
"@reticlehq/core": "2.13.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@iconify-json/heroicons-outline": "^1.2.1",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"@tanstack/query-core": "^5.101.4",
|
|
22
22
|
"jotai": "^2.20.2",
|
|
23
23
|
"jsdom": "^30.0.1",
|
|
24
|
-
"mobx": "^7.0.
|
|
24
|
+
"mobx": "^7.0.3",
|
|
25
25
|
"pinia": "^4.0.3",
|
|
26
26
|
"react": "^19.2.8",
|
|
27
27
|
"recoil": "^0.7.7",
|
|
28
|
-
"svelte": "^5.56.
|
|
28
|
+
"svelte": "^5.56.10",
|
|
29
29
|
"valtio": "^2.3.2",
|
|
30
30
|
"vue": "^3.5.41",
|
|
31
31
|
"xstate": "^5.32.5"
|