@lifeart/async-dom 2.0.0-alpha.3

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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +623 -0
  3. package/dist/base.d.cts +398 -0
  4. package/dist/base.d.cts.map +1 -0
  5. package/dist/base.d.ts +398 -0
  6. package/dist/base.d.ts.map +1 -0
  7. package/dist/cli.cjs +528 -0
  8. package/dist/cli.cjs.map +1 -0
  9. package/dist/cli.d.cts +1 -0
  10. package/dist/cli.d.ts +1 -0
  11. package/dist/cli.js +493 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/debug.d.cts +145 -0
  14. package/dist/debug.d.cts.map +1 -0
  15. package/dist/debug.d.ts +145 -0
  16. package/dist/debug.d.ts.map +1 -0
  17. package/dist/index.cjs +26 -0
  18. package/dist/index.d.cts +560 -0
  19. package/dist/index.d.cts.map +1 -0
  20. package/dist/index.d.ts +560 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +5 -0
  23. package/dist/index2.d.cts +5 -0
  24. package/dist/index2.d.ts +5 -0
  25. package/dist/index3.d.cts +882 -0
  26. package/dist/index3.d.cts.map +1 -0
  27. package/dist/index3.d.ts +882 -0
  28. package/dist/index3.d.ts.map +1 -0
  29. package/dist/main-thread.cjs +5459 -0
  30. package/dist/main-thread.cjs.map +1 -0
  31. package/dist/main-thread.js +5429 -0
  32. package/dist/main-thread.js.map +1 -0
  33. package/dist/react.cjs +116 -0
  34. package/dist/react.cjs.map +1 -0
  35. package/dist/react.d.cts +91 -0
  36. package/dist/react.d.cts.map +1 -0
  37. package/dist/react.d.ts +91 -0
  38. package/dist/react.d.ts.map +1 -0
  39. package/dist/react.js +113 -0
  40. package/dist/react.js.map +1 -0
  41. package/dist/resolve-debug.cjs +24 -0
  42. package/dist/resolve-debug.cjs.map +1 -0
  43. package/dist/resolve-debug.js +19 -0
  44. package/dist/resolve-debug.js.map +1 -0
  45. package/dist/server.cjs +250 -0
  46. package/dist/server.cjs.map +1 -0
  47. package/dist/server.d.cts +127 -0
  48. package/dist/server.d.cts.map +1 -0
  49. package/dist/server.d.ts +127 -0
  50. package/dist/server.d.ts.map +1 -0
  51. package/dist/server.js +245 -0
  52. package/dist/server.js.map +1 -0
  53. package/dist/svelte.cjs +48 -0
  54. package/dist/svelte.cjs.map +1 -0
  55. package/dist/svelte.d.cts +38 -0
  56. package/dist/svelte.d.cts.map +1 -0
  57. package/dist/svelte.d.ts +38 -0
  58. package/dist/svelte.d.ts.map +1 -0
  59. package/dist/svelte.js +47 -0
  60. package/dist/svelte.js.map +1 -0
  61. package/dist/sync-channel.cjs +532 -0
  62. package/dist/sync-channel.cjs.map +1 -0
  63. package/dist/sync-channel.js +425 -0
  64. package/dist/sync-channel.js.map +1 -0
  65. package/dist/transport.cjs +213 -0
  66. package/dist/transport.cjs.map +1 -0
  67. package/dist/transport.d.cts +79 -0
  68. package/dist/transport.d.cts.map +1 -0
  69. package/dist/transport.d.ts +79 -0
  70. package/dist/transport.d.ts.map +1 -0
  71. package/dist/transport.js +202 -0
  72. package/dist/transport.js.map +1 -0
  73. package/dist/vite-plugin.cjs +112 -0
  74. package/dist/vite-plugin.cjs.map +1 -0
  75. package/dist/vite-plugin.d.cts +39 -0
  76. package/dist/vite-plugin.d.cts.map +1 -0
  77. package/dist/vite-plugin.d.ts +39 -0
  78. package/dist/vite-plugin.d.ts.map +1 -0
  79. package/dist/vite-plugin.js +107 -0
  80. package/dist/vite-plugin.js.map +1 -0
  81. package/dist/vue.cjs +123 -0
  82. package/dist/vue.cjs.map +1 -0
  83. package/dist/vue.d.cts +126 -0
  84. package/dist/vue.d.cts.map +1 -0
  85. package/dist/vue.d.ts +126 -0
  86. package/dist/vue.d.ts.map +1 -0
  87. package/dist/vue.js +120 -0
  88. package/dist/vue.js.map +1 -0
  89. package/dist/worker-thread.cjs +2751 -0
  90. package/dist/worker-thread.cjs.map +1 -0
  91. package/dist/worker-thread.js +2692 -0
  92. package/dist/worker-thread.js.map +1 -0
  93. package/dist/worker-transport.cjs +136 -0
  94. package/dist/worker-transport.cjs.map +1 -0
  95. package/dist/worker-transport.d.cts +162 -0
  96. package/dist/worker-transport.d.cts.map +1 -0
  97. package/dist/worker-transport.d.ts +162 -0
  98. package/dist/worker-transport.d.ts.map +1 -0
  99. package/dist/worker-transport.js +125 -0
  100. package/dist/worker-transport.js.map +1 -0
  101. package/dist/worker.cjs +12 -0
  102. package/dist/worker.d.cts +2 -0
  103. package/dist/worker.d.ts +2 -0
  104. package/dist/worker.js +2 -0
  105. package/dist/ws-server-transport.cjs +147 -0
  106. package/dist/ws-server-transport.cjs.map +1 -0
  107. package/dist/ws-server-transport.d.cts +64 -0
  108. package/dist/ws-server-transport.d.cts.map +1 -0
  109. package/dist/ws-server-transport.d.ts +64 -0
  110. package/dist/ws-server-transport.d.ts.map +1 -0
  111. package/dist/ws-server-transport.js +142 -0
  112. package/dist/ws-server-transport.js.map +1 -0
  113. package/dist/ws-transport.cjs +954 -0
  114. package/dist/ws-transport.cjs.map +1 -0
  115. package/dist/ws-transport.js +913 -0
  116. package/dist/ws-transport.js.map +1 -0
  117. package/package.json +145 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Alex Kanunnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,623 @@
1
+ # @lifeart/async-dom
2
+
3
+ [![CI](https://github.com/lifeart/async-dom/actions/workflows/ci.yml/badge.svg)](https://github.com/lifeart/async-dom/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@lifeart/async-dom)](https://www.npmjs.com/package/@lifeart/async-dom)
5
+ [![license](https://img.shields.io/npm/l/@lifeart/async-dom)](./LICENSE)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@lifeart/async-dom)](https://bundlephobia.com/package/@lifeart/async-dom)
7
+
8
+ **Your application runs in a Web Worker. The DOM is just a projection.**
9
+
10
+ async-dom provides a virtual `document` inside a Web Worker with the full DOM API. Your worker code uses standard DOM operations (`createElement`, `addEventListener`, `textContent`). The main thread receives serialized mutations and applies them at 60 fps. Framework adapters let you embed worker-rendered content inside React, Vue, or Svelte host apps.
11
+
12
+ This architecture doesn't just improve performance. It fundamentally changes what is accessible to scrapers, bots, browser extensions, and anyone inspecting your page.
13
+
14
+ **[Live Demo](https://lifeart.github.io/async-dom/)** · **[Demo with DevTools](https://lifeart.github.io/async-dom/?debug)** · **[npm](https://www.npmjs.com/package/@lifeart/async-dom)**
15
+
16
+ ---
17
+
18
+ ## Why async-dom?
19
+
20
+ ### The web has a content protection problem
21
+
22
+ Cloudflare blocked **416 billion AI bot requests** in the past year. OpenAI's crawl-to-referral ratio is 1,700:1 — they consume vastly more content than they return in traffic. `robots.txt` is voluntarily ignored. Legal battles (NYT vs OpenAI, Danish publishers vs OpenAI) are slow. The industry needs structural defenses, not polite requests.
23
+
24
+ ### The web has a performance problem
25
+
26
+ JavaScript is single-threaded. The main thread handles rendering, user input, framework execution, and third-party scripts — all competing for the same 16ms frame budget. The result: jank, poor Core Web Vitals, and frustrated users.
27
+
28
+ ### The web has a security problem
29
+
30
+ Traditional web apps expose everything: business logic in bundled JS, data structures in the DOM tree, auth tokens accessible to any XSS payload, and source code available to anyone with DevTools.
31
+
32
+ **async-dom addresses all three.**
33
+
34
+ ---
35
+
36
+ ## Real-World Use Cases
37
+
38
+ ### Content Protection & Anti-Scraping
39
+
40
+ | Use Case | How async-dom helps |
41
+ | -------- | ------------------- |
42
+ | **AI scraping prevention** | Content never exists in initial HTML. `curl` and simple scrapers get an empty shell. Headless browsers must wait for worker initialization and mutation application, raising the cost and complexity of automated extraction. |
43
+ | **Copyright & DRM** | Business logic and data stay in the worker. The DOM is a procedural artifact — not a template that maps 1:1 to source content. The architecture enables per-session content variation and server-controlled rendering for content protection scenarios. |
44
+ | **NDA UI demos** | Share interactive prototypes where the client cannot copy JS logic — it runs server-side via WebSocket transport or inside an opaque worker. |
45
+ | **Exam & education anti-cheat** | Application state and logic run in a worker or on a server via WebSocket, making them inaccessible from browser DevTools or in-page scripts. This supplements (but does not replace) purpose-built proctoring solutions. |
46
+ | **Dynamic obfuscation** | The architecture supports per-session variation of non-semantic identifiers (class names, element IDs), increasing maintenance cost for selector-based scrapers. This is an advanced pattern with tradeoffs for CSS tooling and testing. |
47
+
48
+ ### Performance & Architecture
49
+
50
+ | Use Case | How async-dom helps |
51
+ | -------- | ------------------- |
52
+ | **Main thread liberation** | Your entire framework (React, Vue, Svelte) runs off the main thread. Framework runtime does not compete with user input or browser rendering on the main thread. Event round-trips add latency compared to same-thread handlers. |
53
+ | **Heavy computation** | Sorting, filtering, data processing, fractal rendering — all happen in the worker without dropping frames. |
54
+ | **Multi-core utilization** | Modern devices have 4-8+ cores. Traditional web apps use one. async-dom lets you use the rest. |
55
+ | **SmartTV & low-power devices** | Run computation on a backend, stream DOM updates via WebSocket to devices with modern browser support. Frame rate depends on network latency and jitter. |
56
+ | **IoT streaming** | Execute the app on a server, stream rendered output to any connected device — TVs, kiosks, embedded displays. |
57
+
58
+ ### Multi-Framework & Isolation
59
+
60
+ | Use Case | How async-dom helps |
61
+ | -------- | ------------------- |
62
+ | **Framework zoo** | Run React, Vue, and Svelte simultaneously on one page — each in its own worker with shadow DOM isolation. Zero conflicts, zero iframes. |
63
+ | **Micro-frontend isolation** | Each team ships a worker. CSS is encapsulated via shadow DOM. No shared global state. Independent deployment. |
64
+ | **Version coexistence** | Run different versions of the same framework side by side — React 18 and React 19 on one page, no conflicts. |
65
+ | **Cross-platform bridge** | Use async-dom as a rendering bridge for React Native, embedded views, or custom renderers. DOM mutations become platform events. |
66
+
67
+ ### Collaboration & Debugging
68
+
69
+ | Use Case | How async-dom helps |
70
+ | -------- | ------------------- |
71
+ | **Parallel editing** | Broadcast a single app instance to multiple viewers via WebSocket. Event forwarding from clients is supported but does not include conflict resolution (events are processed in arrival order). |
72
+ | **Marketing & UX analytics** | WebSocket transport broadcasts UI state to multiple observers. Watch exactly what users experience, live. |
73
+ | **Time-travel debugging** | Record and replay DOM mutation sequences. Scrub through rendering history with a time-travel scrubber. Compare tree snapshots with visual diff. |
74
+ | **Rendering regression tests** | If mutation batches are identical, the UI is identical. Deterministic rendering without pixel comparison. |
75
+
76
+ ---
77
+
78
+ ## Quick Start
79
+
80
+ ```bash
81
+ npm install @lifeart/async-dom
82
+ ```
83
+
84
+ ### main.ts
85
+
86
+ ```ts
87
+ import { createAsyncDom } from "@lifeart/async-dom";
88
+
89
+ const worker = new Worker(new URL("./worker.ts", import.meta.url), {
90
+ type: "module",
91
+ });
92
+
93
+ const dom = createAsyncDom({
94
+ target: document.getElementById("app")!,
95
+ worker,
96
+ });
97
+
98
+ dom.start();
99
+ ```
100
+
101
+ ### worker.ts
102
+
103
+ ```ts
104
+ import { createWorkerDom } from "@lifeart/async-dom/worker";
105
+
106
+ const { document } = createWorkerDom();
107
+
108
+ const div = document.createElement("div");
109
+ div.textContent = "Hello from a Web Worker!";
110
+ document.body.appendChild(div);
111
+
112
+ const input = document.createElement("input");
113
+ input.addEventListener("input", () => {
114
+ console.log("Value:", input.value); // real value from main thread
115
+ });
116
+ document.body.appendChild(input);
117
+ ```
118
+
119
+ That's it. Your app now runs entirely in a worker.
120
+
121
+ ### Further Reading
122
+
123
+ - [Getting Started Guide](./docs/getting-started.md) — Mental model, styling, forms, testing, deployment
124
+ - [Migration Guide](./docs/migration-guide.md) — Adopting async-dom in existing apps
125
+ - [Security Guide](./docs/security-guide.md) — CSP, Trusted Types, COOP/COEP
126
+
127
+ ---
128
+
129
+ ## Framework Adapters
130
+
131
+ async-dom ships adapters for React, Vue, and Svelte. Your framework code runs in the worker with async-dom's virtual DOM API.
132
+
133
+ ### React
134
+
135
+ ```tsx
136
+ import { AsyncDom } from "@lifeart/async-dom/react";
137
+
138
+ function App() {
139
+ return (
140
+ <AsyncDom
141
+ worker="./app.worker.ts"
142
+ debug
143
+ fallback={<div>Loading...</div>}
144
+ onReady={(instance) => console.log("ready")}
145
+ />
146
+ );
147
+ }
148
+ ```
149
+
150
+ ### Vue
151
+
152
+ ```vue
153
+ <template>
154
+ <AsyncDom worker="./app.worker.ts" :debug="true" @ready="onReady">
155
+ <template #fallback><div>Loading...</div></template>
156
+ </AsyncDom>
157
+ </template>
158
+
159
+ <script setup>
160
+ import { AsyncDom } from "@lifeart/async-dom/vue";
161
+ </script>
162
+ ```
163
+
164
+ ### Svelte
165
+
166
+ ```svelte
167
+ <script>
168
+ import { asyncDom } from "@lifeart/async-dom/svelte";
169
+ </script>
170
+
171
+ <div use:asyncDom={{ worker: "./app.worker.ts" }} />
172
+ ```
173
+
174
+ > **Important:** Framework adapters are main-thread mount points. They create a container element and spin up a Web Worker. The worker code uses async-dom's virtual DOM API (standard DOM operations), not the framework's component model. See the [Getting Started Guide](./docs/getting-started.md) for details.
175
+
176
+ ---
177
+
178
+ ## Remote Transports
179
+
180
+ async-dom supports running the worker DOM in a SharedWorker, on a remote server via WebSocket, or any custom transport.
181
+
182
+ ### Remote App (no local Worker)
183
+
184
+ ```ts
185
+ import { createAsyncDom } from "@lifeart/async-dom";
186
+ import { WebSocketTransport } from "@lifeart/async-dom/transport";
187
+
188
+ const dom = createAsyncDom({ target: document.getElementById("app")! });
189
+
190
+ // Connect to a remote server running the app
191
+ dom.addRemoteApp({
192
+ transport: new WebSocketTransport("ws://localhost:3000"),
193
+ name: "remote-app",
194
+ mountPoint: "#app",
195
+ });
196
+
197
+ dom.start();
198
+ ```
199
+
200
+ ### SharedWorker Transport
201
+
202
+ ```ts
203
+ import { createAsyncDom } from "@lifeart/async-dom";
204
+ import { SharedWorkerTransport } from "@lifeart/async-dom/transport";
205
+
206
+ const sw = new SharedWorker("/my-worker.js", { type: "module" });
207
+ const transport = new SharedWorkerTransport(sw.port);
208
+
209
+ const dom = createAsyncDom({ target: document.getElementById("app")! });
210
+ dom.addRemoteApp({ transport, name: "shared-worker-app" });
211
+ dom.start();
212
+ ```
213
+
214
+ ### Server-Side Rendering (Node.js)
215
+
216
+ ```ts
217
+ import { createServerApp } from "@lifeart/async-dom/server";
218
+ import { WebSocketServerTransport } from "@lifeart/async-dom/server";
219
+
220
+ // Inside a WebSocket connection handler:
221
+ const transport = new WebSocketServerTransport(socket);
222
+ const app = createServerApp({
223
+ transport,
224
+ appModule: ({ document }) => {
225
+ const div = document.createElement("div");
226
+ div.textContent = "Server-rendered via async-dom";
227
+ document.body.appendChild(div);
228
+ },
229
+ });
230
+
231
+ // Clean up on disconnect:
232
+ socket.on("close", () => app.destroy());
233
+ ```
234
+
235
+ ### Multi-Client Streaming (Optional)
236
+
237
+ Stream one server-side app instance to multiple browser clients simultaneously. Each client receives full DOM mutation replay on connect and can send events back to the shared app.
238
+
239
+ **Server (`streaming-server.ts`)**
240
+
241
+ ```ts
242
+ import { createStreamingServer } from "@lifeart/async-dom/server";
243
+ import { WebSocketServer } from "ws";
244
+
245
+ const streaming = createStreamingServer({
246
+ createApp: ({ document }) => {
247
+ const div = document.createElement("div");
248
+ div.textContent = "Hello from server!";
249
+ document.body.appendChild(div);
250
+
251
+ setInterval(() => {
252
+ div.textContent = `Server time: ${new Date().toLocaleTimeString()}`;
253
+ }, 1000);
254
+ },
255
+ broadcast: {
256
+ mutationLog: { maxEntries: 5000 },
257
+ maxClients: 100,
258
+ },
259
+ });
260
+
261
+ const wss = new WebSocketServer({ port: 8080 });
262
+ wss.on("connection", (ws) => {
263
+ const clientId = streaming.handleConnection(ws);
264
+ console.log(`Client ${clientId} connected`);
265
+ });
266
+
267
+ await streaming.ready;
268
+ ```
269
+
270
+ **Client** — no special client-side code needed, use the standard transport:
271
+
272
+ ```ts
273
+ import { createAsyncDom } from "@lifeart/async-dom";
274
+ import { WebSocketTransport } from "@lifeart/async-dom/transport";
275
+
276
+ const asyncDom = createAsyncDom({ target: document.getElementById("app")! });
277
+ const transport = new WebSocketTransport("ws://localhost:8080");
278
+ asyncDom.addRemoteApp({ transport, name: "shared-app" });
279
+ asyncDom.start();
280
+ ```
281
+
282
+ **`StreamingServerInstance` API**
283
+
284
+ | Method / Property | Description |
285
+ | ----------------- | ----------- |
286
+ | `handleConnection(socket, clientId?)` | Register a new WebSocket client; returns the assigned `clientId` |
287
+ | `disconnectClient(clientId)` | Remove a specific client |
288
+ | `getClientCount()` | Number of currently connected clients |
289
+ | `getClientIds()` | Array of all active client IDs |
290
+ | `getDom()` | Access the underlying WorkerDom instance |
291
+ | `destroy()` | Shut down the app and disconnect all clients |
292
+ | `ready` | Promise that resolves when the app has finished initializing |
293
+
294
+ **Features**
295
+
296
+ - Late-joining clients automatically receive a replay of all past mutations before switching to the live stream.
297
+ - A client disconnect does not affect the server app or other clients.
298
+ - Events from each client are tagged with the originating `clientId` before reaching the app.
299
+ - Mutation log size and maximum client count are configurable.
300
+ - Backpressure is managed independently per client.
301
+
302
+ **Limitations & Known Gaps**
303
+
304
+ - **No conflict resolution** — Events from concurrent clients are processed in arrival order (FIFO). No last-writer-wins or ownership model is implemented.
305
+ - **Replay safety** — Late-joining clients receive a full mutation log replay. Non-idempotent mutations (`addEventListener`, `callMethod`, `insertAdjacentHTML`) may cause duplicate side effects during replay.
306
+ - **No log compaction** — The mutation log grows linearly up to `maxEntries`. Snapshot-based compaction is not yet implemented.
307
+ - **Single-process** — The streaming server runs in a single Node.js process. For high concurrency, external load balancing is needed.
308
+ - **No built-in authentication** — `handleConnection` does not validate connections. Authentication must be handled at the WebSocket server level before passing the socket.
309
+ - **No per-client backpressure** — A slow client can temporarily degrade broadcast throughput for other clients.
310
+
311
+ `createServerApp` remains available for single-client (one app per connection) use cases.
312
+
313
+ ---
314
+
315
+ ### Named Apps (DevTools)
316
+
317
+ ```ts
318
+ dom.addApp({
319
+ name: "dashboard", // visible in DevTools instead of random hash
320
+ worker: new Worker("./dashboard.worker.ts", { type: "module" }),
321
+ mountPoint: "#dashboard",
322
+ shadow: true,
323
+ });
324
+ ```
325
+
326
+ ---
327
+
328
+ ## Package Exports
329
+
330
+ | Import path | Purpose |
331
+ | --------------------- | -------------------------------------------- |
332
+ | `@lifeart/async-dom` | Main thread API (`createAsyncDom`) |
333
+ | `@lifeart/async-dom/worker` | Worker thread API (virtual `document`) |
334
+ | `@lifeart/async-dom/transport` | Transport backends (Worker, Binary, WS, SharedWorker, Comlink) |
335
+ | `@lifeart/async-dom/react` | React `<AsyncDom>` component + `useAsyncDom` hook |
336
+ | `@lifeart/async-dom/vue` | Vue `<AsyncDom>` component + `useAsyncDom` composable |
337
+ | `@lifeart/async-dom/svelte` | Svelte `asyncDom` action |
338
+ | `@lifeart/async-dom/vite-plugin` | Vite plugin (COOP/COEP headers, binary transport, error overlay) |
339
+ | `@lifeart/async-dom/server` | Server-side runner (`createServerApp`, `createStreamingServer`, `BroadcastTransport`, `MutationLog`, `WebSocketServerTransport`) |
340
+
341
+ For detailed API documentation, see the JSDoc comments on all exported types and functions. Key types: `AsyncDomConfig`, `AsyncDomInstance`, `WorkerDomConfig`, `WorkerDomResult`.
342
+
343
+ ---
344
+
345
+ ## How It Works
346
+
347
+ ```
348
+ Worker Thread Main Thread
349
+ +--------------------+ +---------------------+
350
+ | VirtualDocument | | ThreadManager |
351
+ | (virtual DOM tree) | | (per-app comms) |
352
+ | | | | | |
353
+ | MutationCollector | | FrameScheduler |
354
+ | (batch + coalesce)| | (budget, sort, |
355
+ +--------|----------+ | cull, fairness) |
356
+ | | | |
357
+ Transport ───────────────> | DomRenderer(s) |
358
+ (postMessage / | (per-app, apply |
359
+ binary / WS) | to real DOM) |
360
+ | | | |
361
+ | <─── Events ─────── | EventBridge |
362
+ | | (DOM → Worker) |
363
+ | | | |
364
+ | <─── Sync Reads ──> | SyncChannelHost |
365
+ | (SharedArrayBuffer | (Atomics.notify) |
366
+ | + Atomics.wait) | |
367
+ +--------|----------+ +---------------------+
368
+ | SyncChannel |
369
+ | (blocking reads) |
370
+ +--------------------+
371
+ ```
372
+
373
+ 1. **Worker** — Your framework runs here. Virtual `document` and `window` provide the full DOM API. Mutations are batched and coalesced automatically.
374
+ 2. **Transport** — Mutations are serialized (structured clone, binary codec, or WebSocket) and sent to the main thread.
375
+ 3. **Scheduler** — The main thread applies mutations within a per-frame budget. Priority sorting, viewport culling, and adaptive batch sizing targets 60 fps.
376
+ 4. **Events** — User interactions on the main thread are serialized and dispatched to worker event handlers.
377
+ 5. **Sync Reads** — `getBoundingClientRect()`, `offsetWidth`, `getComputedStyle()` block in the worker via `SharedArrayBuffer` + `Atomics` and return real values from the main thread.
378
+
379
+ ---
380
+
381
+ ## Security Model
382
+
383
+ async-dom provides multiple layers of protection:
384
+
385
+ ### Worker Isolation (Architectural)
386
+
387
+ - **No direct DOM access** — XSS payloads in the page cannot reach worker internal state.
388
+ - **Serialized communication only** — all data passes through `postMessage`, a natural sanitization boundary.
389
+ - **Separate execution context** — workers are isolated at the browser engine level. Main-thread scripts cannot access worker internal state. Note: browser extensions with appropriate permissions can still read the rendered DOM.
390
+ - **Token protection** — auth tokens and session state in the worker are inaccessible to malicious main-thread scripts.
391
+
392
+ ### Content Sanitization (Active)
393
+
394
+ - **HTML sanitizer** — `innerHTML` strips `<script>`, `<iframe>`, `<style>`, `<object>`, `on*` attributes, and `javascript:`/`data:text/html` URIs.
395
+ - **Property allowlist** — `setProperty` only applies safe properties (`value`, `checked`, `textContent`, etc.).
396
+ - **Attribute filtering** — `setAttribute` blocks `on*` handlers and dangerous URIs.
397
+
398
+ ### Anti-Scraping (Structural)
399
+
400
+ Unlike `robots.txt` (voluntary), CDN-level blocks (circumventable), or CAPTCHAs (UX-degrading), worker-based rendering is an **architectural property** that raises the cost of content extraction:
401
+
402
+ - Empty HTML payload — no content for `curl`, `wget`, or simple GET requests.
403
+ - Procedural DOM — the rendered tree is an artifact of the mutation protocol, not a semantic template.
404
+ - Dynamic structure — the architecture supports per-session variation of class names and DOM structure, raising the maintenance burden for selector-based scrapers.
405
+ - Honeypot injection — the worker can be programmed to insert invisible trap elements that automated tools follow but humans never see.
406
+ - Behavioral gating — the worker controls what renders and when, enabling application-level bot detection logic.
407
+
408
+ ---
409
+
410
+ ## Transports
411
+
412
+ | Transport | Use case |
413
+ | --------- | -------- |
414
+ | `WorkerTransport` | Default — structured clone via `postMessage` |
415
+ | `BinaryWorkerTransport` | Production — 22-opcode binary codec with string deduplication |
416
+ | `WebSocketTransport` | Remote rendering — WebSocket with auto-reconnect and exponential backoff |
417
+ | `createComlinkEndpoint` | RPC — Comlink adapter (optional peer dependency) |
418
+
419
+ WebSocket transport enables powerful patterns: server-side rendering to any device, collaborative multi-user editing, and IoT streaming.
420
+
421
+ ---
422
+
423
+ ## Per-App Isolation
424
+
425
+ Run multiple independent applications on one page. Each gets its own renderer, node cache, event bridge, and optional shadow DOM:
426
+
427
+ ```ts
428
+ const dom = createAsyncDom({ target: document.body });
429
+
430
+ dom.addApp({
431
+ worker: new Worker("./react-app.ts", { type: "module" }),
432
+ mountPoint: "#panel-a",
433
+ shadow: true,
434
+ });
435
+
436
+ dom.addApp({
437
+ worker: new Worker("./vue-app.ts", { type: "module" }),
438
+ mountPoint: "#panel-b",
439
+ shadow: { mode: "closed" },
440
+ });
441
+
442
+ dom.start();
443
+ ```
444
+
445
+ ---
446
+
447
+ ## Sandbox Mode
448
+
449
+ Run third-party scripts that expect bare `document`/`window` globals — no modifications needed:
450
+
451
+ ```ts
452
+ // Patch worker globals — bare `document` resolves to virtual DOM
453
+ const { document } = createWorkerDom({ sandbox: "global" });
454
+
455
+ // Sandboxed eval — Proxy + with for full variable interception
456
+ const { window } = createWorkerDom({ sandbox: "eval" });
457
+ window.eval(`document.body.innerHTML = "<h1>Works!</h1>"`);
458
+ ```
459
+
460
+ | Mode | Bare `document` | `eval()` sandbox | Use case |
461
+ | ---- | ---------------- | ---------------- | -------- |
462
+ | `"global"` | Yes | No | Framework code with bare globals |
463
+ | `"eval"` | No | Yes | Third-party analytics/ads scripts |
464
+ | `true` | Yes | Yes | Maximum compatibility |
465
+
466
+ ---
467
+
468
+ ## Synchronous DOM Reads
469
+
470
+ Via `SharedArrayBuffer` + `Atomics.wait/notify` — real values, not guesses:
471
+
472
+ | API | Returns |
473
+ | --- | ------- |
474
+ | `el.getBoundingClientRect()` | Real DOMRect |
475
+ | `el.offsetWidth`, `clientHeight`, etc. | Real layout metrics |
476
+ | `window.getComputedStyle(el)` | Real computed styles |
477
+ | `window.innerWidth` / `innerHeight` | Real viewport size |
478
+
479
+ **Requires** COOP/COEP headers (automatic with the Vite plugin):
480
+ ```
481
+ Cross-Origin-Opener-Policy: same-origin
482
+ Cross-Origin-Embedder-Policy: require-corp
483
+ ```
484
+
485
+ ---
486
+
487
+ ## Built-in DevTools
488
+
489
+ Add `?debug` to the URL or set `debug: { exposeDevtools: true }`:
490
+
491
+ | Tab | What it shows |
492
+ | --- | ------------- |
493
+ | **Tree** | Virtual DOM tree with node inspector — attributes, styles, event listeners, mutation history, "why updated?" trail. Snapshot & diff. |
494
+ | **Performance** | Frame budget flamechart, worker-to-main latency (P50/P95/P99), dropped frames, mutation type chart, coalescing breakdown, sync read heatmap, worker CPU profiler. |
495
+ | **Log** | Live mutation stream, color-coded diffs, event round-trip tracer, time-travel replay with scrubber. |
496
+ | **Warnings** | Grouped by code with docs and fixes. Suppressible. |
497
+ | **Graph** | Causality DAG: events → mutation batches → affected DOM nodes. |
498
+
499
+ Console API available via `__ASYNC_DOM_DEVTOOLS__` for programmatic inspection.
500
+
501
+ ---
502
+
503
+ ## Examples
504
+
505
+ **[Live examples hub](https://lifeart.github.io/async-dom/)**
506
+
507
+ | Example | Description | Tags |
508
+ | ------- | ----------- | ---- |
509
+ | [7000 Nodes Grid](./examples/vanilla) | Interactive color grid with 7,000 DOM nodes from a worker | performance, events |
510
+ | [Counter](./examples/counter) | Minimal example — click handlers, textContent updates | beginner |
511
+ | [Todo List](./examples/todo) | Input sync, dynamic DOM, classList, keyboard events | input sync, dynamic DOM |
512
+ | [Multi-App](./examples/multi-app) | Two workers in shadow DOM — CSS isolation | isolation, shadow DOM |
513
+ | [Audio Player](./examples/audio-player) | Audio playback controlled from a worker | media API, callMethod |
514
+ | [React: Mandelbrot](./examples/react-mandelbrot) | Fractal renderer — 4,800 pixels computed in a worker | React, heavy compute |
515
+ | [Vue: Game of Life](./examples/vue-gameoflife) | 60x40 grid simulation — 2,400 cell DOM updates | Vue, simulation |
516
+ | [Svelte: Particle Life](./examples/svelte-particles) | 320 particles with attraction/repulsion rules | Svelte, simulation |
517
+ | [Framework Showcase](./examples/framework-showcase) | React + Vue + Svelte on one page, zero framework runtime on main thread | multi-framework |
518
+ | [DevTools Panel](./examples/vanilla/?debug) | 7000-node grid with built-in debug panel | devtools |
519
+
520
+ ```bash
521
+ npm run dev # run all examples locally
522
+ ```
523
+
524
+ ---
525
+
526
+ ## Comparison
527
+
528
+ | Feature | async-dom | [Partytown](https://partytown.builder.io/) | @ampproject/worker-dom |
529
+ | ------- | --------- | ------------------------------------------- | ------------------------------------------------------------ |
530
+ | Scope | Full app rendering | Third-party scripts only | AMP components only |
531
+ | Frameworks | React, Vue, Svelte, vanilla | N/A | AMP only |
532
+ | DOM API coverage | Broad (see compatibility table) | Proxy forwarding | Subset |
533
+ | Sync reads | SharedArrayBuffer | Service Worker + Atomics | No |
534
+ | Frame budgeting | Adaptive with priority | No | No |
535
+ | Binary protocol | 22 opcodes + string dedup | No | Transfer list |
536
+ | Multi-app isolation | Shadow DOM | No | No |
537
+ | WebSocket transport | Yes (remote rendering) | No | No |
538
+ | Content protection | Structural (worker isolation) | No | No |
539
+ | DevTools | Built-in 5-tab panel | No | No |
540
+ | Bundle (gzip) | ~21 KB (core, gzip) | ~12 KB | ~12 KB |
541
+ | Status | Active | Maintenance | Inactive |
542
+
543
+ ---
544
+
545
+ ## DOM API Compatibility
546
+
547
+ Layout reads require a SharedArrayBuffer sync channel. Without it, they return zero values. All other APIs work without special setup.
548
+
549
+ | Category | APIs | Status |
550
+ | -------- | ---- | ------ |
551
+ | Tree manipulation | appendChild, removeChild, insertBefore, append, prepend, replaceWith, before, after, replaceChildren | Full |
552
+ | Attributes | get/set/has/removeAttribute, NS variants, attributes iterable | Full |
553
+ | Properties | id, className, textContent, innerHTML, value, checked, disabled, selectedIndex, type | Full |
554
+ | ClassList | add, remove, toggle, contains, replace, length | Full |
555
+ | Style | style proxy (camelCase + kebab-case), cssText | Full |
556
+ | Dataset | Proxy-based data-* attribute access | Full |
557
+ | Events | addEventListener, removeEventListener, dispatchEvent, on* handlers, once option | Full |
558
+ | Queries | querySelector/All, getElementById, getElementsByTagName/ClassName, matches, closest, contains | Full |
559
+ | Layout reads | clientWidth/Height, scrollWidth/Height, offsetWidth/Height/Top/Left, getBoundingClientRect | Sync |
560
+ | Scroll | scrollTop, scrollLeft (get/set), scrollIntoView | Full |
561
+ | Media | play, pause, load, currentTime, duration, paused, ended, readyState | Full |
562
+ | Methods | focus, blur, click, select, showModal, close | Full |
563
+ | Clone | cloneNode (shallow + deep) | Full |
564
+ | Document | createElement, createTextNode, createComment, createDocumentFragment, createEvent, createRange, createTreeWalker | Full |
565
+ | Navigation | parentNode/Element, first/lastChild, next/previousSibling, first/lastElementChild, children, childElementCount, ownerDocument, isConnected, getRootNode | Full |
566
+ | insertAdjacentHTML | insertAdjacentHTML | Full |
567
+ | normalize | normalize() | Stub |
568
+ | Shadow DOM | attachShadow, shadowRoot | -- |
569
+ | outerHTML | outerHTML getter (read-only) | Full |
570
+ | Animations | animate, getAnimations | -- |
571
+ | Fullscreen | requestFullscreen | -- |
572
+ | Pointer capture | setPointerCapture, releasePointerCapture | -- |
573
+
574
+ ---
575
+
576
+ ## CLI Scaffold
577
+
578
+ ```bash
579
+ npx @lifeart/async-dom init my-app --template react-ts
580
+ ```
581
+
582
+ Templates: `vanilla-ts`, `react-ts`, `vue-ts`
583
+
584
+ ---
585
+
586
+ ## Browser Support
587
+
588
+ | Browser | Minimum | Notes |
589
+ | ------- | ------- | ----- |
590
+ | Chrome | 80+ | Full support |
591
+ | Firefox | 79+ | Full support |
592
+ | Safari | 15.2+ | Requires COOP/COEP for sync reads |
593
+ | Edge | 80+ | Full support (Chromium) |
594
+
595
+ ---
596
+
597
+ ## When Not to Use async-dom
598
+
599
+ - **SEO-dependent pages** — Worker-rendered content is not visible to search engine crawlers
600
+ - **Simple apps** — The worker overhead (initialization, message serialization, event round-trips) may exceed the benefit for lightweight UIs
601
+ - **Apps requiring sub-millisecond input response** — Event round-trips add 2-20ms of latency compared to same-thread handlers
602
+ - **Heavy third-party library integration** — Libraries that assume direct DOM access (D3, jQuery, analytics SDKs) will not work in the worker
603
+
604
+ ---
605
+
606
+ ## Development
607
+
608
+ ```bash
609
+ npm install # install dependencies
610
+ npm run dev # dev server with examples
611
+ npm run build # build ESM + CJS + declarations
612
+ npm test # 1,483 tests across 80 files
613
+ npm run typecheck # type-check
614
+ npm run lint # lint (Biome)
615
+ ```
616
+
617
+ ## Contributing
618
+
619
+ Contributions welcome. Please open an issue first. See the [issue tracker](https://github.com/lifeart/async-dom/issues).
620
+
621
+ ## License
622
+
623
+ MIT — see [LICENSE](./LICENSE).