@miaskiewicz/turbo-dom 0.2.3 → 0.2.4

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/Cargo.lock CHANGED
@@ -490,7 +490,7 @@ dependencies = [
490
490
 
491
491
  [[package]]
492
492
  name = "turbo-dom-parser"
493
- version = "0.2.3"
493
+ version = "0.2.4"
494
494
  dependencies = [
495
495
  "html5ever",
496
496
  "markup5ever_rcdom",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "turbo-dom-parser"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  edition = "2021"
5
5
  description = "html5ever-backed native parser for turbo-dom (Layer 1)"
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
Binary file
Binary file
@@ -18,6 +18,10 @@ export * from './dom.mjs';
18
18
  // or force 'wasm' | 'native' | 'auto'. See parser.mjs.
19
19
  export { setParser, setParserMode, getParser } from './parser.mjs';
20
20
 
21
+ // Clock seam (public API): install a virtual clock so time-gated rAF loops (MUI
22
+ // transitions) terminate under a synchronous timer-drain render tier. See window.mjs.
23
+ export { setClock } from './window.mjs';
24
+
21
25
  // Parse cache: the SoA buffer is READ-ONLY (every mutation goes to a Document's
22
26
  // own __kids/__attrs/__cache overlay, never the buffer), so the same buffer can
23
27
  // back many Documents. Test suites call setup with the SAME html per file
@@ -81,6 +81,38 @@ function tagClass(matcher) {
81
81
  const hostSetTimeout = globalThis.setTimeout;
82
82
  const hostClearTimeout = globalThis.clearTimeout;
83
83
 
84
+ // MessageChannel/MessagePort — React 19's scheduler posts its work loop through a
85
+ // MessagePort hop (falling back to setTimeout only if absent). We ship a real,
86
+ // self-contained polyfill whose delivery is scheduled via the LIVE globalThis.
87
+ // setTimeout (read at call time, like rAF), so a render tier that owns setTimeout
88
+ // (a virtual-clock pump) catches every scheduler hop → React runs in virtual time,
89
+ // bounded by the drain/deadline — and it exists in the bare V8 isolate, which has
90
+ // no host MessageChannel. See MESSAGECHANNEL-VIRTUALIZE.md + the clock note in
91
+ // CLAUDE.md. Delivery uses delay 0 (a yield, not elapsed time — animations get
92
+ // their time from rAF's 16ms frame).
93
+ class MessagePort extends EventTarget {
94
+ constructor() { super(); this.onmessage = null; this.onmessageerror = null; this.__other = null; }
95
+ start() {}
96
+ close() { this.__other = null; }
97
+ postMessage(data) {
98
+ const target = this.__other;
99
+ if (!target) return;
100
+ (globalThis.setTimeout || hostSetTimeout)(() => {
101
+ const ev = new Event('message'); ev.data = data;
102
+ if (typeof target.onmessage === 'function') target.onmessage(ev);
103
+ target.dispatchEvent(ev);
104
+ }, 0);
105
+ }
106
+ }
107
+ class MessageChannel {
108
+ constructor() {
109
+ this.port1 = new MessagePort();
110
+ this.port2 = new MessagePort();
111
+ this.port1.__other = this.port2;
112
+ this.port2.__other = this.port1;
113
+ }
114
+ }
115
+
84
116
  // Env-independent lazy global factories — built ONCE at module load, shared by
85
117
  // every window (none capture document/url/windowProxy). Materialization still
86
118
  // self-replaces onto the per-env base, so each env gets its own instance; only
@@ -92,8 +124,13 @@ const SHARED_LAZY = {
92
124
  getComputedStyle: () => makeGetComputedStyle(),
93
125
  IntersectionObserver: () => IntersectionObserver,
94
126
  ResizeObserver: () => ResizeObserver,
95
- requestAnimationFrame: () => (cb) => hostSetTimeout(() => cb(performanceNow()), 0),
96
- cancelAnimationFrame: () => (id) => hostClearTimeout(id),
127
+ // rAF: schedule via the LIVE setTimeout (so a render tier that swaps in a timer
128
+ // queue catches the reschedule) with a non-zero frame delay, and stamp the
129
+ // callback with the injectable clock now() — a virtual clock that ADVANCES per
130
+ // frame lets time-gated transitions (MUI Fade/Grow, react-transition-group:
131
+ // progress = (now-start)/duration) reach progress>=1 and stop rescheduling.
132
+ requestAnimationFrame: () => (cb) => (globalThis.setTimeout || hostSetTimeout)(() => cb(now()), 16),
133
+ cancelAnimationFrame: () => (id) => (globalThis.clearTimeout || hostClearTimeout)(id),
97
134
  navigator: () => ({
98
135
  userAgent: 'Mozilla/5.0 (turbo-dom) AppleWebKit/537.36',
99
136
  platform: 'turbo-dom', vendor: '', language: 'en-US', languages: ['en-US'],
@@ -103,7 +140,7 @@ const SHARED_LAZY = {
103
140
  permissions: { query: async () => ({ state: 'prompt', addEventListener() {}, removeEventListener() {} }) },
104
141
  sendBeacon: () => true, vibrate: () => false,
105
142
  }),
106
- performance: () => ({ now: performanceNow, timeOrigin: 0, mark() {}, measure() {}, getEntriesByName: () => [], getEntriesByType: () => [], clearMarks() {}, clearMeasures() {} }),
143
+ performance: () => ({ now, timeOrigin: 0, mark() {}, measure() {}, getEntriesByName: () => [], getEntriesByType: () => [], clearMarks() {}, clearMeasures() {} }),
107
144
  Storage: () => Storage,
108
145
  devicePixelRatio: () => 1,
109
146
  innerWidth: () => 1024,
@@ -211,6 +248,17 @@ const performanceNow = typeof hostPerformance?.now === 'function'
211
248
  ? () => hostPerformance.now()
212
249
  : () => Date.now();
213
250
 
251
+ // Injectable clock. `window.performance.now()` and the rAF callback timestamp both
252
+ // read through now(), so a single setClock() drives both. Default (null) → real
253
+ // host clock, no behavior change for vitest/jest. The render tier installs a VIRTUAL
254
+ // clock that advances per drained frame so time-gated rAF loops terminate (see the
255
+ // synthetic-geometry/clock note in CLAUDE.md). Resolves performanceNow lazily, so it
256
+ // stays defined regardless of where setClock is called. (Date.now is the embedder's
257
+ // to override — turbo-dom never shadows it.)
258
+ let __clock = null;
259
+ function now() { return __clock ? __clock() : performanceNow(); }
260
+ export function setClock(fn) { __clock = typeof fn === 'function' ? fn : null; }
261
+
214
262
  // base64 without depending on the Node `Buffer` global — prefer the platform
215
263
  // btoa/atob, then Buffer (Node), then a pure-JS fallback so a bare-isolate page
216
264
  // that calls btoa/atob doesn't throw.
@@ -386,7 +434,7 @@ const STATIC_BASE = {
386
434
  crypto: globalThis.crypto, Crypto: globalThis.Crypto, SubtleCrypto: globalThis.SubtleCrypto,
387
435
  btoa: (s) => turboBtoa(String(s)),
388
436
  atob: (s) => turboAtob(String(s)),
389
- MessageChannel: globalThis.MessageChannel, MessagePort: globalThis.MessagePort,
437
+ MessageChannel, MessagePort,
390
438
  BroadcastChannel: globalThis.BroadcastChannel, EventSource: globalThis.EventSource,
391
439
  reportError: (e) => { void e; },
392
440
  requestIdleCallback: (cb) => hostSetTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0),
Binary file
Binary file