1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
((globalThis) => {
const ops = Deno.core.ops;
const _cancelledTimers = new Set();
// Timer generation — bumped by `globalThis.__cancelAllTimers()` so that
// the warm-reuse path in `Page::navigate_warm` can mass-cancel every
// in-flight `setTimeout`/`setInterval` callback from the previous page
// in one O(1) op. Each callback captures the generation at schedule
// time; on resolve, if `_timerGen` has moved past it, the callback
// bails before running. Why this is needed: `op_timer_sleep` is just
// `tokio::sleep(ms)` and isn't tied to `TimerState`, so the Rust-side
// `replace_dom` doesn't preempt in-flight tokio sleeps — those still
// fire and try to run their callbacks. Without the generation guard,
// `humanize.js`'s 30 pending setTimeouts from the previous page would
// dispatch synthetic mouse events into the newly-loaded DOM.
let _timerGen = 0;
globalThis.__cancelAllTimers = function __cancelAllTimers() {
_timerGen++;
};
// W5b-PLUS (twitter/x.com hydration fix): unref every op_timer_sleep
// promise. The deno_core event loop treats every in-flight async op as
// "is_pending=true" (jsruntime.rs:2223 — has_pending_refed_ops).
// A pre-fix profile of x.com showed 33 simultaneous pending ops, ALL
// op_timer_sleep, cycling 33→18→1→33 driven by React's scheduler +
// requestIdleCallback polyfill + IntersectionObserver fanout. The
// loop never reached idle, so SPA hydration timed out at body=69 B.
//
// unrefOpPromise is the deno equivalent of node.js's `timer.unref()`:
// the promise still fires when its time comes, but it doesn't block
// the event loop from reporting "all work done". Real Chrome treats
// setTimeout-scheduled work as background work that doesn't gate
// navigation idle — this matches that semantics exactly.
//
// Side effect: if the page exits run_until_idle before a long timer
// fires, that callback runs on the *next* run_until_idle invocation
// (page.rs schedules several drains per navigate iteration). For SPA
// homepage hydration, this is exactly the desired behavior — once
// the React mount has children (page.rs:1252 early-exit), any
// outstanding setTimeout(50000, retryAnalytics) is just background
// noise we'd be killing on drop anyway.
const _unrefRaw = Deno.core.unrefOpPromise;
// Only unref *long-delay* timers (>= UNREF_THRESHOLD_MS). Short
// timers (< threshold) carry render-critical work — React's
// scheduler postTask, microtask-equivalent rIC fallback, etc. —
// and unrefing them causes run_until_idle to exit before the
// continuation runs (observed empirically on an SPA:
// unref-everything → AllWorkDone in 4 s, body=69 B; unref nothing
// → 90 s timeout, body=69 B).
//
// Threshold history:
// 1000ms — twitter/x flipped to L3 but macys/ria/threads regressed
// to THIN-BODY (their hydration uses setTimeout(fn, ~1500)
// for delayed render steps; unref'd them too eagerly).
// 2000ms — current. Keeps twitter/x (their pinning is cycles of
// hundreds of sub-second timers + occasional 5–60s
// analytics) while preserving macys/ria/threads
// ~1.5s-delay hydration callbacks as refed.
const UNREF_THRESHOLD_MS = 2000;
// On a challenge nav (page.rs sets `__keepLongTimersRefed` when the
// initial doc is an interstitial challenge), keep EVERY timer refed.
// Such challenge scripts schedule a long wait (often 5-30 s) and
// defer a proof-of-work-worker continuation behind a long
// setTimeout; unrefing those lets `run_until_idle` report AllWorkDone
// and the drain hands off before the token is posted. Real Chrome
// keeps the page alive across these waits. Gated on the flag ⇒ benign
// SPA pages keep the default unref behavior (no SPA regression).
const _maybeUnref = _unrefRaw
? (p, ms) => {
if (ms >= UNREF_THRESHOLD_MS && !globalThis.__keepLongTimersRefed) {
_unrefRaw(p);
}
}
: () => {};
globalThis.setTimeout = function setTimeout(callback, delay = 0, ...args) {
if (typeof callback !== "function") {
callback = new Function(String(callback));
}
const ms = Math.max(0, delay | 0);
const id = ops.op_set_timeout(ms);
// Async ops in deno_core 0.311 are called directly and return Promise
const p = ops.op_timer_sleep(ms);
_maybeUnref(p, ms);
const myGen = _timerGen;
p.then(() => {
if (myGen !== _timerGen) return; // post `__cancelAllTimers`, drop
if (!_cancelledTimers.has(id)) {
callback(...args);
}
});
return id;
};
// Background-timer helper for engine-internal scripts that want their
// setTimeout callbacks to fire eventually but DON'T want the timer to
// pin `run_until_idle` open. Used by `crates/browser/src/js/humanize.js`
// — its ~30 synthetic-input timers (50 ms-1.8 s spread) should not
// gate engine "idle" because the page can be returned to the caller
// the moment its own work settles; whatever humanize timers haven't
// fired yet are background no-ops for benign pages, and challenge
// pages keep the loop busy with their own scripts so the
// humanize timers still fire there. Same semantics as Node's
// `setTimeout(...).unref()`.
globalThis.__bgSetTimeout = function __bgSetTimeout(callback, delay = 0, ...args) {
if (typeof callback !== "function") {
callback = new Function(String(callback));
}
const ms = Math.max(0, delay | 0);
const id = ops.op_set_timeout(ms);
const p = ops.op_timer_sleep(ms);
if (_unrefRaw) _unrefRaw(p);
const myGen = _timerGen;
p.then(() => {
if (myGen !== _timerGen) return;
if (!_cancelledTimers.has(id)) {
callback(...args);
}
});
return id;
};
globalThis.setInterval = function setInterval(callback, delay = 0, ...args) {
if (typeof callback !== "function") {
callback = new Function(String(callback));
}
const ms = Math.max(4, delay | 0);
const id = ops.op_set_interval(ms);
const myGen = _timerGen;
function tick() {
const p = ops.op_timer_sleep(ms);
// Intervals are by definition recurring — unref them at the
// same threshold. A 5 s recurring analytics ping shouldn't
// pin the loop any more than a 5 s setTimeout.
_maybeUnref(p, ms);
p.then(() => {
if (myGen !== _timerGen) return;
if (!_cancelledTimers.has(id)) {
callback(...args);
tick();
}
});
}
tick();
return id;
};
globalThis.clearTimeout = function clearTimeout(id) {
if (id !== undefined && id !== null) {
_cancelledTimers.add(id);
ops.op_clear_timer(id);
}
};
globalThis.clearInterval = globalThis.clearTimeout;
let _rafId = 0;
const _rafCallbacks = new Map();
// RAF cadence jitter (60 Hz target, Gaussian
// σ=0.5 ms around 16.67 ms mean, clamped ≥1 ms). A perfect 16 ms
// grid differs from real Chrome, whose RAF cadence
// shows scheduler noise. Sourced from the seeded RNG (the
// Symbol-keyed slot installed by stealth_bootstrap) so the cadence
// is deterministic per session.
const _behaviorRandSym = Symbol.for('__browser_oxide_behavior_rand__');
const _rand = globalThis[_behaviorRandSym] || Math.random;
let _gaussSpare = null;
const _gauss = () => {
if (_gaussSpare !== null) {
const v = _gaussSpare;
_gaussSpare = null;
return v;
}
let u, v, s;
do {
u = _rand() * 2 - 1;
v = _rand() * 2 - 1;
s = u * u + v * v;
} while (s >= 1 || s === 0);
const mul = Math.sqrt(-2 * Math.log(s) / s);
_gaussSpare = v * mul;
return u * mul;
};
const _RAF_MEAN_MS = 16.67;
const _RAF_SIGMA_MS = 0.5;
const _rafDelayMs = () => Math.max(1, _RAF_MEAN_MS + _gauss() * _RAF_SIGMA_MS);
// Symbol-keyed exposure so chrome_compat.rs raf_cadence_jitter test
// can sample the actual delay generator (1000 callbacks ≈ 16 s wall —
// too slow to drive via real setTimeout) without triggering RAF.
try {
const _rafJitterSym = Symbol.for('__browser_oxide_raf_jitter_ms__');
Object.defineProperty(globalThis, _rafJitterSym, {
value: _rafDelayMs,
writable: false, configurable: true, enumerable: false,
});
} catch (e) {}
globalThis.requestAnimationFrame = function requestAnimationFrame(callback) {
const id = ++_rafId;
_rafCallbacks.set(id, callback);
// Fire near 60 Hz via real timer, not microtask. Some scripts
// measure RAF timing; a perfect-grid cadence (`set(diffs).size
// === 1`) differs from real Chrome.
setTimeout(() => {
const cb = _rafCallbacks.get(id);
if (cb) {
_rafCallbacks.delete(id);
cb(performance.now());
}
}, _rafDelayMs());
return id;
};
globalThis.cancelAnimationFrame = function cancelAnimationFrame(id) {
_rafCallbacks.delete(id);
};
if (!globalThis.performance) {
globalThis.performance = {};
}
if (!globalThis.performance.now) {
const startTime = Date.now();
globalThis.performance.now = function() {
return Date.now() - startTime;
};
}
// Native-code masking — some scripts read
// `Function.prototype.toString.call(setTimeout)` and friends. The
// expected serialization is `function setTimeout() { [native code] }`;
// a JS source body differs from real Chrome.
if (typeof _maskFunction === 'function') {
_maskFunction(globalThis.setTimeout, 'setTimeout');
_maskFunction(globalThis.setInterval, 'setInterval');
_maskFunction(globalThis.clearTimeout, 'clearTimeout');
_maskFunction(globalThis.clearInterval, 'clearInterval');
_maskFunction(globalThis.requestAnimationFrame, 'requestAnimationFrame');
_maskFunction(globalThis.cancelAnimationFrame, 'cancelAnimationFrame');
if (globalThis.performance && typeof globalThis.performance.now === 'function') {
_maskFunction(globalThis.performance.now, 'now');
}
}
})(globalThis);