@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.
Files changed (244) hide show
  1. package/dist/actions/actions-dom.d.ts +8 -20
  2. package/dist/actions/actions-dom.js +70 -69
  3. package/dist/actions/actions-press.d.ts +14 -0
  4. package/dist/actions/actions-press.js +123 -0
  5. package/dist/actions/actions.d.ts +1 -89
  6. package/dist/actions/actions.js +64 -365
  7. package/dist/actions/appeared-text.d.ts +0 -20
  8. package/dist/actions/appeared-text.js +6 -36
  9. package/dist/actions/danger-context.d.ts +3 -0
  10. package/dist/actions/danger-context.js +39 -0
  11. package/dist/actions/scroll.d.ts +8 -10
  12. package/dist/actions/scroll.js +28 -17
  13. package/dist/actions/synthetic/synthetic-input.d.ts +2 -0
  14. package/dist/actions/synthetic/synthetic-input.js +13 -0
  15. package/dist/actions/value-input.d.ts +0 -30
  16. package/dist/actions/value-input.js +0 -33
  17. package/dist/commands/commands.d.ts +0 -2
  18. package/dist/commands/commands.js +10 -70
  19. package/dist/connect-options.d.ts +2 -96
  20. package/dist/dom/a11y.d.ts +0 -29
  21. package/dist/dom/a11y.js +3 -90
  22. package/dist/dom/addressing/auto-anchor.d.ts +25 -0
  23. package/dist/dom/addressing/auto-anchor.js +41 -0
  24. package/dist/dom/addressing/refs.d.ts +14 -0
  25. package/dist/dom/addressing/refs.js +114 -0
  26. package/dist/dom/addressing/source.d.ts +9 -0
  27. package/dist/dom/addressing/source.js +29 -0
  28. package/dist/dom/chart.d.ts +0 -61
  29. package/dist/dom/chart.js +0 -60
  30. package/dist/dom/desktop-capture.d.ts +0 -30
  31. package/dist/dom/desktop-capture.js +1 -73
  32. package/dist/dom/dom-ignore.d.ts +0 -30
  33. package/dist/dom/dom-ignore.js +1 -48
  34. package/dist/dom/occlusion.d.ts +0 -7
  35. package/dist/dom/occlusion.js +31 -12
  36. package/dist/dom/paint-context.d.ts +6 -0
  37. package/dist/dom/paint-context.js +38 -0
  38. package/dist/dom/query.d.ts +0 -14
  39. package/dist/dom/query.js +50 -216
  40. package/dist/dom/realm.d.ts +0 -20
  41. package/dist/dom/realm.js +0 -28
  42. package/dist/dom/shadow-registry.d.ts +0 -33
  43. package/dist/dom/shadow-registry.js +2 -72
  44. package/dist/dom/snapshot.d.ts +2 -43
  45. package/dist/dom/snapshot.js +27 -152
  46. package/dist/dom/theme.d.ts +0 -37
  47. package/dist/dom/theme.js +2 -52
  48. package/dist/dom/virtualized.d.ts +0 -17
  49. package/dist/dom/virtualized.js +0 -27
  50. package/dist/edit-epoch.d.ts +1 -23
  51. package/dist/edit-epoch.js +1 -53
  52. package/dist/index.d.ts +4 -5
  53. package/dist/index.js +4 -28
  54. package/dist/observers/animation.d.ts +0 -1
  55. package/dist/observers/animation.js +1 -4
  56. package/dist/observers/blind-spots.d.ts +0 -26
  57. package/dist/observers/blind-spots.js +3 -62
  58. package/dist/observers/console.d.ts +1 -2
  59. package/dist/observers/console.js +41 -30
  60. package/dist/observers/context-open.d.ts +1 -11
  61. package/dist/observers/context-open.js +10 -14
  62. package/dist/observers/departure.d.ts +3 -0
  63. package/dist/observers/departure.js +52 -0
  64. package/dist/observers/dialogs.d.ts +2 -0
  65. package/dist/observers/dialogs.js +39 -0
  66. package/dist/observers/dom.js +28 -52
  67. package/dist/observers/download.d.ts +1 -2
  68. package/dist/observers/download.js +39 -56
  69. package/dist/observers/focus.d.ts +0 -6
  70. package/dist/observers/focus.js +0 -7
  71. package/dist/observers/frames.d.ts +0 -9
  72. package/dist/observers/frames.js +1 -28
  73. package/dist/observers/health.d.ts +0 -5
  74. package/dist/observers/health.js +2 -34
  75. package/dist/observers/install-all.d.ts +2 -0
  76. package/dist/observers/install-all.js +26 -27
  77. package/dist/observers/ipc.d.ts +0 -25
  78. package/dist/observers/ipc.js +0 -65
  79. package/dist/observers/navigation.d.ts +2 -0
  80. package/dist/observers/navigation.js +33 -0
  81. package/dist/observers/net-detail/network-body.d.ts +12 -0
  82. package/dist/observers/net-detail/network-body.js +71 -0
  83. package/dist/observers/net-detail/network-redact.d.ts +1 -0
  84. package/dist/observers/net-detail/network-redact.js +1 -0
  85. package/dist/observers/network-stream.d.ts +0 -21
  86. package/dist/observers/network-stream.js +3 -46
  87. package/dist/observers/network.d.ts +3 -27
  88. package/dist/observers/network.js +178 -295
  89. package/dist/observers/perf.d.ts +0 -10
  90. package/dist/observers/perf.js +0 -16
  91. package/dist/observers/route.d.ts +1 -2
  92. package/dist/observers/route.js +13 -19
  93. package/dist/observers/scroll.d.ts +0 -1
  94. package/dist/observers/scroll.js +2 -11
  95. package/dist/observers/sdk-failure.d.ts +1 -9
  96. package/dist/observers/sdk-failure.js +1 -30
  97. package/dist/observers/state.d.ts +1 -11
  98. package/dist/observers/state.js +13 -41
  99. package/dist/observers/storage.d.ts +0 -14
  100. package/dist/observers/storage.js +10 -34
  101. package/dist/observers/types.d.ts +0 -15
  102. package/dist/observers/types.js +0 -14
  103. package/dist/patching/capture-method.d.ts +0 -24
  104. package/dist/patching/capture-method.js +0 -26
  105. package/dist/presenter/chrome/layers.d.ts +2 -0
  106. package/dist/presenter/chrome/layers.js +2 -0
  107. package/dist/presenter/chrome/log-kinds.d.ts +12 -0
  108. package/dist/presenter/chrome/log-kinds.js +7 -0
  109. package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -1
  110. package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -1
  111. package/dist/presenter/chrome/presenter-brand.d.ts +3 -0
  112. package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -21
  113. package/dist/presenter/chrome/presenter-effects.d.ts +4 -0
  114. package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -10
  115. package/dist/presenter/chrome/presenter-hud-chrome.d.ts +7 -0
  116. package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -10
  117. package/dist/presenter/chrome/presenter-log.d.ts +17 -0
  118. package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +5 -54
  119. package/dist/presenter/chrome/presenter-report-copy.d.ts +51 -0
  120. package/dist/presenter/chrome/presenter-report-copy.js +95 -0
  121. package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
  122. package/dist/presenter/chrome/presenter-safe-html.js +16 -0
  123. package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -5
  124. package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -5
  125. package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -7
  126. package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -8
  127. package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -8
  128. package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -6
  129. package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -6
  130. package/dist/presenter/presenter-account-styles.d.ts +1 -0
  131. package/dist/presenter/presenter-account-styles.js +99 -0
  132. package/dist/presenter/presenter-account.d.ts +40 -0
  133. package/dist/presenter/presenter-account.js +178 -0
  134. package/dist/presenter/presenter-config.d.ts +10 -107
  135. package/dist/presenter/presenter-config.js +5 -92
  136. package/dist/presenter/presenter-controls.d.ts +1 -35
  137. package/dist/presenter/presenter-controls.js +10 -133
  138. package/dist/presenter/presenter-dock-layout.d.ts +0 -3
  139. package/dist/presenter/presenter-dock-layout.js +1 -5
  140. package/dist/presenter/presenter-drag.d.ts +2 -42
  141. package/dist/presenter/presenter-drag.js +3 -76
  142. package/dist/presenter/presenter-glow.d.ts +0 -21
  143. package/dist/presenter/presenter-glow.js +2 -28
  144. package/dist/presenter/presenter-hud-position.d.ts +18 -0
  145. package/dist/presenter/presenter-hud-position.js +50 -0
  146. package/dist/presenter/presenter-report-styles.d.ts +0 -6
  147. package/dist/presenter/presenter-report-styles.js +22 -7
  148. package/dist/presenter/presenter-report.d.ts +5 -7
  149. package/dist/presenter/presenter-report.js +44 -95
  150. package/dist/presenter/presenter-run-state.d.ts +1 -8
  151. package/dist/presenter/presenter-run-state.js +1 -6
  152. package/dist/presenter/presenter-settings-styles.d.ts +0 -1
  153. package/dist/presenter/presenter-settings-styles.js +2 -2
  154. package/dist/presenter/presenter-settings.d.ts +5 -23
  155. package/dist/presenter/presenter-settings.js +21 -33
  156. package/dist/presenter/presenter-shell-styles.js +23 -20
  157. package/dist/presenter/presenter-shell.d.ts +6 -18
  158. package/dist/presenter/presenter-shell.js +26 -77
  159. package/dist/presenter/presenter-styles.d.ts +2 -10
  160. package/dist/presenter/presenter-styles.js +6 -12
  161. package/dist/presenter/presenter-workspace.d.ts +4 -4
  162. package/dist/presenter/presenter-workspace.js +30 -10
  163. package/dist/presenter/presenter.d.ts +3 -63
  164. package/dist/presenter/presenter.js +29 -132
  165. package/dist/presenter/tour/tour-view.d.ts +33 -0
  166. package/dist/presenter/tour/tour-view.js +188 -0
  167. package/dist/presenter/tour/tour.d.ts +25 -0
  168. package/dist/presenter/tour/tour.js +245 -0
  169. package/dist/recorder/recorder-styles.d.ts +0 -6
  170. package/dist/recorder/recorder-styles.js +0 -6
  171. package/dist/recorder/recorder.d.ts +0 -34
  172. package/dist/recorder/recorder.js +6 -49
  173. package/dist/registry/auto-testids.d.ts +0 -19
  174. package/dist/registry/auto-testids.js +1 -21
  175. package/dist/registry/capabilities.d.ts +0 -31
  176. package/dist/registry/capabilities.js +1 -46
  177. package/dist/registry/commit-and-signal.d.ts +0 -6
  178. package/dist/registry/commit-and-signal.js +1 -7
  179. package/dist/registry/declared-channels.d.ts +2 -0
  180. package/dist/registry/declared-channels.js +14 -0
  181. package/dist/registry/declared-platform.d.ts +4 -0
  182. package/dist/registry/declared-platform.js +34 -0
  183. package/dist/registry/domains.d.ts +0 -8
  184. package/dist/registry/domains.js +0 -7
  185. package/dist/registry/emitter.d.ts +0 -14
  186. package/dist/registry/emitter.js +1 -11
  187. package/dist/registry/store-adapters.d.ts +0 -186
  188. package/dist/registry/store-adapters.js +1 -164
  189. package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -10
  190. package/dist/registry/{adapters.js → stores/adapters.js} +0 -10
  191. package/dist/registry/stores.d.ts +0 -33
  192. package/dist/registry/stores.js +1 -60
  193. package/dist/reticle-presenter-helpers.d.ts +0 -13
  194. package/dist/reticle-presenter-helpers.js +10 -20
  195. package/dist/reticle-singleton.d.ts +2 -0
  196. package/dist/reticle-singleton.js +3 -0
  197. package/dist/reticle.d.ts +1 -52
  198. package/dist/reticle.js +115 -180
  199. package/dist/review/annotator-styles.js +1 -8
  200. package/dist/review/annotator.d.ts +2 -13
  201. package/dist/review/annotator.js +2 -42
  202. package/dist/review/mark-anchor.d.ts +0 -11
  203. package/dist/review/mark-anchor.js +3 -11
  204. package/dist/security/serialization.d.ts +0 -20
  205. package/dist/security/serialization.js +1 -66
  206. package/dist/session-continuity.d.ts +0 -16
  207. package/dist/session-continuity.js +1 -20
  208. package/dist/timers/clock.d.ts +1 -14
  209. package/dist/timers/clock.js +59 -60
  210. package/dist/timers/console/native-console.js +5 -0
  211. package/dist/timers/native/native-timers.d.ts +11 -0
  212. package/dist/timers/native/native-timers.js +48 -0
  213. package/dist/transport/transport.d.ts +0 -51
  214. package/dist/transport/transport.js +5 -131
  215. package/dist/transport/unreachable-message.d.ts +0 -17
  216. package/dist/transport/unreachable-message.js +0 -17
  217. package/dist/util/captured-method.d.ts +0 -35
  218. package/dist/util/captured-method.js +0 -35
  219. package/package.json +5 -4
  220. package/dist/actions/synthetic-input.d.ts +0 -9
  221. package/dist/actions/synthetic-input.js +0 -40
  222. package/dist/dom/auto-anchor.d.ts +0 -65
  223. package/dist/dom/auto-anchor.js +0 -74
  224. package/dist/dom/refs.d.ts +0 -90
  225. package/dist/dom/refs.js +0 -245
  226. package/dist/dom/source.d.ts +0 -53
  227. package/dist/dom/source.js +0 -74
  228. package/dist/observers/network-body.d.ts +0 -35
  229. package/dist/observers/network-body.js +0 -168
  230. package/dist/observers/network-redact.d.ts +0 -20
  231. package/dist/observers/network-redact.js +0 -80
  232. package/dist/presenter/presenter-brand.d.ts +0 -22
  233. package/dist/presenter/presenter-effects.d.ts +0 -13
  234. package/dist/presenter/presenter-hud-chrome.d.ts +0 -17
  235. package/dist/presenter/presenter-log.d.ts +0 -64
  236. package/dist/presenter/presenter-report-copy.d.ts +0 -112
  237. package/dist/presenter/presenter-report-copy.js +0 -156
  238. package/dist/presenter/presenter-test-helpers.d.ts +0 -16
  239. package/dist/presenter/presenter-test-helpers.js +0 -41
  240. package/dist/timers/native-console.js +0 -9
  241. package/dist/timers/native-timers.d.ts +0 -36
  242. package/dist/timers/native-timers.js +0 -78
  243. /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
  244. /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
@@ -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
- window.setTimeout = ((cb, delay = 0) => schedule(cb, delay));
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
- // Note: we deliberately do NOT patch performance.now — React 19's scheduler uses it to
44
- // flush updates, and freezing it stalls re-renders. setTimeout/Date.now cover app timers.
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
- window.setTimeout = natives.setTimeout;
92
- window.clearTimeout = natives.clearTimeout;
93
- window.setInterval = natives.setInterval;
94
- window.clearInterval = natives.clearInterval;
95
- Date.now = natives.dateNow;
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
- /** Drop the shim once no re-armed work is outstanding — it costs nothing to remove and one hop to keep. */
162
- function restoreRawClears(natives) {
163
- window.clearTimeout = natives.clearTimeout;
164
- window.clearInterval = natives.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,5 @@
1
+ const g = globalThis;
2
+ const realWarn = 'function' === typeof g.console?.warn ? g.console.warn.bind(g.console) : null;
3
+ export const nativeWarn = (...args) => {
4
+ realWarn?.(...args);
5
+ };
@@ -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; // healthy again — reset the loss timer
73
+ this.#disconnectedSince = undefined;
130
74
  this.#lost = false;
131
- this.#everConnected = true; // a real connection happened ⇒ never warn "unreachable"
132
- this.#reconnectDelay = RECONNECT_DELAY_MS; // recovered — the next drop starts prompt again
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
  }