@miaskiewicz/turbo-dom 0.1.20 → 0.1.22
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/README.md +7 -3
- package/package.json +1 -1
- package/src/runtime/dom.mjs +8 -3
- package/src/runtime/events.mjs +28 -13
- package/src/runtime/stubs.mjs +8 -1
- package/src/runtime/window.mjs +78 -61
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
package/README.md
CHANGED
|
@@ -105,13 +105,17 @@ the suite row (ms/file, lower = faster):
|
|
|
105
105
|
|
|
106
106
|
| benchmark | turbo-dom | happy-dom | jsdom |
|
|
107
107
|
|---|---:|---:|---:|
|
|
108
|
-
| **per-file setup + 1 query** (ops/s) | **5,
|
|
109
|
-
| **realistic suite**, 200 files (ms/file) | **0.
|
|
108
|
+
| **per-file setup + 1 query** (ops/s) | **5,900** | 600 | 250 |
|
|
109
|
+
| **realistic suite**, 200 files (ms/file) | **0.15** | 2.1 | 4.9 |
|
|
110
110
|
| **parse 56 KB SSR** (ops/s) | **478** | 43 | 26 |
|
|
111
111
|
| **parse 20 KB real page** (ops/s) | **4,203** | 190 | 114 |
|
|
112
|
-
| repeated query throughput (iters/s) | **
|
|
112
|
+
| repeated query throughput (iters/s) | **920k** | 600k | 3k |
|
|
113
113
|
| html5lib conformance | **99.72%** | 37.35% | 97.03% |
|
|
114
114
|
|
|
115
|
+
Roughly **~25–30× jsdom** and **~10–14× happy-dom** on per-file setup / realistic
|
|
116
|
+
suites, **18–37×** on parsing, and it edges happy-dom on repeated queries while
|
|
117
|
+
staying 99.7% spec-correct.
|
|
118
|
+
|
|
115
119
|
**turbo-dom wins across the board on what test suites actually do**: per-file
|
|
116
120
|
construction (~10× happy-dom, ~23× jsdom), parsing, realistic suites (~10× happy-dom,
|
|
117
121
|
~23× jsdom), spec-correctness (99.7% vs 37%), **and** repeated queries.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miaskiewicz/turbo-dom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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",
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -98,8 +98,12 @@ export class Node extends EventTarget {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
get childNodes() {
|
|
101
|
+
// the NodeList reads __children() live, so one cached object per node is
|
|
102
|
+
// always correct — avoids re-allocating a Proxy on every .childNodes access
|
|
103
|
+
// (React/RTL hit this constantly).
|
|
104
|
+
if (this.__childNodesList) return this.__childNodesList;
|
|
101
105
|
const self = this;
|
|
102
|
-
return liveNodeList(() => self.__children());
|
|
106
|
+
return (this.__childNodesList = liveNodeList(() => self.__children()));
|
|
103
107
|
}
|
|
104
108
|
get firstChild() { const k = this.__children(); return k[0] ?? null; }
|
|
105
109
|
get lastChild() { const k = this.__children(); return k[k.length - 1] ?? null; }
|
|
@@ -523,8 +527,9 @@ export class Element extends Node {
|
|
|
523
527
|
|
|
524
528
|
// ---- element-only traversal (live) ----
|
|
525
529
|
get children() {
|
|
530
|
+
if (this.__childrenList) return this.__childrenList;
|
|
526
531
|
const self = this;
|
|
527
|
-
return liveHTMLCollection(() => self.__children().filter((n) => n.nodeType === ELEMENT_NODE));
|
|
532
|
+
return (this.__childrenList = liveHTMLCollection(() => self.__children().filter((n) => n.nodeType === ELEMENT_NODE)));
|
|
528
533
|
}
|
|
529
534
|
get childElementCount() { return this.__children().filter((n) => n.nodeType === ELEMENT_NODE).length; }
|
|
530
535
|
get firstElementChild() { return this.__children().find((n) => n.nodeType === ELEMENT_NODE) ?? null; }
|
|
@@ -750,7 +755,7 @@ export class DocumentFragment extends Node {
|
|
|
750
755
|
get nodeName() { return '#document-fragment'; }
|
|
751
756
|
querySelector(sel) { return cachedQS(this, sel); }
|
|
752
757
|
querySelectorAll(sel) { return cachedQSA(this, sel); }
|
|
753
|
-
get children() { const self = this; return liveHTMLCollection(() => self.__children().filter((n) => n.nodeType === ELEMENT_NODE)); }
|
|
758
|
+
get children() { if (this.__childrenList) return this.__childrenList; const self = this; return (this.__childrenList = liveHTMLCollection(() => self.__children().filter((n) => n.nodeType === ELEMENT_NODE))); }
|
|
754
759
|
append(...nodes) { for (const n of nodes) this.appendChild(toNode(this.ownerDocument, n)); }
|
|
755
760
|
cloneNode(deep = false) { const f = new DocumentFragment(this.ownerDocument); if (deep) for (const c of this.__children()) f.appendChild(c.cloneNode(true)); return f; }
|
|
756
761
|
}
|
package/src/runtime/events.mjs
CHANGED
|
@@ -150,8 +150,20 @@ export class EventTarget {
|
|
|
150
150
|
dispatchEvent(event) {
|
|
151
151
|
if (!(event instanceof Event)) throw new TypeError('dispatchEvent requires an Event');
|
|
152
152
|
event.target = this;
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
|
|
154
|
+
// Single ancestor walk: build the path AND note whether any node on it has a
|
|
155
|
+
// listener for this type. React fires thousands of events with zero matching
|
|
156
|
+
// listeners on the path — those skip the capture/target/bubble invoke loops.
|
|
157
|
+
const type = event.type;
|
|
158
|
+
const path = [];
|
|
159
|
+
let hasListener = false;
|
|
160
|
+
let node = this;
|
|
161
|
+
while (node) {
|
|
162
|
+
path.push(node);
|
|
163
|
+
if (!hasListener) { const l = node.__listeners && node.__listeners.get(type); if (l && l.length) hasListener = true; }
|
|
164
|
+
node = node.parentNode || node.__owner || null;
|
|
165
|
+
}
|
|
166
|
+
event._path = path;
|
|
155
167
|
|
|
156
168
|
// pre-click activation (WHATWG): checkbox/radio toggle BEFORE click listeners
|
|
157
169
|
// run, so React's change detection sees the new value. Undone if preventDefault.
|
|
@@ -185,18 +197,21 @@ export class EventTarget {
|
|
|
185
197
|
}
|
|
186
198
|
};
|
|
187
199
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
// at target
|
|
194
|
-
if (!event._stopPropagation) invoke(path[0], PHASE_AT_TARGET);
|
|
195
|
-
// bubbling: target's parent -> root
|
|
196
|
-
if (event.bubbles) {
|
|
197
|
-
for (let i = 1; i < path.length; i++) {
|
|
200
|
+
// no listener anywhere on the path → skip all three propagation phases
|
|
201
|
+
if (hasListener) {
|
|
202
|
+
// capturing: root -> just before target
|
|
203
|
+
for (let i = path.length - 1; i >= 1; i--) {
|
|
198
204
|
if (event._stopPropagation) break;
|
|
199
|
-
invoke(path[i],
|
|
205
|
+
invoke(path[i], PHASE_CAPTURING);
|
|
206
|
+
}
|
|
207
|
+
// at target
|
|
208
|
+
if (!event._stopPropagation) invoke(path[0], PHASE_AT_TARGET);
|
|
209
|
+
// bubbling: target's parent -> root
|
|
210
|
+
if (event.bubbles) {
|
|
211
|
+
for (let i = 1; i < path.length; i++) {
|
|
212
|
+
if (event._stopPropagation) break;
|
|
213
|
+
invoke(path[i], PHASE_BUBBLING);
|
|
214
|
+
}
|
|
200
215
|
}
|
|
201
216
|
}
|
|
202
217
|
|
package/src/runtime/stubs.mjs
CHANGED
|
@@ -83,8 +83,13 @@ export function makeMatchMedia() {
|
|
|
83
83
|
// invents cascade/layout numbers. A property that wasn't set reads as ''.
|
|
84
84
|
export function makeGetComputedStyle() {
|
|
85
85
|
return (el) => {
|
|
86
|
+
// The Proxy reads el.style LIVE on each access, so one cached Proxy per
|
|
87
|
+
// element is always correct (no version needed) — avoids re-allocating a
|
|
88
|
+
// Proxy on every call, which dom-accessibility-api does per element in
|
|
89
|
+
// getByRole/getByText visibility checks.
|
|
90
|
+
if (el && el.__computedStyle) return el.__computedStyle;
|
|
86
91
|
const style = el && el.style ? el.style : null;
|
|
87
|
-
|
|
92
|
+
const proxy = new Proxy({}, {
|
|
88
93
|
get(_t, key) {
|
|
89
94
|
if (key === 'getPropertyValue') return (p) => (style ? style.getPropertyValue(p) : '');
|
|
90
95
|
if (key === '__honest') return 'computed style is inline-only; no layout/cascade available';
|
|
@@ -92,6 +97,8 @@ export function makeGetComputedStyle() {
|
|
|
92
97
|
return style ? style[key] : '';
|
|
93
98
|
},
|
|
94
99
|
});
|
|
100
|
+
if (el) el.__computedStyle = proxy;
|
|
101
|
+
return proxy;
|
|
95
102
|
};
|
|
96
103
|
}
|
|
97
104
|
|
package/src/runtime/window.mjs
CHANGED
|
@@ -87,72 +87,20 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
87
87
|
const touched = new Set();
|
|
88
88
|
let windowProxy;
|
|
89
89
|
|
|
90
|
-
//
|
|
90
|
+
// Per-env globals: only the ~11 that capture document/url/windowProxy. Everything
|
|
91
|
+
// stateless (constructors, timers, stateless window methods) lives in the shared
|
|
92
|
+
// module-level STATIC_BASE, built ONCE — not rebuilt per createWindow (per test
|
|
93
|
+
// file). The Proxy below reads base first, then STATIC_BASE, then lazy; a
|
|
94
|
+
// `window.x = y` assignment writes to base, shadowing STATIC_BASE per-env.
|
|
91
95
|
const base = {
|
|
92
96
|
document,
|
|
93
97
|
name: '',
|
|
94
98
|
closed: false,
|
|
95
99
|
origin: new URL(url).origin,
|
|
96
|
-
// constructors are cheap class refs — expose eagerly
|
|
97
|
-
Node, Element, Text, Comment, Document, DocumentFragment, DocumentType,
|
|
98
|
-
EventTarget,
|
|
99
|
-
Event, CustomEvent,
|
|
100
|
-
UIEvent, MouseEvent, PointerEvent, KeyboardEvent, InputEvent, FocusEvent,
|
|
101
|
-
CompositionEvent, WheelEvent, TouchEvent, DragEvent, ProgressEvent, ClipboardEvent,
|
|
102
|
-
DataTransfer,
|
|
103
|
-
// every element is a plain Element → `el instanceof HTMLElement` is true.
|
|
104
|
-
// Tag-specific interfaces match by localName via Symbol.hasInstance, so
|
|
105
|
-
// `el instanceof HTMLAnchorElement` is true ONLY for <a> (not every element),
|
|
106
|
-
// and React's `while (el instanceof HTMLIFrameElement)` loop terminates.
|
|
107
|
-
HTMLElement: Element, SVGElement: Element,
|
|
108
|
-
HTMLAnchorElement: tagClass('a'), HTMLInputElement: tagClass('input'),
|
|
109
|
-
HTMLTextAreaElement: tagClass('textarea'), HTMLSelectElement: tagClass('select'),
|
|
110
|
-
HTMLOptionElement: tagClass('option'), HTMLButtonElement: tagClass('button'),
|
|
111
|
-
HTMLFormElement: tagClass('form'), HTMLImageElement: tagClass('img'),
|
|
112
|
-
HTMLCanvasElement: tagClass('canvas'), HTMLTemplateElement: tagClass('template'),
|
|
113
|
-
HTMLLabelElement: tagClass('label'), HTMLDivElement: tagClass('div'),
|
|
114
|
-
HTMLSpanElement: tagClass('span'), HTMLParagraphElement: tagClass('p'),
|
|
115
|
-
HTMLUListElement: tagClass('ul'), HTMLLIElement: tagClass('li'),
|
|
116
|
-
HTMLBodyElement: tagClass('body'), HTMLIFrameElement: tagClass('iframe'),
|
|
117
|
-
HTMLHeadingElement: tagClass(/^h[1-6]$/),
|
|
118
|
-
HTMLDocument: Document, DocumentFragment, ShadowRoot: DocumentFragment,
|
|
119
|
-
MutationObserver, DOMParser, XMLSerializer,
|
|
120
|
-
URL: makeURL(), URLSearchParams,
|
|
121
|
-
Blob: globalThis.Blob, File: makeFile(), FileReader,
|
|
122
100
|
customElements: makeCustomElements(),
|
|
123
|
-
AbortController: globalThis.AbortController, AbortSignal: globalThis.AbortSignal,
|
|
124
|
-
TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder,
|
|
125
|
-
// web platform globals Node already provides
|
|
126
|
-
fetch: globalThis.fetch ? (...a) => globalThis.fetch(...a) : undefined,
|
|
127
|
-
Headers: globalThis.Headers, Request: globalThis.Request, Response: globalThis.Response,
|
|
128
|
-
FormData: TurboFormData, ReadableStream: globalThis.ReadableStream,
|
|
129
|
-
crypto: globalThis.crypto, Crypto: globalThis.Crypto, SubtleCrypto: globalThis.SubtleCrypto,
|
|
130
|
-
btoa: (s) => Buffer.from(String(s), 'binary').toString('base64'),
|
|
131
|
-
atob: (s) => Buffer.from(String(s), 'base64').toString('binary'),
|
|
132
|
-
MessageChannel: globalThis.MessageChannel, MessagePort: globalThis.MessagePort,
|
|
133
|
-
BroadcastChannel: globalThis.BroadcastChannel, EventSource: globalThis.EventSource,
|
|
134
|
-
reportError: (e) => { /* swallow; tests assert via handlers */ void e; },
|
|
135
|
-
requestIdleCallback: (cb) => hostSetTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0),
|
|
136
|
-
cancelIdleCallback: (id) => hostClearTimeout(id),
|
|
137
|
-
CSS: { supports: () => true, escape: (s) => String(s).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c) },
|
|
138
|
-
XMLHttpRequest: makeXHR(),
|
|
139
101
|
Image: function Image(w, h) { const img = document.createElement('img'); if (w != null) img.setAttribute('width', w); if (h != null) img.setAttribute('height', h); return img; },
|
|
140
102
|
Audio: function Audio(src) { const a = document.createElement('audio'); if (src) a.setAttribute('src', src); return a; },
|
|
141
|
-
Worker: class Worker { constructor() {} postMessage() {} terminate() {} addEventListener() {} removeEventListener() {} },
|
|
142
|
-
// timers delegate to the captured host fns (NOT the bare names — once these
|
|
143
|
-
// are installed on globalThis the bare names resolve back here → recursion)
|
|
144
|
-
setTimeout: (...a) => hostSetTimeout(...a),
|
|
145
|
-
clearTimeout: (...a) => hostClearTimeout(...a),
|
|
146
|
-
setInterval: (...a) => hostSetInterval(...a),
|
|
147
|
-
clearInterval: (...a) => hostClearInterval(...a),
|
|
148
|
-
queueMicrotask: (...a) => hostQueueMicrotask(...a),
|
|
149
|
-
structuredClone: (...a) => hostStructuredClone(...a),
|
|
150
103
|
getSelection: () => document.getSelection(),
|
|
151
|
-
scrollTo() {}, scroll() {}, scrollBy() {},
|
|
152
|
-
// window methods libraries/tests spy on — must exist as own props for vi.spyOn
|
|
153
|
-
open: () => null, close() {}, stop() {}, print() {}, focus() {}, blur() {},
|
|
154
|
-
moveTo() {}, moveBy() {}, resizeTo() {}, resizeBy() {},
|
|
155
|
-
alert() {}, confirm: () => false, prompt: () => null,
|
|
156
104
|
dispatchEvent: (e) => document.dispatchEvent(e),
|
|
157
105
|
addEventListener: (...a) => document.addEventListener(...a),
|
|
158
106
|
removeEventListener: (...a) => document.removeEventListener(...a),
|
|
@@ -196,7 +144,8 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
196
144
|
windowProxy = new Proxy(base, {
|
|
197
145
|
get(t, k) {
|
|
198
146
|
if (k === 'window' || k === 'self' || k === 'globalThis' || k === 'parent' || k === 'top') return windowProxy;
|
|
199
|
-
if (k in t) return t[k];
|
|
147
|
+
if (k in t) return t[k]; // per-env (incl. overrides + materialized lazy)
|
|
148
|
+
if (k in STATIC_BASE) return STATIC_BASE[k]; // shared stateless globals
|
|
200
149
|
const factory = lazy[k];
|
|
201
150
|
if (factory) {
|
|
202
151
|
touched.add(k);
|
|
@@ -206,11 +155,19 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
206
155
|
}
|
|
207
156
|
return undefined;
|
|
208
157
|
},
|
|
209
|
-
set(t, k, v) { t[k] = v; return true; },
|
|
158
|
+
set(t, k, v) { t[k] = v; return true; }, // writes to base → shadows STATIC_BASE per-env
|
|
210
159
|
has(t, k) {
|
|
211
|
-
return k in t || k in lazy ||
|
|
160
|
+
return k in t || k in STATIC_BASE || k in lazy ||
|
|
212
161
|
k === 'window' || k === 'self' || k === 'globalThis' || k === 'parent' || k === 'top';
|
|
213
162
|
},
|
|
163
|
+
// so vi.spyOn(window, 'scrollTo'/'open'/…) finds STATIC_BASE methods as own
|
|
164
|
+
// props; spyOn then defineProperty's the spy onto base (target), shadowing it.
|
|
165
|
+
getOwnPropertyDescriptor(t, k) {
|
|
166
|
+
const own = Object.getOwnPropertyDescriptor(t, k);
|
|
167
|
+
if (own) return own;
|
|
168
|
+
if (k in STATIC_BASE) return { configurable: true, enumerable: true, writable: true, value: STATIC_BASE[k] };
|
|
169
|
+
return undefined;
|
|
170
|
+
},
|
|
214
171
|
});
|
|
215
172
|
|
|
216
173
|
document.defaultView = windowProxy;
|
|
@@ -220,7 +177,7 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
220
177
|
// which lazy globals this test materialized (the "DOM surface used" report)
|
|
221
178
|
touched: () => [...touched],
|
|
222
179
|
// every global name this window can provide (for environment adapters)
|
|
223
|
-
globalKeys: [...Object.keys(base), ...Object.keys(lazy)],
|
|
180
|
+
globalKeys: [...Object.keys(base), ...Object.keys(STATIC_BASE), ...Object.keys(lazy)],
|
|
224
181
|
// Layer 5: drop materialized global slots, keep the class machinery warm.
|
|
225
182
|
resetGlobals() {
|
|
226
183
|
for (const k of touched) delete base[k];
|
|
@@ -247,6 +204,9 @@ function makeFile() {
|
|
|
247
204
|
constructor(bits = [], name = 'file', opts = {}) { super(bits, opts); this.name = String(name); this.lastModified = opts.lastModified || 0; }
|
|
248
205
|
};
|
|
249
206
|
}
|
|
207
|
+
// stateless — build the classes ONCE, not per createWindow() (per test file).
|
|
208
|
+
const TURBO_URL = makeURL();
|
|
209
|
+
const TURBO_FILE = makeFile();
|
|
250
210
|
|
|
251
211
|
// Minimal XMLHttpRequest backed by fetch — enough that libraries that construct
|
|
252
212
|
// one and issue a request don't crash. No-network setups still get a clean object.
|
|
@@ -276,3 +236,60 @@ function makeXHR() {
|
|
|
276
236
|
}
|
|
277
237
|
};
|
|
278
238
|
}
|
|
239
|
+
|
|
240
|
+
// Stateless globals — identical for every window, so built ONCE at module load
|
|
241
|
+
// instead of per createWindow() (per test file). createWindow's Proxy falls back
|
|
242
|
+
// to this after its tiny per-env `base`. Nothing here captures document/url/window.
|
|
243
|
+
const STATIC_BASE = {
|
|
244
|
+
// DOM + event constructors (cheap class refs)
|
|
245
|
+
Node, Element, Text, Comment, Document, DocumentFragment, DocumentType, EventTarget,
|
|
246
|
+
Event, CustomEvent,
|
|
247
|
+
UIEvent, MouseEvent, PointerEvent, KeyboardEvent, InputEvent, FocusEvent,
|
|
248
|
+
CompositionEvent, WheelEvent, TouchEvent, DragEvent, ProgressEvent, ClipboardEvent,
|
|
249
|
+
DataTransfer,
|
|
250
|
+
// every element is a plain Element → `el instanceof HTMLElement` is true.
|
|
251
|
+
// Tag-specific interfaces match by localName via Symbol.hasInstance.
|
|
252
|
+
HTMLElement: Element, SVGElement: Element,
|
|
253
|
+
HTMLAnchorElement: tagClass('a'), HTMLInputElement: tagClass('input'),
|
|
254
|
+
HTMLTextAreaElement: tagClass('textarea'), HTMLSelectElement: tagClass('select'),
|
|
255
|
+
HTMLOptionElement: tagClass('option'), HTMLButtonElement: tagClass('button'),
|
|
256
|
+
HTMLFormElement: tagClass('form'), HTMLImageElement: tagClass('img'),
|
|
257
|
+
HTMLCanvasElement: tagClass('canvas'), HTMLTemplateElement: tagClass('template'),
|
|
258
|
+
HTMLLabelElement: tagClass('label'), HTMLDivElement: tagClass('div'),
|
|
259
|
+
HTMLSpanElement: tagClass('span'), HTMLParagraphElement: tagClass('p'),
|
|
260
|
+
HTMLUListElement: tagClass('ul'), HTMLLIElement: tagClass('li'),
|
|
261
|
+
HTMLBodyElement: tagClass('body'), HTMLIFrameElement: tagClass('iframe'),
|
|
262
|
+
HTMLHeadingElement: tagClass(/^h[1-6]$/),
|
|
263
|
+
HTMLDocument: Document, ShadowRoot: DocumentFragment,
|
|
264
|
+
MutationObserver, DOMParser, XMLSerializer,
|
|
265
|
+
URL: TURBO_URL, URLSearchParams,
|
|
266
|
+
Blob: globalThis.Blob, File: TURBO_FILE, FileReader,
|
|
267
|
+
AbortController: globalThis.AbortController, AbortSignal: globalThis.AbortSignal,
|
|
268
|
+
TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder,
|
|
269
|
+
fetch: globalThis.fetch ? (...a) => globalThis.fetch(...a) : undefined,
|
|
270
|
+
Headers: globalThis.Headers, Request: globalThis.Request, Response: globalThis.Response,
|
|
271
|
+
FormData: TurboFormData, ReadableStream: globalThis.ReadableStream,
|
|
272
|
+
crypto: globalThis.crypto, Crypto: globalThis.Crypto, SubtleCrypto: globalThis.SubtleCrypto,
|
|
273
|
+
btoa: (s) => Buffer.from(String(s), 'binary').toString('base64'),
|
|
274
|
+
atob: (s) => Buffer.from(String(s), 'base64').toString('binary'),
|
|
275
|
+
MessageChannel: globalThis.MessageChannel, MessagePort: globalThis.MessagePort,
|
|
276
|
+
BroadcastChannel: globalThis.BroadcastChannel, EventSource: globalThis.EventSource,
|
|
277
|
+
reportError: (e) => { void e; },
|
|
278
|
+
requestIdleCallback: (cb) => hostSetTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0),
|
|
279
|
+
cancelIdleCallback: (id) => hostClearTimeout(id),
|
|
280
|
+
CSS: { supports: () => true, escape: (s) => String(s).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c) },
|
|
281
|
+
XMLHttpRequest: makeXHR(),
|
|
282
|
+
Worker: class Worker { constructor() {} postMessage() {} terminate() {} addEventListener() {} removeEventListener() {} },
|
|
283
|
+
// timers delegate to captured host fns (NOT bare names — installed bare names
|
|
284
|
+
// would resolve back here → recursion)
|
|
285
|
+
setTimeout: (...a) => hostSetTimeout(...a),
|
|
286
|
+
clearTimeout: (...a) => hostClearTimeout(...a),
|
|
287
|
+
setInterval: (...a) => hostSetInterval(...a),
|
|
288
|
+
clearInterval: (...a) => hostClearInterval(...a),
|
|
289
|
+
queueMicrotask: (...a) => hostQueueMicrotask(...a),
|
|
290
|
+
structuredClone: (...a) => hostStructuredClone(...a),
|
|
291
|
+
scrollTo() {}, scroll() {}, scrollBy() {},
|
|
292
|
+
open: () => null, close() {}, stop() {}, print() {}, focus() {}, blur() {},
|
|
293
|
+
moveTo() {}, moveBy() {}, resizeTo() {}, resizeBy() {},
|
|
294
|
+
alert() {}, confirm: () => false, prompt: () => null,
|
|
295
|
+
};
|
|
Binary file
|