@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
package/dist/timers/clock.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
// Fake clock: patch the APP's setTimeout/setInterval/Date.now/performance.now so the agent
|
|
2
|
-
// can deterministically advance time (toasts, debounces, auto-dismiss, commit-on-blur).
|
|
3
|
-
// We do NOT patch requestAnimationFrame/microtasks/MessageChannel — React's scheduler relies
|
|
4
|
-
// on those, and freezing them would stall the page. Opt-in + reversible.
|
|
5
1
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
2
|
+
import { captureMethod } from '../patching/capture-method.js';
|
|
3
|
+
export const FREEZE_WATCHDOG_MS = 120_000;
|
|
6
4
|
let installed = false;
|
|
7
5
|
let virtualNow = 0;
|
|
8
6
|
let realBase = 0;
|
|
9
7
|
let seq = 1;
|
|
10
8
|
let tasks = [];
|
|
11
9
|
let originals = null;
|
|
10
|
+
let watchdog;
|
|
11
|
+
let ourFakes = {};
|
|
12
|
+
function armWatchdog() {
|
|
13
|
+
if (null === originals)
|
|
14
|
+
return;
|
|
15
|
+
const natives = originals;
|
|
16
|
+
const clear = natives.clearTimeout.bind(window);
|
|
17
|
+
if (watchdog !== undefined)
|
|
18
|
+
clear(watchdog);
|
|
19
|
+
watchdog = natives.setTimeout.bind(window)(() => {
|
|
20
|
+
watchdog = undefined;
|
|
21
|
+
resetClock();
|
|
22
|
+
}, FREEZE_WATCHDOG_MS);
|
|
23
|
+
}
|
|
12
24
|
export function isClockFrozen() {
|
|
13
25
|
return installed;
|
|
14
26
|
}
|
|
@@ -18,8 +30,6 @@ export function freezeClock() {
|
|
|
18
30
|
installed = true;
|
|
19
31
|
virtualNow = 0;
|
|
20
32
|
realBase = Date.now();
|
|
21
|
-
// Captured as stored VALUES, so teardown restores the same function objects the page had. See
|
|
22
|
-
// capturedMethod: these are deliberate unbound captures and the descriptor read says so.
|
|
23
33
|
originals = {
|
|
24
34
|
setTimeout: requireCapturedMethod(window, 'setTimeout'),
|
|
25
35
|
clearTimeout: requireCapturedMethod(window, 'clearTimeout'),
|
|
@@ -36,15 +46,23 @@ export function freezeClock() {
|
|
|
36
46
|
const cancel = (id) => {
|
|
37
47
|
tasks = tasks.filter((t) => t.id !== id);
|
|
38
48
|
};
|
|
39
|
-
|
|
49
|
+
const withArgs = (cb, args) => () => {
|
|
50
|
+
cb(...args);
|
|
51
|
+
};
|
|
52
|
+
window.setTimeout = ((cb, delay = 0, ...args) => schedule(withArgs(cb, args), delay));
|
|
40
53
|
window.clearTimeout = ((id) => cancel(id));
|
|
41
|
-
window.setInterval = ((cb, delay = 0) => schedule(cb, delay, Math.max(1, delay)));
|
|
54
|
+
window.setInterval = ((cb, delay = 0, ...args) => schedule(withArgs(cb, args), delay, Math.max(1, delay)));
|
|
42
55
|
window.clearInterval = ((id) => cancel(id));
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
ourFakes = {
|
|
57
|
+
setTimeout: captureMethod(window, 'setTimeout'),
|
|
58
|
+
clearTimeout: captureMethod(window, 'clearTimeout'),
|
|
59
|
+
setInterval: captureMethod(window, 'setInterval'),
|
|
60
|
+
clearInterval: captureMethod(window, 'clearInterval'),
|
|
61
|
+
};
|
|
45
62
|
Date.now = () => realBase + virtualNow;
|
|
63
|
+
ourFakes.dateNow = captureMethod(Date, 'now');
|
|
64
|
+
armWatchdog();
|
|
46
65
|
}
|
|
47
|
-
/** Run all timers due within the next `ms` of virtual time, in order. */
|
|
48
66
|
export function advanceClock(ms) {
|
|
49
67
|
if (!installed)
|
|
50
68
|
return;
|
|
@@ -62,37 +80,33 @@ export function advanceClock(ms) {
|
|
|
62
80
|
virtualNow = next.time;
|
|
63
81
|
next.cb();
|
|
64
82
|
if (next.interval !== undefined) {
|
|
65
|
-
// Reschedule under the SAME id — a real setInterval keeps one id for its whole life. Minting a
|
|
66
|
-
// fresh id here (an earlier `id: seq++`) meant the app still held the original id, so its own
|
|
67
|
-
// clearInterval(id) after the first tick matched nothing and the interval fired forever.
|
|
68
83
|
tasks.push({ ...next, time: virtualNow + next.interval });
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
virtualNow = target;
|
|
87
|
+
armWatchdog();
|
|
72
88
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Restore the real timers AND hand back everything the app queued while frozen.
|
|
75
|
-
*
|
|
76
|
-
* The queue used to be discarded (`tasks = []`). Restoring the native functions is only half the job:
|
|
77
|
-
* the callbacks the app scheduled during the freeze live in the virtual queue, and dropping them leaves
|
|
78
|
-
* the app quietly broken in a new way — a toast that never dismisses, a retry that never fires, a
|
|
79
|
-
* session-expiry check that never runs. `Date.now()` and future timers look healthy, so nothing points
|
|
80
|
-
* at the cause. That matters most on the path this is called from: an agent freezes the clock, the
|
|
81
|
-
* bridge dies, and the developer is left with an app that was never un-frozen deliberately.
|
|
82
|
-
*
|
|
83
|
-
* Pending work is re-scheduled onto REAL timers with its remaining virtual delay (a 5s toast frozen 2s
|
|
84
|
-
* in still has ~3s to go), and the ids the app is holding keep working — see the translation note.
|
|
85
|
-
*/
|
|
86
89
|
export function resetClock() {
|
|
87
90
|
if (!installed || null === originals)
|
|
88
91
|
return;
|
|
89
92
|
const natives = originals;
|
|
90
93
|
const pending = tasks;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
if (watchdog !== undefined) {
|
|
95
|
+
natives.clearTimeout.bind(window)(watchdog);
|
|
96
|
+
watchdog = undefined;
|
|
97
|
+
}
|
|
98
|
+
const fakes = ourFakes;
|
|
99
|
+
ourFakes = {};
|
|
100
|
+
if (window.setTimeout === fakes.setTimeout)
|
|
101
|
+
window.setTimeout = natives.setTimeout;
|
|
102
|
+
if (window.clearTimeout === fakes.clearTimeout)
|
|
103
|
+
window.clearTimeout = natives.clearTimeout;
|
|
104
|
+
if (window.setInterval === fakes.setInterval)
|
|
105
|
+
window.setInterval = natives.setInterval;
|
|
106
|
+
if (window.clearInterval === fakes.clearInterval)
|
|
107
|
+
window.clearInterval = natives.clearInterval;
|
|
108
|
+
if (Date.now === fakes.dateNow)
|
|
109
|
+
Date.now = natives.dateNow;
|
|
96
110
|
const resumeFrom = virtualNow;
|
|
97
111
|
originals = null;
|
|
98
112
|
tasks = [];
|
|
@@ -100,31 +114,14 @@ export function resetClock() {
|
|
|
100
114
|
virtualNow = 0;
|
|
101
115
|
if (0 === pending.length)
|
|
102
116
|
return;
|
|
103
|
-
// Re-arm the app's pending work onto REAL timers, translating ids as we go.
|
|
104
|
-
//
|
|
105
|
-
// The app is still holding the VIRTUAL id it was handed while frozen; a native re-arm returns a
|
|
106
|
-
// different one. Without translation the app's own `clearTimeout(id)` would fail to cancel the
|
|
107
|
-
// re-armed callback AND would cancel whichever unrelated native timer happens to hold that id — both
|
|
108
|
-
// id spaces start at 1, so the collision is likely, not theoretical. An earlier version dropped
|
|
109
|
-
// intervals to dodge this and re-armed timeouts anyway, which has the identical hazard: a component
|
|
110
|
-
// that does `const t = setTimeout(hide, 5000); return () => clearTimeout(t)` cannot cancel `hide`,
|
|
111
|
-
// so it fires on an unmounted component.
|
|
112
|
-
//
|
|
113
|
-
// So keep a translation map and a clear-shim for exactly as long as re-armed work is outstanding.
|
|
114
|
-
// Nothing is dropped, and the app's handles keep working.
|
|
115
|
-
// Bound to `window`, not called off `natives`. A timer native invoked with any other receiver
|
|
116
|
-
// throws `TypeError: Illegal invocation` in a real browser — and `natives.setTimeout(...)` passes
|
|
117
|
-
// `natives`. jsdom does not enforce the receiver, so every unit test here passed while the browser
|
|
118
|
-
// threw on the one path that matters: re-arming work the app queued during the freeze. The early
|
|
119
|
-
// return above means it only fires when something IS pending, which is exactly when this function
|
|
120
|
-
// has a job to do. Caught by `bench/harness/clock-timetravel.mjs` driving real Chromium.
|
|
121
117
|
const nativeSetTimeout = natives.setTimeout.bind(window);
|
|
122
118
|
const nativeSetInterval = natives.setInterval.bind(window);
|
|
123
119
|
const reArmed = new Map();
|
|
120
|
+
const shims = {};
|
|
124
121
|
const done = (virtualId) => {
|
|
125
122
|
reArmed.delete(virtualId);
|
|
126
|
-
if (0 === reArmed.size)
|
|
127
|
-
restoreRawClears(natives);
|
|
123
|
+
if (0 === reArmed.size && shims.current !== undefined)
|
|
124
|
+
restoreRawClears(natives, shims.current);
|
|
128
125
|
};
|
|
129
126
|
for (const task of pending) {
|
|
130
127
|
if (task.interval !== undefined) {
|
|
@@ -137,12 +134,9 @@ export function resetClock() {
|
|
|
137
134
|
}, Math.max(0, task.time - resumeFrom)));
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
|
-
installTranslatingClears(reArmed, natives, done);
|
|
137
|
+
shims.current = installTranslatingClears(reArmed, natives, done);
|
|
141
138
|
}
|
|
142
|
-
/** Route a clear() for a re-armed virtual id to its real native id; everything else passes through. */
|
|
143
139
|
function installTranslatingClears(reArmed, natives, done) {
|
|
144
|
-
// `.bind(window)` for the same reason as the re-arm above: a bare `rawClear(id)` reaches the DOM
|
|
145
|
-
// with the wrong receiver and throws `Illegal invocation` in a real browser.
|
|
146
140
|
const translate = (rawClear) => {
|
|
147
141
|
const clear = rawClear.bind(window);
|
|
148
142
|
return ((id) => {
|
|
@@ -157,9 +151,14 @@ function installTranslatingClears(reArmed, natives, done) {
|
|
|
157
151
|
};
|
|
158
152
|
window.clearTimeout = translate(natives.clearTimeout);
|
|
159
153
|
window.clearInterval = translate(natives.clearInterval);
|
|
154
|
+
return {
|
|
155
|
+
clearTimeout: captureMethod(window, 'clearTimeout'),
|
|
156
|
+
clearInterval: captureMethod(window, 'clearInterval'),
|
|
157
|
+
};
|
|
160
158
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
window.clearInterval
|
|
159
|
+
function restoreRawClears(natives, shims) {
|
|
160
|
+
if (window.clearTimeout === shims.clearTimeout)
|
|
161
|
+
window.clearTimeout = natives.clearTimeout;
|
|
162
|
+
if (window.clearInterval === shims.clearInterval)
|
|
163
|
+
window.clearInterval = natives.clearInterval;
|
|
165
164
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const nativeNow: () => number;
|
|
2
|
+
export declare const nativeSetTimeout: (cb: () => void, ms?: number) => number;
|
|
3
|
+
export declare const nativeClearTimeout: (id: number) => void;
|
|
4
|
+
export declare const nativeSetInterval: (cb: () => void, ms: number) => (() => void);
|
|
5
|
+
interface FrameOutcome {
|
|
6
|
+
settled: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const boundedFrame: (budgetMs?: number) => Promise<FrameOutcome>;
|
|
9
|
+
export declare const nativeFrame: () => Promise<void>;
|
|
10
|
+
export declare const settle: (budgetMs?: number) => Promise<FrameOutcome>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const g = globalThis;
|
|
2
|
+
const realSetTimeout = 'function' === typeof g.setTimeout ? g.setTimeout.bind(g) : null;
|
|
3
|
+
const realClearTimeout = 'function' === typeof g.clearTimeout ? g.clearTimeout.bind(g) : null;
|
|
4
|
+
const realRaf = 'function' === typeof g.requestAnimationFrame ? g.requestAnimationFrame.bind(g) : null;
|
|
5
|
+
const realPerfNow = 'function' === typeof g.performance?.now ? g.performance.now.bind(g.performance) : null;
|
|
6
|
+
export const nativeNow = () => (realPerfNow ? realPerfNow() : 0);
|
|
7
|
+
export const nativeSetTimeout = (cb, ms = 0) => realSetTimeout ? realSetTimeout(cb, ms) : 0;
|
|
8
|
+
export const nativeClearTimeout = (id) => {
|
|
9
|
+
realClearTimeout?.(id);
|
|
10
|
+
};
|
|
11
|
+
export const nativeSetInterval = (cb, ms) => {
|
|
12
|
+
let stopped = false;
|
|
13
|
+
let id = 0;
|
|
14
|
+
const tick = () => {
|
|
15
|
+
if (stopped)
|
|
16
|
+
return;
|
|
17
|
+
cb();
|
|
18
|
+
if (stopped)
|
|
19
|
+
return;
|
|
20
|
+
id = nativeSetTimeout(tick, ms);
|
|
21
|
+
};
|
|
22
|
+
id = nativeSetTimeout(tick, ms);
|
|
23
|
+
return () => {
|
|
24
|
+
stopped = true;
|
|
25
|
+
nativeClearTimeout(id);
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const FRAME_BUDGET_MS = 200;
|
|
29
|
+
export const boundedFrame = (budgetMs = FRAME_BUDGET_MS) => new Promise((resolve) => {
|
|
30
|
+
let done = false;
|
|
31
|
+
const finish = (settled) => {
|
|
32
|
+
if (done)
|
|
33
|
+
return;
|
|
34
|
+
done = true;
|
|
35
|
+
nativeClearTimeout(timer);
|
|
36
|
+
resolve({ settled });
|
|
37
|
+
};
|
|
38
|
+
const timer = nativeSetTimeout(() => finish(false), budgetMs);
|
|
39
|
+
if (realRaf)
|
|
40
|
+
realRaf(() => finish(true));
|
|
41
|
+
else
|
|
42
|
+
nativeSetTimeout(() => finish(true), 0);
|
|
43
|
+
});
|
|
44
|
+
export const nativeFrame = () => boundedFrame().then(() => undefined);
|
|
45
|
+
export const settle = async (budgetMs = FRAME_BUDGET_MS) => {
|
|
46
|
+
await Promise.resolve();
|
|
47
|
+
return boundedFrame(budgetMs);
|
|
48
|
+
};
|
|
@@ -8,75 +8,24 @@ interface TransportDeps {
|
|
|
8
8
|
url: string;
|
|
9
9
|
hello: () => HelloMessage;
|
|
10
10
|
handleCommand: (command: CommandMessage) => Promise<CommandOutcome>;
|
|
11
|
-
/** Unthrottled, unpatched clock for bridge-loss timing. Defaults to nativeNow (performance.now). */
|
|
12
11
|
now?: () => number;
|
|
13
|
-
/** Fired each time the WebSocket (re-)connects to the bridge. */
|
|
14
12
|
onConnected?: () => void;
|
|
15
|
-
/**
|
|
16
|
-
* Liveness fallback: fired once when the bridge has been unreachable for SESSION_LIFECYCLE
|
|
17
|
-
*.BRIDGE_LOST_MS — i.e. the server/agent process is gone, so no server-pushed end can arrive and
|
|
18
|
-
* the SDK must end the session itself instead of showing a forever-"running" HUD.
|
|
19
|
-
*/
|
|
20
13
|
onConnectionLost?: () => void;
|
|
21
|
-
/**
|
|
22
|
-
* Fired once when the VERY FIRST connection keeps failing (never opened) — i.e. the bridge is
|
|
23
|
-
* unreachable at this URL, most often a wrong port or a container/WSL network boundary. Distinct
|
|
24
|
-
* from onConnectionLost (which is about losing an ALREADY-established session). The caller surfaces
|
|
25
|
-
* an actionable hint instead of retrying silently forever.
|
|
26
|
-
*/
|
|
27
14
|
onUnreachable?: (detail: {
|
|
28
15
|
url: string;
|
|
29
16
|
attempts: number;
|
|
30
17
|
}) => void;
|
|
31
|
-
/**
|
|
32
|
-
* Subscribe to "the tab became visible again" (default: document `visibilitychange`). Browsers
|
|
33
|
-
* throttle or suspend timers in a backgrounded tab, so the 1s reconnect loop stalls while hidden —
|
|
34
|
-
* a bridge blip during sleep/background otherwise leaves the panel stuck on "ENDED" until a manual
|
|
35
|
-
* reload. On foreground we retry IMMEDIATELY instead of waiting for the throttled timer. Returns an
|
|
36
|
-
* unsubscribe; injected so a test can drive visibility without a real document.
|
|
37
|
-
*/
|
|
38
18
|
onVisible?: (handler: () => void) => () => void;
|
|
39
|
-
/**
|
|
40
|
-
* Schedule an internal retry (default: `nativeSetTimeout`). Injected for the same reason `now` and
|
|
41
|
-
* `onVisible` are — the default is bound to the real timer at module load so a frozen app clock
|
|
42
|
-
* cannot deadlock the SDK, which also means no fake clock can observe what was scheduled.
|
|
43
|
-
*/
|
|
44
19
|
schedule?: (fn: () => void, ms: number) => void;
|
|
45
20
|
}
|
|
46
|
-
/**
|
|
47
|
-
* The ceiling on reconnect backoff.
|
|
48
|
-
*
|
|
49
|
-
* A flat 1s retry with no growth and no cap meant a tab left open against a stopped daemon made
|
|
50
|
-
* ~600 failed WebSocket opens in ten minutes — CPU, and a devtools console full of
|
|
51
|
-
* ERR_CONNECTION_REFUSED. Backing off is only safe BECAUSE this transport also reconnects on
|
|
52
|
-
* focus/visibility, so a human returning to the tab retries immediately rather than waiting out
|
|
53
|
-
* the delay; the backoff resets on that path for the same reason.
|
|
54
|
-
*/
|
|
55
21
|
export declare const RECONNECT_MAX_DELAY_MS = 30000;
|
|
56
|
-
/**
|
|
57
|
-
* The next reconnect delay: double, then clamp. Pure, and exported so the POLICY is testable —
|
|
58
|
-
* the scheduling itself runs on `nativeSetTimeout`, which is bound to the real timer at module load
|
|
59
|
-
* (deliberately, so a frozen `reticle_clock` cannot deadlock the SDK) and therefore cannot be driven
|
|
60
|
-
* by a fake clock.
|
|
61
|
-
*/
|
|
62
22
|
export declare function nextReconnectDelay(previous: number): number;
|
|
63
|
-
/** Offline queue cap. Exported so tests overflow the real bound instead of a mirrored copy of it. */
|
|
64
23
|
export declare const MAX_QUEUE = 500;
|
|
65
|
-
/** WebSocket client to the bridge. Reconnects across reloads; buffers events while down. */
|
|
66
24
|
export declare class Transport {
|
|
67
25
|
#private;
|
|
68
26
|
constructor(deps: TransportDeps);
|
|
69
27
|
connect(): void;
|
|
70
28
|
sendEvent(event: ReticleEvent): void;
|
|
71
|
-
/**
|
|
72
|
-
* Re-send the HELLO on a live socket.
|
|
73
|
-
*
|
|
74
|
-
* The hello carries `hasCapabilities`, and it goes out at connect() — before the app has had a
|
|
75
|
-
* chance to call `registerCapabilities`, which by design happens AFTER connect so `registerStore`
|
|
76
|
-
* has a live SDK to subscribe through. So an app that declared its whole testable surface still
|
|
77
|
-
* appeared to the agent as having none, and nothing ever corrected it. Cheap and idempotent: the
|
|
78
|
-
* bridge treats a hello as the session's current identity, not as a new session.
|
|
79
|
-
*/
|
|
80
29
|
reannounce(): void;
|
|
81
30
|
close(): void;
|
|
82
31
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { CHURN_TYPES, CommandMessageSchema, EventType, MessageKind, SESSION_LIFECYCLE, } from '@reticlehq/core';
|
|
2
|
-
import { nativeSetTimeout, nativeNow } from '../timers/native-timers.js';
|
|
3
|
-
import { nativeWarn } from '../timers/native-console.js';
|
|
2
|
+
import { nativeSetTimeout, nativeNow } from '../timers/native/native-timers.js';
|
|
3
|
+
import { nativeWarn } from '../timers/console/native-console.js';
|
|
4
4
|
import { safeStringify } from '../security/serialization.js';
|
|
5
|
-
/** Default visibility source: fire `handler` whenever the document returns to the foreground. */
|
|
6
5
|
function subscribeDocumentVisible(handler) {
|
|
7
6
|
if ('undefined' === typeof document)
|
|
8
7
|
return () => undefined;
|
|
@@ -13,69 +12,29 @@ function subscribeDocumentVisible(handler) {
|
|
|
13
12
|
document.addEventListener('visibilitychange', listener);
|
|
14
13
|
return () => document.removeEventListener('visibilitychange', listener);
|
|
15
14
|
}
|
|
16
|
-
/** The first retry after a drop. Kept short: the common outage is a daemon restarting. */
|
|
17
15
|
const RECONNECT_DELAY_MS = 1000;
|
|
18
|
-
/**
|
|
19
|
-
* The ceiling on reconnect backoff.
|
|
20
|
-
*
|
|
21
|
-
* A flat 1s retry with no growth and no cap meant a tab left open against a stopped daemon made
|
|
22
|
-
* ~600 failed WebSocket opens in ten minutes — CPU, and a devtools console full of
|
|
23
|
-
* ERR_CONNECTION_REFUSED. Backing off is only safe BECAUSE this transport also reconnects on
|
|
24
|
-
* focus/visibility, so a human returning to the tab retries immediately rather than waiting out
|
|
25
|
-
* the delay; the backoff resets on that path for the same reason.
|
|
26
|
-
*/
|
|
27
16
|
export const RECONNECT_MAX_DELAY_MS = 30_000;
|
|
28
|
-
/**
|
|
29
|
-
* The next reconnect delay: double, then clamp. Pure, and exported so the POLICY is testable —
|
|
30
|
-
* the scheduling itself runs on `nativeSetTimeout`, which is bound to the real timer at module load
|
|
31
|
-
* (deliberately, so a frozen `reticle_clock` cannot deadlock the SDK) and therefore cannot be driven
|
|
32
|
-
* by a fake clock.
|
|
33
|
-
*/
|
|
34
17
|
export function nextReconnectDelay(previous) {
|
|
35
18
|
return Math.min(previous * 2, RECONNECT_MAX_DELAY_MS);
|
|
36
19
|
}
|
|
37
|
-
/** Offline queue cap. Exported so tests overflow the real bound instead of a mirrored copy of it. */
|
|
38
20
|
export const MAX_QUEUE = 500;
|
|
39
|
-
/**
|
|
40
|
-
* Standard WebSocket close code 1008 ("policy violation") — the bridge sends it for TERMINAL refusals:
|
|
41
|
-
* a protocol-version mismatch (the browser package is older than the bridge and must be upgraded),
|
|
42
|
-
* auth failure, or a duplicate/invalid handshake. Reconnecting cannot fix any of those, so a blind
|
|
43
|
-
* retry loop just hammers the bridge every second forever and buries the actionable reason. 1013
|
|
44
|
-
* ("try again later" — rate/session limits) and transport-level closes stay retryable.
|
|
45
|
-
*/
|
|
46
21
|
const WS_POLICY_VIOLATION = 1008;
|
|
47
|
-
/** Warn that the bridge is unreachable after this many consecutive failed INITIAL connects (~3s). */
|
|
48
22
|
const UNREACHABLE_WARN_AFTER = 3;
|
|
49
|
-
/** WebSocket client to the bridge. Reconnects across reloads; buffers events while down. */
|
|
50
23
|
export class Transport {
|
|
51
24
|
#ws;
|
|
52
25
|
#churnQueue = [];
|
|
53
26
|
#signalQueue = [];
|
|
54
27
|
#insertOrder = 0;
|
|
55
28
|
#closed = false;
|
|
56
|
-
/** When the current continuous outage began (nativeNow), or undefined while connected. */
|
|
57
29
|
#disconnectedSince;
|
|
58
|
-
/** Current reconnect delay; grows on each failure, resets on a successful open or on focus. */
|
|
59
30
|
#reconnectDelay = RECONNECT_DELAY_MS;
|
|
60
|
-
/** Whether onConnectionLost has already fired for the current outage (fire-once). */
|
|
61
31
|
#lost = false;
|
|
62
|
-
/** True once the socket has opened at least once — gates the "unreachable" first-connect warning. */
|
|
63
32
|
#everConnected = false;
|
|
64
|
-
/** Consecutive failed INITIAL connects (before any success). */
|
|
65
33
|
#initialFailures = 0;
|
|
66
|
-
/** Whether the unreachable warning has fired (fire-once). */
|
|
67
34
|
#warnedUnreachable = false;
|
|
68
|
-
/** Events the offline queue has evicted since the last gap marker reached the bridge. */
|
|
69
35
|
#dropped = 0;
|
|
70
|
-
/**
|
|
71
|
-
* Envelope coordinates of the most recent evicted event — the pending marker's tombstone. Set while
|
|
72
|
-
* a gap is undeclared, cleared once the bridge has been told about it.
|
|
73
|
-
*/
|
|
74
36
|
#gap;
|
|
75
|
-
/** Session id, cached from the first hello(). Stable for a Transport's life — no need to rebuild the
|
|
76
|
-
* whole HelloMessage (url/title/adapters) on every inbound command just to read it. */
|
|
77
37
|
#sessionId;
|
|
78
|
-
/** Teardown for the visibility subscription (foreground-triggered reconnect), while connected. */
|
|
79
38
|
#unsubscribeVisible;
|
|
80
39
|
#deps;
|
|
81
40
|
#now;
|
|
@@ -90,18 +49,9 @@ export class Transport {
|
|
|
90
49
|
this.#unsubscribeVisible ??= (this.#deps.onVisible ?? subscribeDocumentVisible)(() => this.#onVisible());
|
|
91
50
|
this.#open();
|
|
92
51
|
}
|
|
93
|
-
/**
|
|
94
|
-
* The tab returned to the foreground. If we're disconnected (and not deliberately closed), reconnect
|
|
95
|
-
* NOW — a hidden tab's throttled timer may be minutes from firing. A no-op when a socket already
|
|
96
|
-
* exists (connected or mid-connect), so we never open a duplicate racing the scheduled retry.
|
|
97
|
-
*/
|
|
98
52
|
#onVisible() {
|
|
99
53
|
if (this.#closed)
|
|
100
54
|
return;
|
|
101
|
-
// A human just came back to the tab, which is the strongest signal the outage may be over — and
|
|
102
|
-
// it is what makes a 30s ceiling safe. Reset the backoff BEFORE the mid-connect early return: if
|
|
103
|
-
// a socket is already in flight and then fails, that retry should be prompt too, rather than
|
|
104
|
-
// inheriting a delay earned while nobody was watching.
|
|
105
55
|
this.#reconnectDelay = RECONNECT_DELAY_MS;
|
|
106
56
|
if (this.#ws !== undefined)
|
|
107
57
|
return;
|
|
@@ -113,12 +63,6 @@ export class Transport {
|
|
|
113
63
|
ws = new WebSocket(this.#deps.url);
|
|
114
64
|
}
|
|
115
65
|
catch {
|
|
116
|
-
// `new WebSocket()` throws SYNCHRONOUSLY on a mixed-content SecurityError — a ws:// URL from an
|
|
117
|
-
// https page, which Safari does not exempt for localhost the way Chrome does. Unguarded, that
|
|
118
|
-
// exception propagates out of connect() into the host app's bootstrap (the SDK's cardinal sin:
|
|
119
|
-
// it must never break the app it observes) AND leaves connect() half-run, so #connected never
|
|
120
|
-
// flips true and disconnect() can't remove the monkeypatches. Treat it like any failed open:
|
|
121
|
-
// note the outage and schedule the normal retry.
|
|
122
66
|
this.#noteOutage();
|
|
123
67
|
this.#noteInitialFailure();
|
|
124
68
|
this.#scheduleReopen();
|
|
@@ -126,18 +70,13 @@ export class Transport {
|
|
|
126
70
|
}
|
|
127
71
|
this.#ws = ws;
|
|
128
72
|
ws.onopen = () => {
|
|
129
|
-
this.#disconnectedSince = undefined;
|
|
73
|
+
this.#disconnectedSince = undefined;
|
|
130
74
|
this.#lost = false;
|
|
131
|
-
this.#everConnected = true;
|
|
132
|
-
this.#reconnectDelay = RECONNECT_DELAY_MS;
|
|
133
|
-
// HELLO is SDK-owned schema data. Preserve its pairing token; the generic sanitizer
|
|
134
|
-
// intentionally redacts fields named "token" from app-controlled payloads.
|
|
75
|
+
this.#everConnected = true;
|
|
76
|
+
this.#reconnectDelay = RECONNECT_DELAY_MS;
|
|
135
77
|
const greeting = this.#deps.hello();
|
|
136
78
|
this.#sessionId ??= greeting.sessionId;
|
|
137
79
|
ws.send(JSON.stringify(greeting));
|
|
138
|
-
// Declare any gap BEFORE replaying the backlog, stamped with the OLDEST event the queue
|
|
139
|
-
// dropped — see #sendRaw. The two queues are drained back into one stream in insertion order,
|
|
140
|
-
// so what the bridge receives is the order the page produced, minus the holes.
|
|
141
80
|
this.#sendGapMarker(ws, greeting.sessionId);
|
|
142
81
|
const replay = [...this.#churnQueue, ...this.#signalQueue].sort((a, b) => a.order - b.order);
|
|
143
82
|
for (const msg of replay)
|
|
@@ -152,10 +91,6 @@ export class Transport {
|
|
|
152
91
|
};
|
|
153
92
|
ws.onclose = (event) => {
|
|
154
93
|
this.#ws = undefined;
|
|
155
|
-
// A 1008 is the bridge saying "don't come back as you are" — a version mismatch, a rejected
|
|
156
|
-
// handshake. Retrying every second cannot change the outcome; stop and surface the reason (the
|
|
157
|
-
// bridge puts an actionable message there, e.g. "upgrade @reticlehq/browser") instead of a
|
|
158
|
-
// silent reconnect storm. End the session cleanly so the HUD doesn't hang on "running".
|
|
159
94
|
if (event.code === WS_POLICY_VIOLATION) {
|
|
160
95
|
this.#closed = true;
|
|
161
96
|
const reason = event.reason.length > 0 ? event.reason : 'policy violation';
|
|
@@ -171,16 +106,6 @@ export class Transport {
|
|
|
171
106
|
ws.close();
|
|
172
107
|
};
|
|
173
108
|
}
|
|
174
|
-
/**
|
|
175
|
-
* A scheduled reconnect — skipped if the SDK was torn down, or if a foreground-triggered reconnect
|
|
176
|
-
* already opened a socket (guard against the throttled timer racing #onVisible into a duplicate).
|
|
177
|
-
*/
|
|
178
|
-
/**
|
|
179
|
-
* Schedule the next retry and grow the delay. Nothing is scheduled once the SDK is torn down.
|
|
180
|
-
*
|
|
181
|
-
* The delay grows AFTER scheduling, so the first retry of any outage is prompt (a daemon
|
|
182
|
-
* restarting must not cost 30 seconds) and only a persistent outage backs off.
|
|
183
|
-
*/
|
|
184
109
|
#scheduleReopen() {
|
|
185
110
|
if (this.#closed)
|
|
186
111
|
return;
|
|
@@ -194,11 +119,6 @@ export class Transport {
|
|
|
194
119
|
return;
|
|
195
120
|
this.#open();
|
|
196
121
|
}
|
|
197
|
-
/**
|
|
198
|
-
* The first connection has never opened: count failures and, once they cross the threshold, fire
|
|
199
|
-
* onUnreachable ONCE so the app surfaces an actionable hint (wrong port / container network) rather
|
|
200
|
-
* than retrying silently forever. Suppressed entirely once any connection has succeeded.
|
|
201
|
-
*/
|
|
202
122
|
#noteInitialFailure() {
|
|
203
123
|
if (this.#everConnected || this.#warnedUnreachable)
|
|
204
124
|
return;
|
|
@@ -208,11 +128,6 @@ export class Transport {
|
|
|
208
128
|
this.#deps.onUnreachable?.({ url: this.#deps.url, attempts: this.#initialFailures });
|
|
209
129
|
}
|
|
210
130
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Track how long the bridge has been unreachable. Once the outage exceeds BRIDGE_LOST_MS, fire
|
|
213
|
-
* onConnectionLost exactly once so the SDK can end the session (the server/agent is gone and can
|
|
214
|
-
* no longer push an end itself).
|
|
215
|
-
*/
|
|
216
131
|
#noteOutage() {
|
|
217
132
|
const at = this.#now();
|
|
218
133
|
this.#disconnectedSince ??= at;
|
|
@@ -231,10 +146,6 @@ export class Transport {
|
|
|
231
146
|
}
|
|
232
147
|
const result = CommandMessageSchema.safeParse(parsed);
|
|
233
148
|
if (!result.success) {
|
|
234
|
-
// Also once silent. A command the page cannot parse is a version skew between the SDK and the
|
|
235
|
-
// daemon, and dropping it produces the same unfalsifiable timeout every other hang produces — the one
|
|
236
|
-
// failure mode where the cause is knowable and was being thrown away. Reply if we can recover
|
|
237
|
-
// an id; a message without one is not a command we can answer at all.
|
|
238
149
|
const id = parsed?.id;
|
|
239
150
|
if ('string' === typeof id && id.length > 0) {
|
|
240
151
|
this.#sendRaw(safeStringify({
|
|
@@ -248,13 +159,6 @@ export class Transport {
|
|
|
248
159
|
return;
|
|
249
160
|
}
|
|
250
161
|
const command = result.data;
|
|
251
|
-
// #sessionId is set in onopen, which always precedes any inbound command on the same socket.
|
|
252
|
-
//
|
|
253
|
-
// A mismatch used to `return` — silently. The command then had no reply of any kind, so the agent
|
|
254
|
-
// waited out its full timeout and got "command timed out", which reads as a hung or suspended
|
|
255
|
-
// page. Measured on a Tauri shell: the session was connected and streaming events the whole time
|
|
256
|
-
// while every command vanished here, and three rounds of debugging went looking at the webview.
|
|
257
|
-
// A dropped command is a real fault, so it now says so and names both ids.
|
|
258
162
|
if (command.sessionId !== undefined && command.sessionId !== this.#sessionId) {
|
|
259
163
|
this.#sendRaw(safeStringify({
|
|
260
164
|
kind: MessageKind.COMMAND_RESULT,
|
|
@@ -291,15 +195,6 @@ export class Transport {
|
|
|
291
195
|
type: event.type,
|
|
292
196
|
});
|
|
293
197
|
}
|
|
294
|
-
/**
|
|
295
|
-
* Re-send the HELLO on a live socket.
|
|
296
|
-
*
|
|
297
|
-
* The hello carries `hasCapabilities`, and it goes out at connect() — before the app has had a
|
|
298
|
-
* chance to call `registerCapabilities`, which by design happens AFTER connect so `registerStore`
|
|
299
|
-
* has a live SDK to subscribe through. So an app that declared its whole testable surface still
|
|
300
|
-
* appeared to the agent as having none, and nothing ever corrected it. Cheap and idempotent: the
|
|
301
|
-
* bridge treats a hello as the session's current identity, not as a new session.
|
|
302
|
-
*/
|
|
303
198
|
reannounce() {
|
|
304
199
|
if (this.#ws === undefined || this.#ws.readyState !== WebSocket.OPEN)
|
|
305
200
|
return;
|
|
@@ -311,17 +206,9 @@ export class Transport {
|
|
|
311
206
|
return;
|
|
312
207
|
}
|
|
313
208
|
if (this.#churnQueue.length + this.#signalQueue.length >= MAX_QUEUE) {
|
|
314
|
-
// Prefer sacrificing churn (high-volume, low-signal) over signal events. Falls back to signal
|
|
315
|
-
// FIFO only when the churn queue is empty — the buffer is bounded either way.
|
|
316
209
|
const evicted = this.#churnQueue.length > 0 ? this.#churnQueue.shift() : this.#signalQueue.shift();
|
|
317
210
|
if (evicted?.event !== undefined) {
|
|
318
211
|
this.#dropped += 1;
|
|
319
|
-
// Keep the EARLIEST drop, not the latest. Under plain FIFO the two were the same thing, so
|
|
320
|
-
// "latest evicted" was a precise boundary: everything before it was gone. Churn-first
|
|
321
|
-
// eviction breaks that — a churn event newer than a surviving signal event can be the one
|
|
322
|
-
// sacrificed, and stamping the marker with it would claim a hole that swallows events the
|
|
323
|
-
// agent is about to receive. The oldest drop is the honest floor: the hole starts no later
|
|
324
|
-
// than here. A truncation marker may over-state its reach; it must never under-state it.
|
|
325
212
|
const previous = this.#gap;
|
|
326
213
|
if (previous === undefined || evicted.event.t < previous.t)
|
|
327
214
|
this.#gap = evicted.event;
|
|
@@ -336,19 +223,6 @@ export class Transport {
|
|
|
336
223
|
this.#signalQueue.push({ text, event, order });
|
|
337
224
|
}
|
|
338
225
|
}
|
|
339
|
-
/**
|
|
340
|
-
* Tell the bridge the offline queue swallowed events, then consider the gap declared.
|
|
341
|
-
*
|
|
342
|
-
* Written straight to the open socket instead of going back through `sendEvent`, for two reasons
|
|
343
|
-
* that both bite at exactly the moment the marker matters. The queue is full when a drop happens,
|
|
344
|
-
* so a queued marker would evict another real event — every declared gap widening the gap it
|
|
345
|
-
* declares. And it would re-enter `#sendRaw` from inside the eviction branch, between the `shift`
|
|
346
|
-
* and the `push`, growing the queue by one on every later send. Writing here makes both impossible.
|
|
347
|
-
*
|
|
348
|
-
* The marker inherits the seq/t of the last event it displaced. That event never reaches the
|
|
349
|
-
* bridge, so the coordinates are free, and the server orders a session by `seq` — a marker minted
|
|
350
|
-
* at reconnect would carry the highest seq and blame a segment that never had a hole in it.
|
|
351
|
-
*/
|
|
352
226
|
#sendGapMarker(ws, sessionId) {
|
|
353
227
|
const gap = this.#gap;
|
|
354
228
|
if (gap === undefined)
|
|
@@ -1,19 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the page says when its websocket never opened.
|
|
3
|
-
*
|
|
4
|
-
* Pulled out of the connect path so it is a pure string function with a test on its CLAIMS rather
|
|
5
|
-
* than a template inlined in a warn call. The rule it exists to hold: report what this page observed
|
|
6
|
-
* and what it could not determine, never a cause. See unreachable-message.test.ts.
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* The page tried, failed, and cannot see why. Names the address and the attempt count, refuses to
|
|
10
|
-
* diagnose the daemon, and lists the checks in the order that actually resolves the ambiguity.
|
|
11
|
-
*/
|
|
12
1
|
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
2
|
export declare function unreachableStripText(url: string, attempts: number): string;
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the page says when its websocket never opened.
|
|
3
|
-
*
|
|
4
|
-
* Pulled out of the connect path so it is a pure string function with a test on its CLAIMS rather
|
|
5
|
-
* than a template inlined in a warn call. The rule it exists to hold: report what this page observed
|
|
6
|
-
* and what it could not determine, never a cause. See unreachable-message.test.ts.
|
|
7
|
-
*/
|
|
8
1
|
import { UNREACHABLE_NOTICE_PREFIX } from '@reticlehq/core';
|
|
9
|
-
/**
|
|
10
|
-
* The page tried, failed, and cannot see why. Names the address and the attempt count, refuses to
|
|
11
|
-
* diagnose the daemon, and lists the checks in the order that actually resolves the ambiguity.
|
|
12
|
-
*/
|
|
13
2
|
export function unreachableMessage(url, attempts) {
|
|
14
3
|
return (`${UNREACHABLE_NOTICE_PREFIX}${url}. ${String(attempts)} attempts, all ` +
|
|
15
4
|
`failed. That is everything the page can see: from inside the browser it cannot tell a daemon ` +
|
|
@@ -19,12 +8,6 @@ export function unreachableMessage(url, attempts) {
|
|
|
19
8
|
`different host, so set the URL explicitly (Vite: VITE_RETICLE_WS_URL, or ` +
|
|
20
9
|
`reticle.connect({ url })). An https page cannot open a ws:// socket at all. Still retrying…`);
|
|
21
10
|
}
|
|
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
11
|
export function unreachableStripText(url, attempts) {
|
|
29
12
|
return `no bridge at ${url} — ${String(attempts)} attempt${1 === attempts ? '' : 's'}`;
|
|
30
13
|
}
|