@reticlehq/browser 2.13.1 → 3.1.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-dom.d.ts +8 -20
- package/dist/actions/actions-dom.js +70 -69
- package/dist/actions/actions-press.d.ts +14 -0
- package/dist/actions/actions-press.js +123 -0
- package/dist/actions/actions.d.ts +1 -89
- package/dist/actions/actions.js +64 -365
- package/dist/actions/appeared-text.d.ts +0 -20
- package/dist/actions/appeared-text.js +6 -36
- package/dist/actions/danger-context.d.ts +3 -0
- package/dist/actions/danger-context.js +39 -0
- package/dist/actions/scroll.d.ts +8 -10
- package/dist/actions/scroll.js +28 -17
- package/dist/actions/synthetic/synthetic-input.d.ts +2 -0
- package/dist/actions/synthetic/synthetic-input.js +13 -0
- package/dist/actions/value-input.d.ts +0 -30
- package/dist/actions/value-input.js +0 -33
- package/dist/commands/commands.d.ts +0 -2
- package/dist/commands/commands.js +10 -70
- package/dist/connect-options.d.ts +2 -96
- package/dist/dom/a11y.d.ts +0 -29
- package/dist/dom/a11y.js +3 -90
- package/dist/dom/addressing/auto-anchor.d.ts +25 -0
- package/dist/dom/addressing/auto-anchor.js +41 -0
- package/dist/dom/addressing/refs.d.ts +14 -0
- package/dist/dom/addressing/refs.js +114 -0
- package/dist/dom/addressing/source.d.ts +9 -0
- package/dist/dom/addressing/source.js +29 -0
- package/dist/dom/chart.d.ts +0 -61
- package/dist/dom/chart.js +0 -60
- package/dist/dom/desktop-capture.d.ts +0 -30
- package/dist/dom/desktop-capture.js +1 -73
- package/dist/dom/dom-ignore.d.ts +0 -30
- package/dist/dom/dom-ignore.js +1 -48
- package/dist/dom/occlusion.d.ts +0 -7
- package/dist/dom/occlusion.js +31 -12
- package/dist/dom/paint-context.d.ts +6 -0
- package/dist/dom/paint-context.js +38 -0
- package/dist/dom/query.d.ts +0 -14
- package/dist/dom/query.js +50 -216
- package/dist/dom/realm.d.ts +0 -20
- package/dist/dom/realm.js +0 -28
- package/dist/dom/shadow-registry.d.ts +0 -33
- package/dist/dom/shadow-registry.js +2 -72
- package/dist/dom/snapshot.d.ts +2 -43
- package/dist/dom/snapshot.js +27 -152
- package/dist/dom/theme.d.ts +0 -37
- package/dist/dom/theme.js +2 -52
- package/dist/dom/virtualized.d.ts +0 -17
- package/dist/dom/virtualized.js +0 -27
- package/dist/edit-epoch.d.ts +1 -23
- package/dist/edit-epoch.js +1 -53
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -28
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +1 -4
- package/dist/observers/blind-spots.d.ts +0 -26
- package/dist/observers/blind-spots.js +3 -62
- package/dist/observers/console.d.ts +1 -2
- package/dist/observers/console.js +41 -30
- package/dist/observers/context-open.d.ts +1 -11
- package/dist/observers/context-open.js +10 -14
- package/dist/observers/departure.d.ts +3 -0
- package/dist/observers/departure.js +52 -0
- package/dist/observers/dialogs.d.ts +2 -0
- package/dist/observers/dialogs.js +39 -0
- package/dist/observers/dom.js +28 -52
- package/dist/observers/download.d.ts +1 -2
- package/dist/observers/download.js +39 -56
- package/dist/observers/focus.d.ts +0 -6
- package/dist/observers/focus.js +0 -7
- package/dist/observers/frames.d.ts +0 -9
- package/dist/observers/frames.js +1 -28
- package/dist/observers/health.d.ts +0 -5
- package/dist/observers/health.js +2 -34
- package/dist/observers/install-all.d.ts +2 -0
- package/dist/observers/install-all.js +26 -27
- package/dist/observers/ipc.d.ts +0 -25
- package/dist/observers/ipc.js +0 -65
- package/dist/observers/navigation.d.ts +2 -0
- package/dist/observers/navigation.js +33 -0
- package/dist/observers/net-detail/network-body.d.ts +12 -0
- package/dist/observers/net-detail/network-body.js +71 -0
- package/dist/observers/net-detail/network-redact.d.ts +1 -0
- package/dist/observers/net-detail/network-redact.js +1 -0
- package/dist/observers/network-stream.d.ts +0 -21
- package/dist/observers/network-stream.js +3 -46
- package/dist/observers/network.d.ts +3 -27
- package/dist/observers/network.js +178 -295
- package/dist/observers/perf.d.ts +0 -10
- package/dist/observers/perf.js +0 -16
- package/dist/observers/route.d.ts +1 -2
- package/dist/observers/route.js +13 -19
- package/dist/observers/scroll.d.ts +0 -1
- package/dist/observers/scroll.js +2 -11
- package/dist/observers/sdk-failure.d.ts +1 -9
- package/dist/observers/sdk-failure.js +1 -30
- package/dist/observers/state.d.ts +1 -11
- package/dist/observers/state.js +13 -41
- package/dist/observers/storage.d.ts +0 -14
- package/dist/observers/storage.js +10 -34
- package/dist/observers/types.d.ts +0 -15
- package/dist/observers/types.js +0 -14
- package/dist/patching/capture-method.d.ts +0 -24
- package/dist/patching/capture-method.js +0 -26
- package/dist/presenter/chrome/layers.d.ts +2 -0
- package/dist/presenter/chrome/layers.js +2 -0
- package/dist/presenter/chrome/log-kinds.d.ts +12 -0
- package/dist/presenter/chrome/log-kinds.js +7 -0
- package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -1
- package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -1
- package/dist/presenter/chrome/presenter-brand.d.ts +3 -0
- package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -21
- package/dist/presenter/chrome/presenter-effects.d.ts +4 -0
- package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -10
- package/dist/presenter/chrome/presenter-hud-chrome.d.ts +7 -0
- package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -10
- package/dist/presenter/chrome/presenter-log.d.ts +17 -0
- package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +5 -54
- package/dist/presenter/chrome/presenter-report-copy.d.ts +51 -0
- package/dist/presenter/chrome/presenter-report-copy.js +95 -0
- package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
- package/dist/presenter/chrome/presenter-safe-html.js +16 -0
- package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -5
- package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -5
- package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -7
- package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -8
- package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -8
- package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -6
- package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -6
- package/dist/presenter/presenter-account-styles.d.ts +1 -0
- package/dist/presenter/presenter-account-styles.js +99 -0
- package/dist/presenter/presenter-account.d.ts +40 -0
- package/dist/presenter/presenter-account.js +178 -0
- package/dist/presenter/presenter-config.d.ts +10 -107
- package/dist/presenter/presenter-config.js +5 -92
- package/dist/presenter/presenter-controls.d.ts +1 -35
- package/dist/presenter/presenter-controls.js +10 -133
- package/dist/presenter/presenter-dock-layout.d.ts +0 -3
- package/dist/presenter/presenter-dock-layout.js +1 -5
- package/dist/presenter/presenter-drag.d.ts +2 -42
- package/dist/presenter/presenter-drag.js +3 -76
- package/dist/presenter/presenter-glow.d.ts +0 -21
- package/dist/presenter/presenter-glow.js +2 -28
- package/dist/presenter/presenter-hud-position.d.ts +18 -0
- package/dist/presenter/presenter-hud-position.js +50 -0
- package/dist/presenter/presenter-report-styles.d.ts +0 -6
- package/dist/presenter/presenter-report-styles.js +22 -7
- package/dist/presenter/presenter-report.d.ts +5 -7
- package/dist/presenter/presenter-report.js +44 -95
- package/dist/presenter/presenter-run-state.d.ts +1 -8
- package/dist/presenter/presenter-run-state.js +1 -6
- package/dist/presenter/presenter-settings-styles.d.ts +0 -1
- package/dist/presenter/presenter-settings-styles.js +2 -2
- package/dist/presenter/presenter-settings.d.ts +5 -23
- package/dist/presenter/presenter-settings.js +21 -33
- package/dist/presenter/presenter-shell-styles.js +23 -20
- package/dist/presenter/presenter-shell.d.ts +6 -18
- package/dist/presenter/presenter-shell.js +26 -77
- package/dist/presenter/presenter-styles.d.ts +2 -10
- package/dist/presenter/presenter-styles.js +6 -12
- package/dist/presenter/presenter-workspace.d.ts +4 -4
- package/dist/presenter/presenter-workspace.js +30 -10
- package/dist/presenter/presenter.d.ts +3 -63
- package/dist/presenter/presenter.js +29 -132
- package/dist/presenter/tour/tour-view.d.ts +33 -0
- package/dist/presenter/tour/tour-view.js +188 -0
- package/dist/presenter/tour/tour.d.ts +25 -0
- package/dist/presenter/tour/tour.js +245 -0
- package/dist/recorder/recorder-styles.d.ts +0 -6
- package/dist/recorder/recorder-styles.js +0 -6
- package/dist/recorder/recorder.d.ts +0 -34
- package/dist/recorder/recorder.js +6 -49
- package/dist/registry/auto-testids.d.ts +0 -19
- package/dist/registry/auto-testids.js +1 -21
- package/dist/registry/capabilities.d.ts +0 -31
- package/dist/registry/capabilities.js +1 -46
- package/dist/registry/commit-and-signal.d.ts +0 -6
- package/dist/registry/commit-and-signal.js +1 -7
- package/dist/registry/declared-channels.d.ts +2 -0
- package/dist/registry/declared-channels.js +14 -0
- package/dist/registry/declared-platform.d.ts +4 -0
- package/dist/registry/declared-platform.js +34 -0
- package/dist/registry/domains.d.ts +0 -8
- package/dist/registry/domains.js +0 -7
- package/dist/registry/emitter.d.ts +0 -14
- package/dist/registry/emitter.js +1 -11
- package/dist/registry/store-adapters.d.ts +0 -186
- package/dist/registry/store-adapters.js +1 -164
- package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -10
- package/dist/registry/{adapters.js → stores/adapters.js} +0 -10
- package/dist/registry/stores.d.ts +0 -33
- package/dist/registry/stores.js +1 -60
- package/dist/reticle-presenter-helpers.d.ts +0 -13
- package/dist/reticle-presenter-helpers.js +10 -20
- package/dist/reticle-singleton.d.ts +2 -0
- package/dist/reticle-singleton.js +3 -0
- package/dist/reticle.d.ts +1 -52
- package/dist/reticle.js +115 -180
- package/dist/review/annotator-styles.js +1 -8
- package/dist/review/annotator.d.ts +2 -13
- package/dist/review/annotator.js +2 -42
- package/dist/review/mark-anchor.d.ts +0 -11
- package/dist/review/mark-anchor.js +3 -11
- package/dist/security/serialization.d.ts +0 -20
- package/dist/security/serialization.js +1 -66
- package/dist/session-continuity.d.ts +0 -16
- package/dist/session-continuity.js +1 -20
- package/dist/timers/clock.d.ts +1 -14
- package/dist/timers/clock.js +59 -60
- package/dist/timers/console/native-console.js +5 -0
- package/dist/timers/native/native-timers.d.ts +11 -0
- package/dist/timers/native/native-timers.js +48 -0
- package/dist/transport/transport.d.ts +0 -51
- package/dist/transport/transport.js +5 -131
- package/dist/transport/unreachable-message.d.ts +0 -17
- package/dist/transport/unreachable-message.js +0 -17
- package/dist/util/captured-method.d.ts +0 -35
- package/dist/util/captured-method.js +0 -35
- package/package.json +5 -4
- package/dist/actions/synthetic-input.d.ts +0 -9
- package/dist/actions/synthetic-input.js +0 -40
- package/dist/dom/auto-anchor.d.ts +0 -65
- package/dist/dom/auto-anchor.js +0 -74
- package/dist/dom/refs.d.ts +0 -90
- package/dist/dom/refs.js +0 -245
- package/dist/dom/source.d.ts +0 -53
- package/dist/dom/source.js +0 -74
- package/dist/observers/network-body.d.ts +0 -35
- package/dist/observers/network-body.js +0 -168
- package/dist/observers/network-redact.d.ts +0 -20
- package/dist/observers/network-redact.js +0 -80
- package/dist/presenter/presenter-brand.d.ts +0 -22
- package/dist/presenter/presenter-effects.d.ts +0 -13
- package/dist/presenter/presenter-hud-chrome.d.ts +0 -17
- package/dist/presenter/presenter-log.d.ts +0 -64
- package/dist/presenter/presenter-report-copy.d.ts +0 -112
- package/dist/presenter/presenter-report-copy.js +0 -156
- package/dist/presenter/presenter-test-helpers.d.ts +0 -16
- package/dist/presenter/presenter-test-helpers.js +0 -41
- package/dist/timers/native-console.js +0 -9
- package/dist/timers/native-timers.d.ts +0 -36
- package/dist/timers/native-timers.js +0 -78
- /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
- /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
|
@@ -1,37 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read a host object's method as a STORED VALUE, for code that has to put it back.
|
|
3
|
-
*
|
|
4
|
-
* Every observer here patches a global and must be able to undo it: `console.error`, `window.fetch`,
|
|
5
|
-
* `window.setTimeout`, `Date.now`. Undoing means holding the true original — the same function
|
|
6
|
-
* object, so teardown restores exactly what was there and a later install can recognise it by
|
|
7
|
-
* identity — and calling through a separately bound copy, because a host method invoked with the
|
|
8
|
-
* wrong receiver throws (`fetch` gives "Illegal invocation").
|
|
9
|
-
*
|
|
10
|
-
* Written as `const original = window.fetch`, that is a bare unbound method reference, and
|
|
11
|
-
* `@typescript-eslint/unbound-method` is right to flag the SHAPE even though this use is deliberate:
|
|
12
|
-
* the rule cannot see that the next line binds it, nor that the raw value only ever goes back into
|
|
13
|
-
* the slot it came from.
|
|
14
|
-
*
|
|
15
|
-
* The two obvious ways out are both wrong here. An `eslint-disable` is against this repo's standing
|
|
16
|
-
* rule and hides the shape from every future reader. Annotating `this: void` asserts the function
|
|
17
|
-
* does not use its receiver — which for a console method or `fetch` is simply false, and is the very
|
|
18
|
-
* reason the binding exists.
|
|
19
|
-
*
|
|
20
|
-
* So the intent is expressed instead of suppressed. A property descriptor's `value` is a stored
|
|
21
|
-
* value, not a method being detached from its object, which is exactly the distinction the rule is
|
|
22
|
-
* drawing and exactly what this code wants. It is also strictly more accurate for the restore case:
|
|
23
|
-
* it reads what the slot HOLDS rather than what calling the property would do, so an accessor
|
|
24
|
-
* defined on the object cannot be silently flattened into its result.
|
|
25
|
-
*
|
|
26
|
-
* Returns undefined when the property is absent or inherited rather than own — a caller that needs a
|
|
27
|
-
* fallback to the prototype chain should say so at its own call site rather than have it hidden here.
|
|
28
|
-
*/
|
|
29
1
|
export declare function capturedMethod<T>(target: object, key: PropertyKey): T | undefined;
|
|
30
|
-
/**
|
|
31
|
-
* The same, for a property this code knows is present — the globals every observer patches.
|
|
32
|
-
*
|
|
33
|
-
* Throws rather than returning a broken teardown. A missing `window.fetch` is not a condition any
|
|
34
|
-
* caller here can recover from, and silently installing a patch that cannot be undone is worse than
|
|
35
|
-
* refusing: the page keeps the wrapper for the rest of its life with nothing able to remove it.
|
|
36
|
-
*/
|
|
37
2
|
export declare function requireCapturedMethod<T>(target: object, key: PropertyKey): T;
|
|
@@ -1,41 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read a host object's method as a STORED VALUE, for code that has to put it back.
|
|
3
|
-
*
|
|
4
|
-
* Every observer here patches a global and must be able to undo it: `console.error`, `window.fetch`,
|
|
5
|
-
* `window.setTimeout`, `Date.now`. Undoing means holding the true original — the same function
|
|
6
|
-
* object, so teardown restores exactly what was there and a later install can recognise it by
|
|
7
|
-
* identity — and calling through a separately bound copy, because a host method invoked with the
|
|
8
|
-
* wrong receiver throws (`fetch` gives "Illegal invocation").
|
|
9
|
-
*
|
|
10
|
-
* Written as `const original = window.fetch`, that is a bare unbound method reference, and
|
|
11
|
-
* `@typescript-eslint/unbound-method` is right to flag the SHAPE even though this use is deliberate:
|
|
12
|
-
* the rule cannot see that the next line binds it, nor that the raw value only ever goes back into
|
|
13
|
-
* the slot it came from.
|
|
14
|
-
*
|
|
15
|
-
* The two obvious ways out are both wrong here. An `eslint-disable` is against this repo's standing
|
|
16
|
-
* rule and hides the shape from every future reader. Annotating `this: void` asserts the function
|
|
17
|
-
* does not use its receiver — which for a console method or `fetch` is simply false, and is the very
|
|
18
|
-
* reason the binding exists.
|
|
19
|
-
*
|
|
20
|
-
* So the intent is expressed instead of suppressed. A property descriptor's `value` is a stored
|
|
21
|
-
* value, not a method being detached from its object, which is exactly the distinction the rule is
|
|
22
|
-
* drawing and exactly what this code wants. It is also strictly more accurate for the restore case:
|
|
23
|
-
* it reads what the slot HOLDS rather than what calling the property would do, so an accessor
|
|
24
|
-
* defined on the object cannot be silently flattened into its result.
|
|
25
|
-
*
|
|
26
|
-
* Returns undefined when the property is absent or inherited rather than own — a caller that needs a
|
|
27
|
-
* fallback to the prototype chain should say so at its own call site rather than have it hidden here.
|
|
28
|
-
*/
|
|
29
1
|
export function capturedMethod(target, key) {
|
|
30
2
|
return Object.getOwnPropertyDescriptor(target, key)?.value;
|
|
31
3
|
}
|
|
32
|
-
/**
|
|
33
|
-
* The same, for a property this code knows is present — the globals every observer patches.
|
|
34
|
-
*
|
|
35
|
-
* Throws rather than returning a broken teardown. A missing `window.fetch` is not a condition any
|
|
36
|
-
* caller here can recover from, and silently installing a patch that cannot be undone is worse than
|
|
37
|
-
* refusing: the page keeps the wrapper for the rest of its life with nothing able to remove it.
|
|
38
|
-
*/
|
|
39
4
|
export function requireCapturedMethod(target, key) {
|
|
40
5
|
const value = capturedMethod(target, key);
|
|
41
6
|
if (value === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/browser",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.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,12 +13,13 @@
|
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@reticlehq/core": "
|
|
16
|
+
"@reticlehq/core": "3.1.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@iconify-json/heroicons-outline": "^1.2.1",
|
|
20
20
|
"@iconify-json/heroicons-solid": "^1.2.1",
|
|
21
21
|
"@tanstack/query-core": "^5.101.4",
|
|
22
|
+
"esbuild": "^0.28.2",
|
|
22
23
|
"jotai": "^2.20.2",
|
|
23
24
|
"jsdom": "^30.0.1",
|
|
24
25
|
"mobx": "^7.0.3",
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"repository": {
|
|
36
37
|
"type": "git",
|
|
37
38
|
"url": "git+https://github.com/reticlehq/reticle.git",
|
|
38
|
-
"directory": "
|
|
39
|
+
"directory": "adapters/realm/browser"
|
|
39
40
|
},
|
|
40
41
|
"homepage": "https://github.com/reticlehq/reticle#readme",
|
|
41
42
|
"bugs": "https://github.com/reticlehq/reticle/issues",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"node": ">=20.0.0"
|
|
60
61
|
},
|
|
61
62
|
"scripts": {
|
|
62
|
-
"build": "node scripts/embed-heroicons.mjs && tsc -b",
|
|
63
|
+
"build": "node scripts/embed-heroicons.mjs && tsc -b && node ../../../scripts/alias-dist.mjs",
|
|
63
64
|
"typecheck": "tsc -b",
|
|
64
65
|
"lint": "eslint src",
|
|
65
66
|
"test:unit": "vitest run src --passWithNoTests",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/** True while a Reticle-dispatched input event is being delivered. */
|
|
2
|
-
export declare function isSyntheticInput(): boolean;
|
|
3
|
-
/**
|
|
4
|
-
* Run `dispatch` marked as Reticle's own.
|
|
5
|
-
*
|
|
6
|
-
* `finally` rather than a trailing decrement: a listener that throws must not leave the flag stuck
|
|
7
|
-
* on, which would silently disable annotation for the rest of the page's life.
|
|
8
|
-
*/
|
|
9
|
-
export declare function asSyntheticInput<T>(dispatch: () => T): T;
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Is the input event currently being dispatched one that RETICLE made?
|
|
3
|
-
*
|
|
4
|
-
* The annotator listens for clicks in the capture phase and cancels them, because a click in
|
|
5
|
-
* annotate mode is a request to place a mark rather than a request to press a button. That is right
|
|
6
|
-
* for a person and wrong for the agent: `reticle_act` dispatches its clicks into the same document,
|
|
7
|
-
* so with the HUD expanded every agent click was swallowed while the action still reported
|
|
8
|
-
* `dispatched: true`. Measured on this repo's own fixture — the same click that produced
|
|
9
|
-
* `POST /api/login -> 200` with annotate off produced no network at all with it on.
|
|
10
|
-
*
|
|
11
|
-
* `isTrusted` looks like the answer and is not usable: it is false for everything `dispatchEvent`
|
|
12
|
-
* creates, which is also how every test in this package drives the annotator, so gating on it would
|
|
13
|
-
* make annotation untestable and would still not distinguish Reticle's clicks from the page's own
|
|
14
|
-
* synthetic ones.
|
|
15
|
-
*
|
|
16
|
-
* So Reticle marks its own. Dispatch is synchronous, so a plain depth counter held across the call
|
|
17
|
-
* is exact: any listener running inside it — capture phase included — is running because of us.
|
|
18
|
-
* Nested actions are counted rather than flagged, so an inner action finishing does not clear the
|
|
19
|
-
* mark for the outer one still in flight.
|
|
20
|
-
*/
|
|
21
|
-
let depth = 0;
|
|
22
|
-
/** True while a Reticle-dispatched input event is being delivered. */
|
|
23
|
-
export function isSyntheticInput() {
|
|
24
|
-
return depth > 0;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Run `dispatch` marked as Reticle's own.
|
|
28
|
-
*
|
|
29
|
-
* `finally` rather than a trailing decrement: a listener that throws must not leave the flag stuck
|
|
30
|
-
* on, which would silently disable annotation for the rest of the page's life.
|
|
31
|
-
*/
|
|
32
|
-
export function asSyntheticInput(dispatch) {
|
|
33
|
-
depth += 1;
|
|
34
|
-
try {
|
|
35
|
-
return dispatch();
|
|
36
|
-
}
|
|
37
|
-
finally {
|
|
38
|
-
depth -= 1;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-anchor synthesis — derive a STABLE way to address any element WITHOUT a hand-added
|
|
3
|
-
* data-testid. This is the brain of zero-config integration: given what the framework already knows
|
|
4
|
-
* about an element (its component name + source location from the fiber, its role + accessible name,
|
|
5
|
-
* its position), pick the most durable anchor so a flow can re-find the element across refactors.
|
|
6
|
-
*
|
|
7
|
-
* Pure and framework-agnostic: it takes a plain descriptor (the React adapter, or a future Vue/Svelte
|
|
8
|
-
* adapter, fills it in) and returns the chosen anchor + whether it is stable. No DOM, no fiber here.
|
|
9
|
-
*
|
|
10
|
-
* Priority (most → least durable). Each tier is only used when the one above is unavailable:
|
|
11
|
-
* 1. testid — explicit, author-chosen, the gold standard. Stable.
|
|
12
|
-
* 2. component@source — component display name + source file:line. Survives text/markup/style
|
|
13
|
-
* changes; uniquely identifies WHERE in the source the element comes from.
|
|
14
|
-
* Stable (a move that changes the source line is a real change worth re-pinning).
|
|
15
|
-
* 3. component+role/name — component name plus role/accessible-name when no source is available.
|
|
16
|
-
* Stable enough: the component identity anchors it, role/name disambiguate.
|
|
17
|
-
* 4. role+name — semantic but content-dependent: an accessible name can change with copy.
|
|
18
|
-
* NOT marked stable (a healed locator could drift to a renamed element).
|
|
19
|
-
* 5. role / position — last resort. NOT stable; surfaced so the element is still addressable, but
|
|
20
|
-
* a flow built on it should be flagged (the recorder marks the step degraded).
|
|
21
|
-
*
|
|
22
|
-
* The point: tiers 1–3 are "stable" (a flow anchored on them can be trusted to re-resolve to the
|
|
23
|
-
* same element); tiers 4–5 are best-effort. Today most elements without a testid fall to role/text
|
|
24
|
-
* (degraded). With component+source, the SAME elements become tier-2 stable anchors — that is how
|
|
25
|
-
* integration becomes universal without the app author adding a single testid.
|
|
26
|
-
*/
|
|
27
|
-
/** Which signal produced the anchor — also its durability tier. */
|
|
28
|
-
export declare const AnchorStrategy: {
|
|
29
|
-
readonly TESTID: "testid";
|
|
30
|
-
readonly COMPONENT: "component";
|
|
31
|
-
readonly ROLE: "role";
|
|
32
|
-
readonly POSITION: "position";
|
|
33
|
-
};
|
|
34
|
-
export type AnchorStrategy = (typeof AnchorStrategy)[keyof typeof AnchorStrategy];
|
|
35
|
-
/** What an adapter knows about an element — any subset; richer input yields a more durable anchor. */
|
|
36
|
-
export interface AnchorInput {
|
|
37
|
-
/** Explicit data-testid, if the author added one. */
|
|
38
|
-
testid?: string;
|
|
39
|
-
/** Nearest component display name (React fiber / Vue instance / …). */
|
|
40
|
-
component?: string;
|
|
41
|
-
/** Source location stamped by the framework's compiler/plugin. */
|
|
42
|
-
source?: {
|
|
43
|
-
file: string;
|
|
44
|
-
line: number;
|
|
45
|
-
};
|
|
46
|
-
/** ARIA role. */
|
|
47
|
-
role?: string;
|
|
48
|
-
/** Accessible name (label / text). */
|
|
49
|
-
name?: string;
|
|
50
|
-
/** Index among same-component-or-role siblings — a positional tiebreaker. */
|
|
51
|
-
nth?: number;
|
|
52
|
-
}
|
|
53
|
-
interface SynthesizedAnchor {
|
|
54
|
-
strategy: AnchorStrategy;
|
|
55
|
-
/** A compact, human-legible, re-resolvable anchor string. */
|
|
56
|
-
value: string;
|
|
57
|
-
/** True when a flow can trust this anchor to re-resolve to the same element (tiers 1–3). */
|
|
58
|
-
stable: boolean;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Choose the most durable anchor for an element from whatever the adapter could observe. Always
|
|
62
|
-
* returns something addressable (falls through to position), with `stable` marking tiers 1–3.
|
|
63
|
-
*/
|
|
64
|
-
export declare function synthesizeAnchor(input: AnchorInput): SynthesizedAnchor;
|
|
65
|
-
export {};
|
package/dist/dom/auto-anchor.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-anchor synthesis — derive a STABLE way to address any element WITHOUT a hand-added
|
|
3
|
-
* data-testid. This is the brain of zero-config integration: given what the framework already knows
|
|
4
|
-
* about an element (its component name + source location from the fiber, its role + accessible name,
|
|
5
|
-
* its position), pick the most durable anchor so a flow can re-find the element across refactors.
|
|
6
|
-
*
|
|
7
|
-
* Pure and framework-agnostic: it takes a plain descriptor (the React adapter, or a future Vue/Svelte
|
|
8
|
-
* adapter, fills it in) and returns the chosen anchor + whether it is stable. No DOM, no fiber here.
|
|
9
|
-
*
|
|
10
|
-
* Priority (most → least durable). Each tier is only used when the one above is unavailable:
|
|
11
|
-
* 1. testid — explicit, author-chosen, the gold standard. Stable.
|
|
12
|
-
* 2. component@source — component display name + source file:line. Survives text/markup/style
|
|
13
|
-
* changes; uniquely identifies WHERE in the source the element comes from.
|
|
14
|
-
* Stable (a move that changes the source line is a real change worth re-pinning).
|
|
15
|
-
* 3. component+role/name — component name plus role/accessible-name when no source is available.
|
|
16
|
-
* Stable enough: the component identity anchors it, role/name disambiguate.
|
|
17
|
-
* 4. role+name — semantic but content-dependent: an accessible name can change with copy.
|
|
18
|
-
* NOT marked stable (a healed locator could drift to a renamed element).
|
|
19
|
-
* 5. role / position — last resort. NOT stable; surfaced so the element is still addressable, but
|
|
20
|
-
* a flow built on it should be flagged (the recorder marks the step degraded).
|
|
21
|
-
*
|
|
22
|
-
* The point: tiers 1–3 are "stable" (a flow anchored on them can be trusted to re-resolve to the
|
|
23
|
-
* same element); tiers 4–5 are best-effort. Today most elements without a testid fall to role/text
|
|
24
|
-
* (degraded). With component+source, the SAME elements become tier-2 stable anchors — that is how
|
|
25
|
-
* integration becomes universal without the app author adding a single testid.
|
|
26
|
-
*/
|
|
27
|
-
/** Which signal produced the anchor — also its durability tier. */
|
|
28
|
-
export const AnchorStrategy = {
|
|
29
|
-
TESTID: 'testid',
|
|
30
|
-
COMPONENT: 'component',
|
|
31
|
-
ROLE: 'role',
|
|
32
|
-
POSITION: 'position',
|
|
33
|
-
};
|
|
34
|
-
function nonEmpty(s) {
|
|
35
|
-
return 'string' === typeof s && s.length > 0;
|
|
36
|
-
}
|
|
37
|
-
/** Source as a compact `file:line` suffix; basename only to stay terse and path-independent. */
|
|
38
|
-
function sourceTag(source) {
|
|
39
|
-
const base = source.file.split('/').pop() ?? source.file;
|
|
40
|
-
return `${base}:${source.line}`;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Choose the most durable anchor for an element from whatever the adapter could observe. Always
|
|
44
|
-
* returns something addressable (falls through to position), with `stable` marking tiers 1–3.
|
|
45
|
-
*/
|
|
46
|
-
export function synthesizeAnchor(input) {
|
|
47
|
-
if (nonEmpty(input.testid)) {
|
|
48
|
-
return { strategy: AnchorStrategy.TESTID, value: input.testid, stable: true };
|
|
49
|
-
}
|
|
50
|
-
if (nonEmpty(input.component) && input.source !== undefined) {
|
|
51
|
-
return {
|
|
52
|
-
strategy: AnchorStrategy.COMPONENT,
|
|
53
|
-
value: `${input.component}@${sourceTag(input.source)}`,
|
|
54
|
-
stable: true,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
if (nonEmpty(input.component) && (nonEmpty(input.role) || nonEmpty(input.name))) {
|
|
58
|
-
const qualifier = nonEmpty(input.name) ? input.name : input.role;
|
|
59
|
-
return {
|
|
60
|
-
strategy: AnchorStrategy.COMPONENT,
|
|
61
|
-
value: `${input.component}[${qualifier ?? ''}]`,
|
|
62
|
-
stable: true,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
if (nonEmpty(input.role) && nonEmpty(input.name)) {
|
|
66
|
-
return { strategy: AnchorStrategy.ROLE, value: `${input.role}:${input.name}`, stable: false };
|
|
67
|
-
}
|
|
68
|
-
if (nonEmpty(input.role)) {
|
|
69
|
-
const suffix = input.nth !== undefined ? `#${input.nth}` : '';
|
|
70
|
-
return { strategy: AnchorStrategy.ROLE, value: `${input.role}${suffix}`, stable: false };
|
|
71
|
-
}
|
|
72
|
-
// Last resort: position only — addressable but never trusted.
|
|
73
|
-
return { strategy: AnchorStrategy.POSITION, value: `el#${input.nth ?? 0}`, stable: false };
|
|
74
|
-
}
|
package/dist/dom/refs.d.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { type Ref } from '@reticlehq/core';
|
|
2
|
-
/**
|
|
3
|
-
* Stable, human-meaningful element handles. Each element gets a ref like `e7`; the same
|
|
4
|
-
* element keeps its ref across snapshots, and a ref re-resolves to its element as long as
|
|
5
|
-
* the element is still in the document.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* How many ref -> element entries are retained.
|
|
9
|
-
*
|
|
10
|
-
* The forward map is weak, so elements are always collectable; this bounds the BOOKKEEPING, which was
|
|
11
|
-
* not. Refs are minted far more often than an agent asks about them — every meaningful DOM addition,
|
|
12
|
-
* every transitionend, every scroll reveal — and the only eviction used to be "the agent happened to
|
|
13
|
-
* resolve this exact dead ref". A busy app over a long session therefore accumulated entries for
|
|
14
|
-
* elements that had been garbage for hours.
|
|
15
|
-
*
|
|
16
|
-
* Sized generously because eviction is not free of consequence (see refFor): the useful lifetime of a
|
|
17
|
-
* ref is one agent turn, and this is far more than any one turn produces.
|
|
18
|
-
*/
|
|
19
|
-
export declare const MAX_TRACKED_REFS = 10000;
|
|
20
|
-
/**
|
|
21
|
-
* A ref, made safe to interpolate into an error message.
|
|
22
|
-
*
|
|
23
|
-
* A ref Reticle mints is `e7` — three characters. A ref a CALLER passes is whatever they typed, and
|
|
24
|
-
* an agent guessing at the schema can pass 100KB of it. Every stale-ref message interpolates that
|
|
25
|
-
* value directly, so the caller's own bad argument came back as a 100KB error.
|
|
26
|
-
*
|
|
27
|
-
* The server caps error messages centrally, but it cannot help here: the browser's transport
|
|
28
|
-
* serializer truncates from the TAIL first, which severs "… no longer resolves to an element" — the
|
|
29
|
-
* exact substring the server's recovery table matches on. A stale ref then arrives classified as a
|
|
30
|
-
* possible Reticle defect. So the echo is bounded at the source, where the untrusted value enters
|
|
31
|
-
* the message.
|
|
32
|
-
*/
|
|
33
|
-
export declare function echoRef(ref: string): string;
|
|
34
|
-
/**
|
|
35
|
-
* How many numbers a document claims at once.
|
|
36
|
-
*
|
|
37
|
-
* The reservation is a synchronous storage write, so it cannot happen per mint — mints are frequent
|
|
38
|
-
* (every meaningful DOM addition, every transitionend, every scroll reveal). One write per this many
|
|
39
|
-
* mints is free.
|
|
40
|
-
*
|
|
41
|
-
* Sized to keep refs SHORT, and the first attempt at that reasoned about the wrong axis. It bounded
|
|
42
|
-
* how many navigations it took to reach seven digits and concluded "a hundred" — but the cost is not
|
|
43
|
-
* paid at seven digits, it is paid at every digit, on every ref, in every snapshot and every error
|
|
44
|
-
* message, on every turn. At 10,000 the SECOND document already mints five-digit refs.
|
|
45
|
-
*
|
|
46
|
-
* Measured on one unchanged three-element page across a session: 61 tokens on the first document,
|
|
47
|
-
* 64 after a reload, 65 later — +6.5% for the same page, entirely in ref width. The benchmark saw the
|
|
48
|
-
* same thing as a ~4% rise in average observation cost, which is what a benchmark is for.
|
|
49
|
-
*
|
|
50
|
-
* So the block is small. A document claims another one when it outmints this, which costs one more
|
|
51
|
-
* storage write and is exactly proportional: a page with thousands of elements has a long snapshot
|
|
52
|
-
* anyway, and a small page — the common case, and the one an agent drives repeatedly — keeps
|
|
53
|
-
* three-digit refs across dozens of navigations.
|
|
54
|
-
*/
|
|
55
|
-
export declare const REF_BLOCK = 100;
|
|
56
|
-
export declare class RefRegistry {
|
|
57
|
-
#private;
|
|
58
|
-
/**
|
|
59
|
-
* Storage is injected rather than read from the global for the same reason the clock is: it is an
|
|
60
|
-
* ambient dependency that decides behaviour, and the cross-document property is untestable if the
|
|
61
|
-
* only way to get a second document is to actually navigate.
|
|
62
|
-
*/
|
|
63
|
-
constructor(storage?: () => Storage | undefined);
|
|
64
|
-
/** How many reverse entries are currently retained. Exposed so the bound can be asserted. */
|
|
65
|
-
get size(): number;
|
|
66
|
-
/**
|
|
67
|
-
* A hot update just replaced modules in this document: every ref handed out so far was minted
|
|
68
|
-
* against the DOM the previous version of that code produced.
|
|
69
|
-
*
|
|
70
|
-
* Only the boundary is recorded. Nothing is evicted — a hot update is not a navigation, and most
|
|
71
|
-
* of the page survives one, so refs minted before it very often still resolve. This exists so the
|
|
72
|
-
* ones that DON'T can say why.
|
|
73
|
-
*/
|
|
74
|
-
markEdited(): void;
|
|
75
|
-
/** Was this ref handed out before the last hot update? False while no update has been observed. */
|
|
76
|
-
mintedBeforeLastEdit(ref: string): boolean;
|
|
77
|
-
/** Get the existing ref for an element, or mint a new one. */
|
|
78
|
-
refFor(el: Element): Ref;
|
|
79
|
-
/**
|
|
80
|
-
* Resolve a ref back to its element, or null if it's gone/detached.
|
|
81
|
-
*
|
|
82
|
-
* Takes a plain `string` ON PURPOSE: this is the untrusted-input path — the ref comes from the agent
|
|
83
|
-
* over the wire, and a miss is answered with null rather than an error. Requiring a branded Ref here
|
|
84
|
-
* would force a meaningless cast at every wire boundary and buy nothing; the brand's value is on the
|
|
85
|
-
* MINT (refFor) so our own code cannot pass, say, a sessionId where a handle is expected.
|
|
86
|
-
*/
|
|
87
|
-
resolve(ref: string): Element | null;
|
|
88
|
-
}
|
|
89
|
-
/** Process-wide registry shared by snapshot, query, and the action executor. */
|
|
90
|
-
export declare const refs: RefRegistry;
|
package/dist/dom/refs.js
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
|
|
2
|
-
/**
|
|
3
|
-
* Stable, human-meaningful element handles. Each element gets a ref like `e7`; the same
|
|
4
|
-
* element keeps its ref across snapshots, and a ref re-resolves to its element as long as
|
|
5
|
-
* the element is still in the document.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* How many ref -> element entries are retained.
|
|
9
|
-
*
|
|
10
|
-
* The forward map is weak, so elements are always collectable; this bounds the BOOKKEEPING, which was
|
|
11
|
-
* not. Refs are minted far more often than an agent asks about them — every meaningful DOM addition,
|
|
12
|
-
* every transitionend, every scroll reveal — and the only eviction used to be "the agent happened to
|
|
13
|
-
* resolve this exact dead ref". A busy app over a long session therefore accumulated entries for
|
|
14
|
-
* elements that had been garbage for hours.
|
|
15
|
-
*
|
|
16
|
-
* Sized generously because eviction is not free of consequence (see refFor): the useful lifetime of a
|
|
17
|
-
* ref is one agent turn, and this is far more than any one turn produces.
|
|
18
|
-
*/
|
|
19
|
-
export const MAX_TRACKED_REFS = 10000;
|
|
20
|
-
/** Marks a ref that was too long to echo in full. */
|
|
21
|
-
const REF_ELISION = '…';
|
|
22
|
-
/** Every ref Reticle mints is this letter followed by its sequence number. */
|
|
23
|
-
const REF_PREFIX = 'e';
|
|
24
|
-
/**
|
|
25
|
-
* A ref, made safe to interpolate into an error message.
|
|
26
|
-
*
|
|
27
|
-
* A ref Reticle mints is `e7` — three characters. A ref a CALLER passes is whatever they typed, and
|
|
28
|
-
* an agent guessing at the schema can pass 100KB of it. Every stale-ref message interpolates that
|
|
29
|
-
* value directly, so the caller's own bad argument came back as a 100KB error.
|
|
30
|
-
*
|
|
31
|
-
* The server caps error messages centrally, but it cannot help here: the browser's transport
|
|
32
|
-
* serializer truncates from the TAIL first, which severs "… no longer resolves to an element" — the
|
|
33
|
-
* exact substring the server's recovery table matches on. A stale ref then arrives classified as a
|
|
34
|
-
* possible Reticle defect. So the echo is bounded at the source, where the untrusted value enters
|
|
35
|
-
* the message.
|
|
36
|
-
*/
|
|
37
|
-
export function echoRef(ref) {
|
|
38
|
-
const max = TRANSPORT_LIMITS.MAX_REF_LENGTH;
|
|
39
|
-
return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Full-sweep cadence. The deref-sweep is O(retained), and a page legitimately holding ≥ MAX_TRACKED_REFS
|
|
43
|
-
* live elements (a 10k-node dashboard) can never get under the cap that way, so running it on EVERY mint
|
|
44
|
-
* over the cap turned each mint into a 10k-entry scan — measured 264µs vs 0.20µs (1,300x) on the app's
|
|
45
|
-
* main thread, ~2.6% of it forever on a churning page. The cap is instead held by an O(1) oldest-drop per
|
|
46
|
-
* over-cap mint, and the expensive dead-entry sweep runs once per this many mints (amortized O(1)).
|
|
47
|
-
*/
|
|
48
|
-
const SWEEP_EVERY_MINTS = 1000;
|
|
49
|
-
/**
|
|
50
|
-
* Where a document records the ref numbers it has claimed, so the next one starts past them.
|
|
51
|
-
*
|
|
52
|
-
* Refs are per-DOCUMENT: a reload or a cross-page link tears this module down and the next document
|
|
53
|
-
* used to start minting from `e1` again. So `e7` from page A was a valid, resolvable, DIFFERENT
|
|
54
|
-
* element on page B — nothing refused, the wrong element was acted on, and the result came back `ok`.
|
|
55
|
-
* The SPA case was always safe (WeakRef liveness + isConnected); this was the other half, and the
|
|
56
|
-
* more dangerous one, because it produced a false green instead of an error.
|
|
57
|
-
*
|
|
58
|
-
* Deliberately NOT a change to the ref grammar. Stamping refs `e7@3` would have worked too, but every
|
|
59
|
-
* agent passes refs back verbatim and every saved flow on disk already holds bare ones, so it buys a
|
|
60
|
-
* wire-format migration for a property the sequence itself can carry. A number that never restarts
|
|
61
|
-
* means a stale ref simply misses the map, and the refusal that already exists — already worded for
|
|
62
|
-
* exactly this case — fires unchanged.
|
|
63
|
-
*
|
|
64
|
-
* Namespaced like the session key it sits beside; `sessionStorage` is the right scope for the same
|
|
65
|
-
* reason session continuity uses it (survives reloads and same-tab navigations, not shared with
|
|
66
|
-
* another tab, which is a different session with its own registry anyway).
|
|
67
|
-
*/
|
|
68
|
-
const REF_BASE_KEY = '__reticle_ref_base';
|
|
69
|
-
/**
|
|
70
|
-
* How many numbers a document claims at once.
|
|
71
|
-
*
|
|
72
|
-
* The reservation is a synchronous storage write, so it cannot happen per mint — mints are frequent
|
|
73
|
-
* (every meaningful DOM addition, every transitionend, every scroll reveal). One write per this many
|
|
74
|
-
* mints is free.
|
|
75
|
-
*
|
|
76
|
-
* Sized to keep refs SHORT, and the first attempt at that reasoned about the wrong axis. It bounded
|
|
77
|
-
* how many navigations it took to reach seven digits and concluded "a hundred" — but the cost is not
|
|
78
|
-
* paid at seven digits, it is paid at every digit, on every ref, in every snapshot and every error
|
|
79
|
-
* message, on every turn. At 10,000 the SECOND document already mints five-digit refs.
|
|
80
|
-
*
|
|
81
|
-
* Measured on one unchanged three-element page across a session: 61 tokens on the first document,
|
|
82
|
-
* 64 after a reload, 65 later — +6.5% for the same page, entirely in ref width. The benchmark saw the
|
|
83
|
-
* same thing as a ~4% rise in average observation cost, which is what a benchmark is for.
|
|
84
|
-
*
|
|
85
|
-
* So the block is small. A document claims another one when it outmints this, which costs one more
|
|
86
|
-
* storage write and is exactly proportional: a page with thousands of elements has a long snapshot
|
|
87
|
-
* anyway, and a small page — the common case, and the one an agent drives repeatedly — keeps
|
|
88
|
-
* three-digit refs across dozens of navigations.
|
|
89
|
-
*/
|
|
90
|
-
export const REF_BLOCK = 100;
|
|
91
|
-
/** The reserved-block high-water mark, or 0 when storage is unavailable or its value is junk. */
|
|
92
|
-
function readBase(store) {
|
|
93
|
-
try {
|
|
94
|
-
const raw = store?.getItem(REF_BASE_KEY);
|
|
95
|
-
const value = null === raw || undefined === raw ? Number.NaN : Number(raw);
|
|
96
|
-
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return 0; // a sandboxed iframe throws on access; a session must still start
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
export class RefRegistry {
|
|
103
|
-
#toRef = new WeakMap();
|
|
104
|
-
#fromRef = new Map();
|
|
105
|
-
#storage;
|
|
106
|
-
#seq = 0;
|
|
107
|
-
/** The end of the block currently reserved. Minting past it claims the next one. */
|
|
108
|
-
#reserved = 0;
|
|
109
|
-
#mintsSinceSweep = 0;
|
|
110
|
-
/**
|
|
111
|
-
* The highest number handed out before the last hot update was applied, or 0 while none has been.
|
|
112
|
-
*
|
|
113
|
-
* A watermark rather than a per-ref stamp: the sequence never restarts within a document, so one
|
|
114
|
-
* number separates "minted before the code changed" from "minted after it" for every ref at once.
|
|
115
|
-
* The alternative — an epoch on every map entry — is bookkeeping paid on every mint for an answer
|
|
116
|
-
* needed only on the rare refusal.
|
|
117
|
-
*/
|
|
118
|
-
#editedAtSeq = 0;
|
|
119
|
-
/**
|
|
120
|
-
* Storage is injected rather than read from the global for the same reason the clock is: it is an
|
|
121
|
-
* ambient dependency that decides behaviour, and the cross-document property is untestable if the
|
|
122
|
-
* only way to get a second document is to actually navigate.
|
|
123
|
-
*/
|
|
124
|
-
constructor(storage = () => globalThis.sessionStorage) {
|
|
125
|
-
this.#storage = storage;
|
|
126
|
-
}
|
|
127
|
-
/** Read the tab's high-water mark and claim the next block, BEFORE any of it is handed out. */
|
|
128
|
-
#reserve() {
|
|
129
|
-
let store;
|
|
130
|
-
try {
|
|
131
|
-
store = this.#storage();
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
store = undefined; // hostile storage: no cross-document guarantee, but the session still starts
|
|
135
|
-
}
|
|
136
|
-
const base = Math.max(readBase(store), this.#seq);
|
|
137
|
-
this.#reserved = base + REF_BLOCK;
|
|
138
|
-
try {
|
|
139
|
-
store?.setItem(REF_BASE_KEY, String(this.#reserved));
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
/* same: never block a mint on storage */
|
|
143
|
-
}
|
|
144
|
-
// Claimed on the FIRST mint, not at unload — a page that crashes, is killed, or navigates away
|
|
145
|
-
// without firing unload has still permanently spent its numbers.
|
|
146
|
-
this.#seq = base;
|
|
147
|
-
}
|
|
148
|
-
/** How many reverse entries are currently retained. Exposed so the bound can be asserted. */
|
|
149
|
-
get size() {
|
|
150
|
-
return this.#fromRef.size;
|
|
151
|
-
}
|
|
152
|
-
/** O(1): drop the least-recently-minted reverse entry. Map iterates in insertion order, so the front
|
|
153
|
-
* is the oldest. A still-live element whose entry is dropped re-registers on its next refFor. */
|
|
154
|
-
#evictOldest() {
|
|
155
|
-
const oldest = this.#fromRef.keys().next();
|
|
156
|
-
if (oldest.done !== true)
|
|
157
|
-
this.#fromRef.delete(oldest.value);
|
|
158
|
-
}
|
|
159
|
-
/** Full pass: drop every entry whose element has been collected or detached (frees genuinely dead
|
|
160
|
-
* bookkeeping), then any remaining excess by age. Amortized — called once per SWEEP_EVERY_MINTS. */
|
|
161
|
-
#sweep() {
|
|
162
|
-
for (const [ref, weak] of this.#fromRef) {
|
|
163
|
-
const el = weak.deref();
|
|
164
|
-
if (el === undefined || !el.isConnected)
|
|
165
|
-
this.#fromRef.delete(ref);
|
|
166
|
-
}
|
|
167
|
-
while (this.#fromRef.size > MAX_TRACKED_REFS)
|
|
168
|
-
this.#evictOldest();
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* A hot update just replaced modules in this document: every ref handed out so far was minted
|
|
172
|
-
* against the DOM the previous version of that code produced.
|
|
173
|
-
*
|
|
174
|
-
* Only the boundary is recorded. Nothing is evicted — a hot update is not a navigation, and most
|
|
175
|
-
* of the page survives one, so refs minted before it very often still resolve. This exists so the
|
|
176
|
-
* ones that DON'T can say why.
|
|
177
|
-
*/
|
|
178
|
-
markEdited() {
|
|
179
|
-
this.#editedAtSeq = this.#seq;
|
|
180
|
-
}
|
|
181
|
-
/** Was this ref handed out before the last hot update? False while no update has been observed. */
|
|
182
|
-
mintedBeforeLastEdit(ref) {
|
|
183
|
-
if (0 === this.#editedAtSeq || !ref.startsWith(REF_PREFIX))
|
|
184
|
-
return false;
|
|
185
|
-
const n = Number(ref.slice(REF_PREFIX.length));
|
|
186
|
-
return Number.isSafeInteger(n) && n > 0 && n <= this.#editedAtSeq;
|
|
187
|
-
}
|
|
188
|
-
/** Get the existing ref for an element, or mint a new one. */
|
|
189
|
-
refFor(el) {
|
|
190
|
-
const existing = this.#toRef.get(el);
|
|
191
|
-
if (existing !== undefined) {
|
|
192
|
-
// Re-register if this element's entry was evicted while it was off the agent's radar. Without
|
|
193
|
-
// this, an element that is still on the page could hold a ref that no longer resolves — the
|
|
194
|
-
// eviction would surface as "element not found", which is a wrong answer rather than a slow one.
|
|
195
|
-
// Either way, TOUCH it to the recent end (LRU): an element being surfaced right now must not be
|
|
196
|
-
// the oldest-drop victim of a mint storm before the agent gets to act on it.
|
|
197
|
-
const weak = this.#fromRef.get(existing);
|
|
198
|
-
this.#fromRef.delete(existing);
|
|
199
|
-
this.#fromRef.set(existing, weak ?? new WeakRef(el));
|
|
200
|
-
return existing;
|
|
201
|
-
}
|
|
202
|
-
if (this.#seq >= this.#reserved)
|
|
203
|
-
this.#reserve();
|
|
204
|
-
this.#seq += 1;
|
|
205
|
-
const ref = asRef(`${REF_PREFIX}${String(this.#seq)}`);
|
|
206
|
-
this.#toRef.set(el, ref);
|
|
207
|
-
this.#fromRef.set(ref, new WeakRef(el));
|
|
208
|
-
// Keep bounded cheaply on every mint; run the full dead-entry sweep only periodically.
|
|
209
|
-
this.#mintsSinceSweep += 1;
|
|
210
|
-
if (this.#mintsSinceSweep >= SWEEP_EVERY_MINTS) {
|
|
211
|
-
this.#mintsSinceSweep = 0;
|
|
212
|
-
this.#sweep();
|
|
213
|
-
}
|
|
214
|
-
else if (this.#fromRef.size > MAX_TRACKED_REFS) {
|
|
215
|
-
this.#evictOldest();
|
|
216
|
-
}
|
|
217
|
-
return ref;
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Resolve a ref back to its element, or null if it's gone/detached.
|
|
221
|
-
*
|
|
222
|
-
* Takes a plain `string` ON PURPOSE: this is the untrusted-input path — the ref comes from the agent
|
|
223
|
-
* over the wire, and a miss is answered with null rather than an error. Requiring a branded Ref here
|
|
224
|
-
* would force a meaningless cast at every wire boundary and buy nothing; the brand's value is on the
|
|
225
|
-
* MINT (refFor) so our own code cannot pass, say, a sessionId where a handle is expected.
|
|
226
|
-
*/
|
|
227
|
-
resolve(ref) {
|
|
228
|
-
const weak = this.#fromRef.get(ref);
|
|
229
|
-
if (weak === undefined)
|
|
230
|
-
return null;
|
|
231
|
-
const el = weak.deref();
|
|
232
|
-
if (el === undefined || !el.isConnected) {
|
|
233
|
-
this.#fromRef.delete(ref);
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
// LRU touch: a ref the agent just resolved is in ACTIVE use — move it to the most-recent end so an
|
|
237
|
-
// oldest-drop eviction between this resolve and the follow-up act can't reclaim it and turn a live
|
|
238
|
-
// element into a false "ref no longer resolves". O(1) — Map preserves insertion order.
|
|
239
|
-
this.#fromRef.delete(ref);
|
|
240
|
-
this.#fromRef.set(ref, weak);
|
|
241
|
-
return el;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
/** Process-wide registry shared by snapshot, query, and the action executor. */
|
|
245
|
-
export const refs = new RefRegistry();
|