@reticlehq/browser 2.13.1 → 2.14.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 +0 -20
- package/dist/actions/actions-dom.js +0 -66
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +77 -0
- package/dist/actions/actions.d.ts +0 -89
- package/dist/actions/actions.js +24 -358
- package/dist/actions/appeared-text.d.ts +0 -20
- package/dist/actions/appeared-text.js +0 -36
- package/dist/actions/danger-context.d.ts +3 -0
- package/dist/actions/danger-context.js +39 -0
- package/dist/actions/scroll.d.ts +0 -9
- package/dist/actions/scroll.js +0 -7
- package/dist/actions/synthetic-input.d.ts +0 -7
- package/dist/actions/synthetic-input.js +0 -27
- 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 +0 -67
- package/dist/connect-options.d.ts +2 -96
- package/dist/dom/a11y.d.ts +0 -29
- package/dist/dom/a11y.js +1 -88
- package/dist/dom/auto-anchor.d.ts +0 -40
- package/dist/dom/auto-anchor.js +0 -33
- 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 +0 -72
- package/dist/dom/dom-ignore.d.ts +0 -30
- package/dist/dom/dom-ignore.js +0 -47
- package/dist/dom/occlusion.d.ts +0 -7
- package/dist/dom/occlusion.js +0 -7
- package/dist/dom/query.d.ts +0 -14
- package/dist/dom/query.js +48 -214
- package/dist/dom/realm.d.ts +0 -20
- package/dist/dom/realm.js +0 -28
- package/dist/dom/refs.d.ts +0 -76
- package/dist/dom/refs.js +2 -133
- 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 +26 -151
- package/dist/dom/source.d.ts +0 -44
- package/dist/dom/source.js +0 -45
- 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 +0 -22
- package/dist/edit-epoch.js +0 -52
- package/dist/index.js +1 -23
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +0 -3
- package/dist/observers/blind-spots.d.ts +0 -26
- package/dist/observers/blind-spots.js +2 -61
- 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 +0 -44
- package/dist/observers/download.d.ts +1 -2
- package/dist/observers/download.js +38 -55
- 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 +1 -33
- 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/network-body.d.ts +5 -28
- package/dist/observers/network-body.js +11 -108
- package/dist/observers/network-redact.d.ts +1 -20
- package/dist/observers/network-redact.js +1 -80
- package/dist/observers/network-stream.d.ts +0 -21
- package/dist/observers/network-stream.js +1 -44
- package/dist/observers/network.d.ts +2 -26
- package/dist/observers/network.js +177 -294
- 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 +0 -9
- 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/overlay.d.ts +0 -1
- package/dist/presenter/overlay.js +0 -1
- package/dist/presenter/presenter-brand.d.ts +0 -19
- package/dist/presenter/presenter-brand.js +0 -21
- package/dist/presenter/presenter-config.d.ts +1 -105
- package/dist/presenter/presenter-config.js +2 -91
- package/dist/presenter/presenter-controls.d.ts +0 -29
- package/dist/presenter/presenter-controls.js +3 -131
- package/dist/presenter/presenter-dock-layout.d.ts +0 -3
- package/dist/presenter/presenter-dock-layout.js +0 -4
- package/dist/presenter/presenter-drag.d.ts +2 -24
- package/dist/presenter/presenter-drag.js +0 -26
- package/dist/presenter/presenter-effects.d.ts +0 -9
- package/dist/presenter/presenter-effects.js +0 -9
- package/dist/presenter/presenter-glow.d.ts +0 -21
- package/dist/presenter/presenter-glow.js +1 -27
- package/dist/presenter/presenter-heroicons-data.d.ts +0 -8
- package/dist/presenter/presenter-heroicons-data.js +0 -8
- package/dist/presenter/presenter-hud-chrome.d.ts +0 -10
- package/dist/presenter/presenter-hud-chrome.js +0 -10
- package/dist/presenter/presenter-icons.d.ts +0 -6
- package/dist/presenter/presenter-icons.js +0 -6
- package/dist/presenter/presenter-log.d.ts +0 -38
- package/dist/presenter/presenter-log.js +1 -45
- package/dist/presenter/presenter-report-copy.d.ts +5 -66
- package/dist/presenter/presenter-report-copy.js +5 -66
- package/dist/presenter/presenter-report-styles.d.ts +0 -6
- package/dist/presenter/presenter-report-styles.js +6 -6
- package/dist/presenter/presenter-report.d.ts +4 -7
- package/dist/presenter/presenter-report.js +18 -76
- package/dist/presenter/presenter-run-state.d.ts +0 -7
- package/dist/presenter/presenter-run-state.js +0 -5
- package/dist/presenter/presenter-settings-styles.d.ts +0 -1
- package/dist/presenter/presenter-settings-styles.js +0 -1
- package/dist/presenter/presenter-settings.d.ts +1 -23
- package/dist/presenter/presenter-settings.js +1 -31
- package/dist/presenter/presenter-shell-styles.js +2 -5
- package/dist/presenter/presenter-shell.d.ts +2 -18
- package/dist/presenter/presenter-shell.js +1 -73
- package/dist/presenter/presenter-styles.d.ts +0 -9
- package/dist/presenter/presenter-styles.js +0 -9
- package/dist/presenter/presenter-tally.d.ts +0 -5
- package/dist/presenter/presenter-tally.js +0 -4
- package/dist/presenter/presenter-test-helpers.d.ts +0 -2
- package/dist/presenter/presenter-test-helpers.js +0 -2
- package/dist/presenter/presenter-verbs.js +0 -7
- package/dist/presenter/presenter-workspace.d.ts +0 -4
- package/dist/presenter/presenter-workspace.js +1 -8
- package/dist/presenter/presenter.d.ts +0 -60
- package/dist/presenter/presenter.js +4 -118
- 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 +0 -49
- package/dist/registry/adapters.d.ts +0 -10
- package/dist/registry/adapters.js +0 -10
- 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/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 +0 -10
- package/dist/registry/store-adapters.d.ts +0 -186
- package/dist/registry/store-adapters.js +0 -163
- package/dist/registry/stores.d.ts +0 -33
- package/dist/registry/stores.js +0 -59
- package/dist/reticle-presenter-helpers.d.ts +0 -13
- package/dist/reticle-presenter-helpers.js +0 -14
- package/dist/reticle.d.ts +1 -52
- package/dist/reticle.js +20 -145
- package/dist/review/annotator-styles.js +0 -7
- package/dist/review/annotator.d.ts +2 -13
- package/dist/review/annotator.js +0 -40
- package/dist/review/mark-anchor.d.ts +0 -11
- package/dist/review/mark-anchor.js +0 -8
- 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/native-console.js +0 -4
- package/dist/timers/native-timers.d.ts +0 -25
- package/dist/timers/native-timers.js +3 -33
- package/dist/transport/transport.d.ts +0 -51
- package/dist/transport/transport.js +3 -129
- 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 +2 -2
|
@@ -1,33 +1,13 @@
|
|
|
1
1
|
import { isSensitiveKey, scrubKnownSecrets } from '@reticlehq/core';
|
|
2
|
-
/**
|
|
3
|
-
* What the transport caps removed from a value, if anything.
|
|
4
|
-
*
|
|
5
|
-
* Truncation used to be entirely silent: a 1,000-entity store came back as ~142 entities with no
|
|
6
|
-
* marker and no count, and a caller comparing it against expected data would conclude the app had
|
|
7
|
-
* lost the rest. A partial answer that cannot be distinguished from a complete one is the precise
|
|
8
|
-
* shape of a false green — the failure this project exists to prevent — so the caps now report.
|
|
9
|
-
*/
|
|
10
2
|
export interface TruncationReport {
|
|
11
|
-
/** Whole collection items not included, either past the item cap or past the node budget. */
|
|
12
3
|
droppedItems: number;
|
|
13
|
-
/** Values replaced by the `[TRUNCATED]` placeholder because a cap was hit inside them. */
|
|
14
4
|
truncatedValues: number;
|
|
15
|
-
/** Human-and-agent readable summary. Present so a consumer never has to compose one. */
|
|
16
5
|
note: string;
|
|
17
6
|
}
|
|
18
|
-
/** Convert arbitrary app state into a bounded, redacted JSON-compatible value. */
|
|
19
7
|
export declare function sanitizeForTransport(value: unknown): unknown;
|
|
20
|
-
/**
|
|
21
|
-
* As `sanitizeForTransport`, but also reports what the caps removed.
|
|
22
|
-
*
|
|
23
|
-
* Returned as a separate field rather than embedded in the value: the value has to keep the exact
|
|
24
|
-
* shape the consumer's schema declares, so a marker spliced inside it would break validation on the
|
|
25
|
-
* very payloads that are already under pressure.
|
|
26
|
-
*/
|
|
27
8
|
export declare function sanitizeWithReport(value: unknown): {
|
|
28
9
|
value: unknown;
|
|
29
10
|
truncation?: TruncationReport;
|
|
30
11
|
};
|
|
31
|
-
/** Serialize without allowing cycles, BigInt, getters, or secrets to break the transport. */
|
|
32
12
|
export declare function safeStringify(value: unknown): string;
|
|
33
13
|
export { isSensitiveKey, scrubKnownSecrets };
|
|
@@ -3,8 +3,6 @@ const TRUNCATED_VALUE = '[TRUNCATED]';
|
|
|
3
3
|
const UNSERIALIZABLE_VALUE = '[UNSERIALIZABLE]';
|
|
4
4
|
const OMIT_VALUE = Symbol('omit');
|
|
5
5
|
const MAX_KEY_LENGTH = 256;
|
|
6
|
-
// UTF-8 encodes at most ~3 bytes per JS (UTF-16) code unit, so /4 is a provably-safe bound that never
|
|
7
|
-
// exceeds the byte budget — /8 was ~1/8 of the real 1MiB wire budget and truncated legitimate state.
|
|
8
6
|
const MAX_TOTAL_CHARACTERS = Math.floor(TRANSPORT_LIMITS.MAX_MESSAGE_BYTES / 4);
|
|
9
7
|
const MAX_TOTAL_NODES = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS * 5;
|
|
10
8
|
function boundedString(value, state, max) {
|
|
@@ -13,9 +11,6 @@ function boundedString(value, state, max) {
|
|
|
13
11
|
state.remainingCharacters -= value.length;
|
|
14
12
|
return value;
|
|
15
13
|
}
|
|
16
|
-
// Count the cut. A silently shortened string is the same false green the collection caps were made
|
|
17
|
-
// to report: a caller comparing a 500k-char value against expected data sees 65k and concludes the
|
|
18
|
-
// app dropped the rest, with no marker saying WE did. The report interface exists for exactly this.
|
|
19
14
|
state.truncatedValues += 1;
|
|
20
15
|
const truncated = allowed <= TRUNCATED_VALUE.length
|
|
21
16
|
? TRUNCATED_VALUE.slice(0, allowed)
|
|
@@ -45,23 +40,13 @@ function sanitize(value, state, depth, key) {
|
|
|
45
40
|
if ('undefined' === typeof value || 'function' === typeof value || 'symbol' === typeof value) {
|
|
46
41
|
return OMIT_VALUE;
|
|
47
42
|
}
|
|
48
|
-
// An invalid Date (`new Date(NaN)` / `new Date(undefined)`, easy to land in app state) throws
|
|
49
|
-
// RangeError on toISOString — and only safeStringify catches it, so a direct sanitize caller
|
|
50
|
-
// (readStoresWithTruncation, the state observer) crashed the WHOLE state read over one bad Date.
|
|
51
|
-
// Degrade to null, the same way a non-finite number does two branches up.
|
|
52
43
|
if (value instanceof Date)
|
|
53
44
|
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
54
|
-
// Map and Set are common enough in app state (a normalized store keyed by id, a set of selected
|
|
55
|
-
// ids) that serializing them to `{}` — which is what the plain-object branch below does, since they
|
|
56
|
-
// have no enumerable own keys — is a false green: an agent reads empty and concludes the state is
|
|
57
|
-
// empty, when really it was unrepresentable. Convert to a readable shape, bounded by the same caps.
|
|
58
45
|
if (value instanceof Map) {
|
|
59
46
|
const out = Object.create(null);
|
|
60
47
|
let n = 0;
|
|
61
48
|
for (const [k, v] of value) {
|
|
62
49
|
if (state.nodes >= MAX_TOTAL_NODES || n >= TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS) {
|
|
63
|
-
// Budget spent — STOP. `continue` iterated a million-entry Map to the end just to count drops
|
|
64
|
-
// one at a time; size-n records the remainder in one shot (like the typed-array branch).
|
|
65
50
|
state.droppedItems += value.size - n;
|
|
66
51
|
break;
|
|
67
52
|
}
|
|
@@ -78,7 +63,7 @@ function sanitize(value, state, depth, key) {
|
|
|
78
63
|
let n = 0;
|
|
79
64
|
for (const item of value) {
|
|
80
65
|
if (state.nodes >= MAX_TOTAL_NODES || n >= TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS) {
|
|
81
|
-
state.droppedItems += value.size - n;
|
|
66
|
+
state.droppedItems += value.size - n;
|
|
82
67
|
break;
|
|
83
68
|
}
|
|
84
69
|
const sv = sanitize(item, state, depth + 1);
|
|
@@ -87,10 +72,6 @@ function sanitize(value, state, depth, key) {
|
|
|
87
72
|
}
|
|
88
73
|
return out;
|
|
89
74
|
}
|
|
90
|
-
// A typed array (Uint8Array, Float32Array, …) is array-like DATA, not a plain object. The
|
|
91
|
-
// plain-object branch below turned it into index-keyed keys ({"0":1,"1":2,…}) — a shape lie: an
|
|
92
|
-
// agent reading a tensor/binary field saw an object where an array lives, and a big one blew the key
|
|
93
|
-
// cap silently. Emit its numbers as a bounded array like a Set. DataView is opaque bytes — leave it.
|
|
94
75
|
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
95
76
|
const len = value.length;
|
|
96
77
|
const out = [];
|
|
@@ -117,35 +98,15 @@ function sanitize(value, state, depth, key) {
|
|
|
117
98
|
state.seen.add(value);
|
|
118
99
|
try {
|
|
119
100
|
if (Array.isArray(value)) {
|
|
120
|
-
// Truncate by dropping whole ITEMS, never by corrupting them.
|
|
121
|
-
//
|
|
122
|
-
// The node budget used to run out mid-collection, so later items kept their shape while
|
|
123
|
-
// individual fields became the string "[TRUNCATED]" — an array field turned into a string, a
|
|
124
|
-
// boolean into a string. Consumers declare output schemas over these payloads, so that was not
|
|
125
|
-
// a degraded answer: validation rejected the whole message and the caller received NOTHING. On
|
|
126
|
-
// a page with thousands of matches that is a total loss of the query tool, in exactly the
|
|
127
|
-
// conditions where it matters most.
|
|
128
|
-
//
|
|
129
|
-
// Stopping BEFORE an item that will not fit keeps every survivor whole and type-correct. The
|
|
130
|
-
// collection's true size travels separately as a scalar (serialized first), so "how many" stays
|
|
131
|
-
// exact while the sample shrinks.
|
|
132
101
|
const out = [];
|
|
133
102
|
const considered = value.slice(0, TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS);
|
|
134
|
-
// Items beyond the collection cap are dropped before the loop even sees them; count them here
|
|
135
|
-
// so the report covers BOTH reasons a caller is holding a short list.
|
|
136
103
|
state.droppedItems += Math.max(0, value.length - considered.length);
|
|
137
104
|
for (const item of considered) {
|
|
138
105
|
if (state.nodes >= MAX_TOTAL_NODES) {
|
|
139
106
|
state.droppedItems += 1;
|
|
140
107
|
continue;
|
|
141
108
|
}
|
|
142
|
-
// Checking only BEFORE the item is not enough: one that starts under the budget can cross it
|
|
143
|
-
// partway through and come back with its tail replaced by placeholders. Serialize, then keep
|
|
144
|
-
// it only if it fitted — otherwise discard it and stop, so what ships is always whole.
|
|
145
109
|
const sanitized = sanitize(item, state, depth + 1);
|
|
146
|
-
// `>=`, not `>`: sanitize stops AT the ceiling rather than passing it, so the budget never
|
|
147
|
-
// reads as exceeded. Reaching it during this item means some field inside was replaced by a
|
|
148
|
-
// placeholder, which makes the item schema-invalid — drop it and stop.
|
|
149
110
|
if (state.nodes >= MAX_TOTAL_NODES) {
|
|
150
111
|
state.droppedItems += 1;
|
|
151
112
|
continue;
|
|
@@ -155,24 +116,9 @@ function sanitize(value, state, depth, key) {
|
|
|
155
116
|
return out;
|
|
156
117
|
}
|
|
157
118
|
const out = Object.create(null);
|
|
158
|
-
// Scalars first, collections second. The node budget is spent in iteration order, so a large
|
|
159
|
-
// array sitting earlier in the object would consume the whole budget and turn the scalars after
|
|
160
|
-
// it into "[TRUNCATED]". Those scalars are the summary fields — a match `count`, a `total`, a
|
|
161
|
-
// status — and a summary that degrades to a placeholder while its own sample survives is the
|
|
162
|
-
// worst trade available: the caller keeps a partial list and loses the number that says the list
|
|
163
|
-
// is partial. Ordering by cost makes that impossible regardless of how a producer writes its
|
|
164
|
-
// object literal, so no caller has to know this rule to stay correct.
|
|
165
119
|
const allKeys = Object.keys(value);
|
|
166
120
|
const keys = allKeys.slice(0, TRANSPORT_LIMITS.MAX_OBJECT_KEYS);
|
|
167
|
-
// Keys past the cap are dropped — count them, like the array/Map/Set paths do, so a 5,000-key
|
|
168
|
-
// store reported as 200 keys carries a droppedItems marker instead of reading as complete.
|
|
169
121
|
state.droppedItems += Math.max(0, allKeys.length - keys.length);
|
|
170
|
-
// Read each key's value ONCE, inside the guard. A property getter can throw — MobX strict mode,
|
|
171
|
-
// a Vue reactive trap read outside a reactive context, any hostile Proxy — and that throw must
|
|
172
|
-
// cost only its own key, not the whole object read. The earlier code dereferenced the getter in
|
|
173
|
-
// an unguarded `isScalar` (and again in the sanitize call: two invocations, doubling side
|
|
174
|
-
// effects), so one bad property lost the entire STATE_READ — the exact silent-loss shape the Date
|
|
175
|
-
// guard above was written to prevent. Snapshot first, then partition scalars-before-collections.
|
|
176
122
|
const read = keys.map((key) => {
|
|
177
123
|
try {
|
|
178
124
|
const v = value[key];
|
|
@@ -203,17 +149,9 @@ function sanitize(value, state, depth, key) {
|
|
|
203
149
|
state.seen.delete(value);
|
|
204
150
|
}
|
|
205
151
|
}
|
|
206
|
-
/** Convert arbitrary app state into a bounded, redacted JSON-compatible value. */
|
|
207
152
|
export function sanitizeForTransport(value) {
|
|
208
153
|
return sanitizeWithReport(value).value;
|
|
209
154
|
}
|
|
210
|
-
/**
|
|
211
|
-
* As `sanitizeForTransport`, but also reports what the caps removed.
|
|
212
|
-
*
|
|
213
|
-
* Returned as a separate field rather than embedded in the value: the value has to keep the exact
|
|
214
|
-
* shape the consumer's schema declares, so a marker spliced inside it would break validation on the
|
|
215
|
-
* very payloads that are already under pressure.
|
|
216
|
-
*/
|
|
217
155
|
export function sanitizeWithReport(value) {
|
|
218
156
|
const state = {
|
|
219
157
|
seen: new WeakSet(),
|
|
@@ -240,7 +178,6 @@ export function sanitizeWithReport(value) {
|
|
|
240
178
|
},
|
|
241
179
|
};
|
|
242
180
|
}
|
|
243
|
-
/** Serialize without allowing cycles, BigInt, getters, or secrets to break the transport. */
|
|
244
181
|
export function safeStringify(value) {
|
|
245
182
|
try {
|
|
246
183
|
return JSON.stringify(sanitizeForTransport(value));
|
|
@@ -249,6 +186,4 @@ export function safeStringify(value) {
|
|
|
249
186
|
return JSON.stringify(UNSERIALIZABLE_VALUE);
|
|
250
187
|
}
|
|
251
188
|
}
|
|
252
|
-
// Re-exported: these moved to @reticlehq/core (they are wire rules, not DOM rules).
|
|
253
|
-
// Kept on this module's surface so every existing importer inside the SDK is unaffected.
|
|
254
189
|
export { isSensitiveKey, scrubKnownSecrets };
|
|
@@ -1,17 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keep a tab's session id across a reload.
|
|
3
|
-
*
|
|
4
|
-
* The id was minted fresh on every `connect()`. A reload is a new document, so the page came back as
|
|
5
|
-
* a NEW session while the agent still held the old id — and every call after
|
|
6
|
-
* `reticle_navigate { reload: true }` was refused with "no browser session connected". Reported on 6
|
|
7
|
-
* of 6 apps; a reload is the most ordinary thing an agent does.
|
|
8
|
-
*
|
|
9
|
-
* `sessionStorage` is exactly the right scope: it survives reloads and same-tab navigations, and is
|
|
10
|
-
* NOT shared with another tab of the same app — so two tabs stay two sessions, which is what the
|
|
11
|
-
* multi-session tooling assumes.
|
|
12
|
-
*
|
|
13
|
-
* An explicit id still wins. `connect({ session })` and the `__reticle_session` URL param a lease
|
|
14
|
-
* stamps are callers ASSERTING an identity, not asking for one; they are also remembered, so a
|
|
15
|
-
* leased tab that reloads rejoins its own lease.
|
|
16
|
-
*/
|
|
17
1
|
export declare function rememberSessionLabel(explicit: string | undefined, store: Storage | undefined, generate: () => string): string;
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keep a tab's session id across a reload.
|
|
3
|
-
*
|
|
4
|
-
* The id was minted fresh on every `connect()`. A reload is a new document, so the page came back as
|
|
5
|
-
* a NEW session while the agent still held the old id — and every call after
|
|
6
|
-
* `reticle_navigate { reload: true }` was refused with "no browser session connected". Reported on 6
|
|
7
|
-
* of 6 apps; a reload is the most ordinary thing an agent does.
|
|
8
|
-
*
|
|
9
|
-
* `sessionStorage` is exactly the right scope: it survives reloads and same-tab navigations, and is
|
|
10
|
-
* NOT shared with another tab of the same app — so two tabs stay two sessions, which is what the
|
|
11
|
-
* multi-session tooling assumes.
|
|
12
|
-
*
|
|
13
|
-
* An explicit id still wins. `connect({ session })` and the `__reticle_session` URL param a lease
|
|
14
|
-
* stamps are callers ASSERTING an identity, not asking for one; they are also remembered, so a
|
|
15
|
-
* leased tab that reloads rejoins its own lease.
|
|
16
|
-
*/
|
|
17
|
-
/** Where the id lives. Namespaced like the URL param it mirrors. */
|
|
18
1
|
const STORAGE_KEY = '__reticle_session';
|
|
19
2
|
export function rememberSessionLabel(explicit, store, generate) {
|
|
20
3
|
const read = () => {
|
|
@@ -23,7 +6,7 @@ export function rememberSessionLabel(explicit, store, generate) {
|
|
|
23
6
|
return value !== null && value !== undefined && value.length > 0 ? value : undefined;
|
|
24
7
|
}
|
|
25
8
|
catch {
|
|
26
|
-
return undefined;
|
|
9
|
+
return undefined;
|
|
27
10
|
}
|
|
28
11
|
};
|
|
29
12
|
const write = (value) => {
|
|
@@ -31,10 +14,8 @@ export function rememberSessionLabel(explicit, store, generate) {
|
|
|
31
14
|
store?.setItem(STORAGE_KEY, value);
|
|
32
15
|
}
|
|
33
16
|
catch {
|
|
34
|
-
/* same: never block connect on storage */
|
|
35
17
|
}
|
|
36
18
|
};
|
|
37
|
-
// `explicit` may be '' — resolveSessionLabel yields that for "none given" — and `??` would take it.
|
|
38
19
|
const asked = explicit !== undefined && explicit.length > 0 ? explicit : undefined;
|
|
39
20
|
const chosen = asked ?? read() ?? generate();
|
|
40
21
|
write(chosen);
|
package/dist/timers/clock.d.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
|
+
export declare const FREEZE_WATCHDOG_MS = 120000;
|
|
1
2
|
export declare function isClockFrozen(): boolean;
|
|
2
3
|
export declare function freezeClock(): void;
|
|
3
|
-
/** Run all timers due within the next `ms` of virtual time, in order. */
|
|
4
4
|
export declare function advanceClock(ms: number): void;
|
|
5
|
-
/**
|
|
6
|
-
* Restore the real timers AND hand back everything the app queued while frozen.
|
|
7
|
-
*
|
|
8
|
-
* The queue used to be discarded (`tasks = []`). Restoring the native functions is only half the job:
|
|
9
|
-
* the callbacks the app scheduled during the freeze live in the virtual queue, and dropping them leaves
|
|
10
|
-
* the app quietly broken in a new way — a toast that never dismisses, a retry that never fires, a
|
|
11
|
-
* session-expiry check that never runs. `Date.now()` and future timers look healthy, so nothing points
|
|
12
|
-
* at the cause. That matters most on the path this is called from: an agent freezes the clock, the
|
|
13
|
-
* bridge dies, and the developer is left with an app that was never un-frozen deliberately.
|
|
14
|
-
*
|
|
15
|
-
* Pending work is re-scheduled onto REAL timers with its remaining virtual delay (a 5s toast frozen 2s
|
|
16
|
-
* in still has ~3s to go), and the ids the app is holding keep working — see the translation note.
|
|
17
|
-
*/
|
|
18
5
|
export declare function resetClock(): void;
|
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
|
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// Native console.warn captured at module load, BEFORE installConsole patches console.
|
|
2
|
-
// SDK-internal diagnostics (transport failures, unreachable-bridge, store registration warnings)
|
|
3
|
-
// MUST use this instead of bare `console.warn` — otherwise they emit spurious CONSOLE_WARN
|
|
4
|
-
// events to the agent, polluting the observation stream with SDK internals.
|
|
5
1
|
const g = globalThis;
|
|
6
2
|
const realWarn = 'function' === typeof g.console?.warn ? g.console.warn.bind(g.console) : null;
|
|
7
3
|
export const nativeWarn = (...args) => {
|
|
@@ -1,36 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Monotonic "now" bound at load. clock.ts deliberately does NOT patch performance.now, so this
|
|
3
|
-
* stays live even when the app clock (reticle_clock) is frozen — the presenter state machine relies
|
|
4
|
-
* on it. Falls back to 0 in SSR where performance is absent.
|
|
5
|
-
*/
|
|
6
1
|
export declare const nativeNow: () => number;
|
|
7
2
|
export declare const nativeSetTimeout: (cb: () => void, ms?: number) => number;
|
|
8
3
|
export declare const nativeClearTimeout: (id: number) => void;
|
|
9
|
-
/**
|
|
10
|
-
* A self-rescheduling interval built on the bound real timer. We do NOT use
|
|
11
|
-
* `setInterval` because reticle_clock can freeze the app's timers — a native, pre-bound
|
|
12
|
-
* timer keeps the page-health heartbeat ticking so a frozen clock never reads as stale.
|
|
13
|
-
* Returns a stop function.
|
|
14
|
-
*/
|
|
15
4
|
export declare const nativeSetInterval: (cb: () => void, ms: number) => (() => void);
|
|
16
|
-
/** Outcome of a bounded frame/settle wait. */
|
|
17
5
|
interface FrameOutcome {
|
|
18
|
-
/** true = a real rAF fired within budget; false = the timeout fallback fired. */
|
|
19
6
|
settled: boolean;
|
|
20
7
|
}
|
|
21
|
-
/**
|
|
22
|
-
* One animation frame, BOUNDED: races real rAF against a nativeSetTimeout(FRAME_BUDGET_MS).
|
|
23
|
-
* In a throttled/background tab rAF never fires, so the timer wins and we still resolve.
|
|
24
|
-
* Returns whether rAF won (settled:true) or the fallback fired (settled:false).
|
|
25
|
-
*/
|
|
26
8
|
export declare const boundedFrame: (budgetMs?: number) => Promise<FrameOutcome>;
|
|
27
|
-
/** One animation frame, BOUNDED — delegates to boundedFrame so it never hangs in a throttled tab. */
|
|
28
9
|
export declare const nativeFrame: () => Promise<void>;
|
|
29
|
-
/**
|
|
30
|
-
* "Settle": let the framework flush. Awaits a microtask then one BOUNDED animation frame so a
|
|
31
|
-
* React commit (and the MutationObserver records it triggers) lands before the caller returns.
|
|
32
|
-
* Uses native timers/rAF so a frozen app clock (reticle_clock) never stalls it, AND is bounded so a
|
|
33
|
-
* throttled/background tab (rAF never fires) never deadlocks. Reports whether a real frame fired.
|
|
34
|
-
*/
|
|
35
10
|
export declare const settle: (budgetMs?: number) => Promise<FrameOutcome>;
|
|
36
11
|
export {};
|
|
@@ -1,30 +1,13 @@
|
|
|
1
|
-
// Native timers captured (bound) at module load, BEFORE any fake-clock patch. The SDK uses
|
|
2
|
-
// these for its own internal waits (presenter pacing, hover dwell, transport reconnect) so
|
|
3
|
-
// that freezing the app's clock (reticle_clock) never deadlocks Reticle itself.
|
|
4
|
-
//
|
|
5
|
-
// Important: we bind the reference now. Resolving `setTimeout` at call time would pick up the
|
|
6
|
-
// patched (frozen) timer once reticle_clock freezes the clock — which would hang the SDK.
|
|
7
1
|
const g = globalThis;
|
|
8
2
|
const realSetTimeout = 'function' === typeof g.setTimeout ? g.setTimeout.bind(g) : null;
|
|
9
3
|
const realClearTimeout = 'function' === typeof g.clearTimeout ? g.clearTimeout.bind(g) : null;
|
|
10
4
|
const realRaf = 'function' === typeof g.requestAnimationFrame ? g.requestAnimationFrame.bind(g) : null;
|
|
11
5
|
const realPerfNow = 'function' === typeof g.performance?.now ? g.performance.now.bind(g.performance) : null;
|
|
12
|
-
/**
|
|
13
|
-
* Monotonic "now" bound at load. clock.ts deliberately does NOT patch performance.now, so this
|
|
14
|
-
* stays live even when the app clock (reticle_clock) is frozen — the presenter state machine relies
|
|
15
|
-
* on it. Falls back to 0 in SSR where performance is absent.
|
|
16
|
-
*/
|
|
17
6
|
export const nativeNow = () => (realPerfNow ? realPerfNow() : 0);
|
|
18
7
|
export const nativeSetTimeout = (cb, ms = 0) => realSetTimeout ? realSetTimeout(cb, ms) : 0;
|
|
19
8
|
export const nativeClearTimeout = (id) => {
|
|
20
9
|
realClearTimeout?.(id);
|
|
21
10
|
};
|
|
22
|
-
/**
|
|
23
|
-
* A self-rescheduling interval built on the bound real timer. We do NOT use
|
|
24
|
-
* `setInterval` because reticle_clock can freeze the app's timers — a native, pre-bound
|
|
25
|
-
* timer keeps the page-health heartbeat ticking so a frozen clock never reads as stale.
|
|
26
|
-
* Returns a stop function.
|
|
27
|
-
*/
|
|
28
11
|
export const nativeSetInterval = (cb, ms) => {
|
|
29
12
|
let stopped = false;
|
|
30
13
|
let id = 0;
|
|
@@ -42,13 +25,7 @@ export const nativeSetInterval = (cb, ms) => {
|
|
|
42
25
|
nativeClearTimeout(id);
|
|
43
26
|
};
|
|
44
27
|
};
|
|
45
|
-
/** Max time we wait for a real animation frame before giving up and resolving anyway. */
|
|
46
28
|
const FRAME_BUDGET_MS = 200;
|
|
47
|
-
/**
|
|
48
|
-
* One animation frame, BOUNDED: races real rAF against a nativeSetTimeout(FRAME_BUDGET_MS).
|
|
49
|
-
* In a throttled/background tab rAF never fires, so the timer wins and we still resolve.
|
|
50
|
-
* Returns whether rAF won (settled:true) or the fallback fired (settled:false).
|
|
51
|
-
*/
|
|
52
29
|
export const boundedFrame = (budgetMs = FRAME_BUDGET_MS) => new Promise((resolve) => {
|
|
53
30
|
let done = false;
|
|
54
31
|
const finish = (settled) => {
|
|
@@ -62,17 +39,10 @@ export const boundedFrame = (budgetMs = FRAME_BUDGET_MS) => new Promise((resolve
|
|
|
62
39
|
if (realRaf)
|
|
63
40
|
realRaf(() => finish(true));
|
|
64
41
|
else
|
|
65
|
-
nativeSetTimeout(() => finish(true), 0);
|
|
42
|
+
nativeSetTimeout(() => finish(true), 0);
|
|
66
43
|
});
|
|
67
|
-
/** One animation frame, BOUNDED — delegates to boundedFrame so it never hangs in a throttled tab. */
|
|
68
44
|
export const nativeFrame = () => boundedFrame().then(() => undefined);
|
|
69
|
-
/**
|
|
70
|
-
* "Settle": let the framework flush. Awaits a microtask then one BOUNDED animation frame so a
|
|
71
|
-
* React commit (and the MutationObserver records it triggers) lands before the caller returns.
|
|
72
|
-
* Uses native timers/rAF so a frozen app clock (reticle_clock) never stalls it, AND is bounded so a
|
|
73
|
-
* throttled/background tab (rAF never fires) never deadlocks. Reports whether a real frame fired.
|
|
74
|
-
*/
|
|
75
45
|
export const settle = async (budgetMs = FRAME_BUDGET_MS) => {
|
|
76
|
-
await Promise.resolve();
|
|
77
|
-
return boundedFrame(budgetMs);
|
|
46
|
+
await Promise.resolve();
|
|
47
|
+
return boundedFrame(budgetMs);
|
|
78
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
|
}
|