bitwrench 2.0.24 → 2.0.30
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 +17 -9
- package/dist/bitwrench-bccl.cjs.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
- package/dist/bitwrench-debug.js +1 -1
- package/dist/bitwrench-debug.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +661 -174
- package/dist/bitwrench-lean.cjs.min.js +7 -7
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +690 -178
- package/dist/bitwrench-lean.es5.min.js +5 -5
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +661 -174
- package/dist/bitwrench-lean.esm.min.js +6 -6
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +661 -174
- package/dist/bitwrench-lean.umd.min.js +7 -7
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-css.cjs.js +1 -1
- package/dist/bitwrench-util-css.cjs.min.js +1 -1
- package/dist/bitwrench-util-css.es5.js +1 -1
- package/dist/bitwrench-util-css.es5.min.js +1 -1
- package/dist/bitwrench-util-css.esm.js +1 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -1
- package/dist/bitwrench-util-css.umd.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +659 -172
- package/dist/bitwrench.cjs.min.js +6 -6
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +6 -6
- package/dist/bitwrench.d.ts +666 -0
- package/dist/bitwrench.es5.js +687 -175
- package/dist/bitwrench.es5.min.js +6 -6
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +659 -172
- package/dist/bitwrench.esm.min.js +5 -5
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +659 -172
- package/dist/bitwrench.umd.min.js +6 -6
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +96 -96
- package/dist/bwserve.cjs.js +140 -7
- package/dist/bwserve.esm.js +141 -8
- package/dist/sri.json +46 -46
- package/docs/README.md +5 -3
- package/docs/bitwrench-for-wasm.md +851 -0
- package/docs/bitwrench-mcp.md +1 -1
- package/docs/bitwrench-taco-schema-discussion.md +694 -0
- package/docs/bitwrench_api.md +134 -24
- package/docs/bitwrench_typescript_usage.md +441 -0
- package/docs/component-cheatsheet.md +1 -1
- package/docs/framework-translation-table.md +1 -1
- package/docs/llm-bitwrench-guide.md +34 -6
- package/docs/routing.md +1 -1
- package/docs/state-management.md +27 -3
- package/docs/thinking-in-bitwrench.md +6 -5
- package/docs/tutorial-bwserve.md +1 -1
- package/docs/tutorial-website.md +1 -1
- package/package.json +16 -10
- package/readme.html +29 -14
- package/src/bitwrench-styles.js +17 -17
- package/src/bitwrench.d.ts +666 -0
- package/src/bitwrench.js +638 -150
- package/src/bwserve/bwclient.js +3 -3
- package/src/bwserve/client.js +26 -0
- package/src/bwserve/index.js +110 -3
- package/src/cli/attach.js +7 -5
- package/src/cli/serve.js +53 -9
- package/src/mcp/live.js +3 -1
- package/src/mcp/server.js +7 -7
- package/src/version.js +3 -3
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
# Bitwrench for WebAssembly
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
0. [The Thesis](#0-the-thesis)
|
|
6
|
+
1. [The WASM/JS Boundary Problem](#1-the-wasmjs-boundary-problem)
|
|
7
|
+
2. [TACO as a Rendering IDL](#2-taco-as-a-rendering-idl)
|
|
8
|
+
3. [bwserve: The Browser as a Display Server](#3-bwserve-the-browser-as-a-display-server)
|
|
9
|
+
4. [Architecture Patterns](#4-architecture-patterns)
|
|
10
|
+
5. [Rust Integration](#5-rust-integration)
|
|
11
|
+
6. [C/C++ Integration](#6-cc-integration)
|
|
12
|
+
7. [AI and Agent-Driven UI](#7-ai-and-agent-driven-ui)
|
|
13
|
+
8. [Why Not DOM Diffing?](#8-why-not-dom-diffing)
|
|
14
|
+
9. [Comparison to Other Approaches](#9-comparison-to-other-approaches)
|
|
15
|
+
10. [What You Get and What You Don't](#10-what-you-get-and-what-you-dont)
|
|
16
|
+
|
|
17
|
+
**Related docs:** [Thinking in Bitwrench](thinking-in-bitwrench.md) | [bwserve](bwserve.md) | [App Patterns](app-patterns.md) | [Embedded Tutorial](tutorial-embedded.md)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 0. The Thesis
|
|
22
|
+
|
|
23
|
+
Systems programmers who need a web UI have several options: per-DOM-call bindings (`web-sys`/`wasm-bindgen`), shipping a full language runtime to the browser (Blazor), learning a WASM-native component framework (Yew, Leptos, Dioxus), or wrapping a browser in a desktop shell (Tauri, Electron). Each makes a different set of tradeoffs around payload size, boundary crossing cost, and how much of the browser platform is accessible.
|
|
24
|
+
|
|
25
|
+
This document describes a different tradeoff: **the WASM module produces UI as JSON data, and a JS-side library handles rendering.**
|
|
26
|
+
|
|
27
|
+
The WASM module produces TACO objects -- `{t, a, c, o}`, bitwrench's plain-object component format. It sends them across the WASM/JS boundary as structured data. Bitwrench on the JS side materializes them into DOM elements with CSS, accessibility, text selection, forms, and browser-native layout. Events flow back. Screenshots flow back. The WASM module does not interact with the DOM directly.
|
|
28
|
+
|
|
29
|
+
With bwserve, the protocol extends to a full display-server model -- analogous to X11/Wayland for web UIs. The WASM module (running in-page, in a Worker, or as a network server) sends rendering commands and receives user events through a message protocol.
|
|
30
|
+
|
|
31
|
+
This architecture also applies to **AI-driven interfaces**. A local WASM model (or an agent connected to one) can generate TACO specs, push them to the browser, observe the result via screenshots, and iterate.
|
|
32
|
+
|
|
33
|
+
> **The core idea: a WASM module that computes a state change already knows what changed. It can say so directly with a targeted patch, rather than re-describing the entire UI for a differ to reconcile.**
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. The WASM/JS Boundary Problem
|
|
38
|
+
|
|
39
|
+
WebAssembly runs in a sandboxed linear memory space. It cannot touch the DOM directly. Every interaction with the browser requires crossing a boundary -- calling a JS function from WASM or vice versa. This boundary is the central architectural constraint of any WASM-based web application.
|
|
40
|
+
|
|
41
|
+
### The cost of crossing
|
|
42
|
+
|
|
43
|
+
Each boundary crossing has overhead: type marshaling, memory copying, function dispatch. The cost per crossing is small (~50-200ns), but it accumulates. A framework that crosses the boundary once per DOM operation -- one `createElement`, one `setAttribute`, one `appendChild` -- pays this cost hundreds or thousands of times per render.
|
|
44
|
+
|
|
45
|
+
### How existing frameworks handle it
|
|
46
|
+
|
|
47
|
+
**Yew / Leptos / Dioxus (Rust):** These frameworks run a virtual DOM or reactive graph inside WASM and call `web-sys` bindings (which wrap `wasm-bindgen`) for each DOM mutation. A render of 100 elements means hundreds of boundary crossings: create element, set attribute, set text, append child, for each node. The frameworks optimize by batching and diffing, but the fundamental cost is per-DOM-operation.
|
|
48
|
+
|
|
49
|
+
**Blazor (.NET):** Ships the entire .NET runtime as WASM (~2-5MB download). Every DOM mutation crosses the boundary through a JS interop layer. The runtime overhead is substantial both in payload and per-operation cost.
|
|
50
|
+
|
|
51
|
+
**Raw `web-sys`:** The most direct approach. You call `document.create_element("div")` from Rust, which becomes a `wasm-bindgen` call, which becomes a JS function call, which calls the DOM API. This works and gives full control, but each DOM operation is a separate boundary crossing.
|
|
52
|
+
|
|
53
|
+
### An alternative: batch the boundary
|
|
54
|
+
|
|
55
|
+
A different approach is to cross the boundary **once per render, not once per DOM operation.** The WASM module builds a TACO object (a JSON-serializable data structure) and sends it across the boundary as a single message. The JS-side library materializes the entire tree in one shot using `bw.createDOM()` or `bw.DOM()`.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
WASM side JS side (bitwrench)
|
|
59
|
+
--------- -------------------
|
|
60
|
+
Build TACO tree in linear memory |
|
|
61
|
+
Serialize to JSON string |
|
|
62
|
+
|
|
|
63
|
+
--- one boundary crossing ---> |
|
|
64
|
+
JSON.parse()
|
|
65
|
+
bw.DOM('#app', taco)
|
|
66
|
+
createElement('div')
|
|
67
|
+
setAttribute('class', 'card')
|
|
68
|
+
createTextNode('Hello')
|
|
69
|
+
appendChild(...)
|
|
70
|
+
... (all native JS, zero crossings)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
100 elements = 1 crossing, not 300+. The DOM operations happen inside JS where they're native function calls with zero marshaling overhead.
|
|
74
|
+
|
|
75
|
+
For incremental updates, the same principle applies. Instead of diffing an entire virtual tree across the boundary, the WASM module sends a targeted patch message: `{type: 'patch', target: '#counter', content: '42'}`. One crossing, one DOM mutation.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 2. TACO as a Rendering IDL
|
|
80
|
+
|
|
81
|
+
An IDL (Interface Definition Language) defines a contract between two systems that may use different languages and memory models. Protocol Buffers are an IDL for RPC. OpenAPI is an IDL for REST APIs. TACO is effectively an **IDL for UI rendering.**
|
|
82
|
+
|
|
83
|
+
### The format
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
{
|
|
87
|
+
t: 'div', // tag name
|
|
88
|
+
a: { class: 'card', id: 'main' }, // HTML attributes
|
|
89
|
+
c: [ // content: string, TACO, or array
|
|
90
|
+
{ t: 'h1', c: 'Dashboard' },
|
|
91
|
+
{ t: 'p', c: 'Status: online' }
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Four keys. `t` is the tag. `a` is attributes. `c` is content. `o` is options (lifecycle hooks, state -- JS-side only). The first three are pure data. Any language that can produce JSON can produce a TACO.
|
|
97
|
+
|
|
98
|
+
### Why this matters for WASM
|
|
99
|
+
|
|
100
|
+
A Rust struct, a C string buffer, a Python dict -- all can represent `{t, a, c}`. There is no special type system, no framework-specific abstractions, no closures or proxies to marshal. The TACO is data that describes UI. The renderer is a separate system that consumes that data.
|
|
101
|
+
|
|
102
|
+
Compare this to what you'd need to cross the boundary for other frameworks:
|
|
103
|
+
|
|
104
|
+
| Framework | What crosses the boundary | Serializable? |
|
|
105
|
+
|-----------|--------------------------|---------------|
|
|
106
|
+
| React | Virtual DOM tree with closures, refs, fiber nodes | No |
|
|
107
|
+
| Vue | Reactive proxy objects with dependency tracking | No |
|
|
108
|
+
| Svelte | Compiler-generated update functions | No (they're code) |
|
|
109
|
+
| Solid | Signal graph with closure-based subscriptions | No |
|
|
110
|
+
| **Bitwrench** | **JSON object: `{t, a, c}`** | **Yes** |
|
|
111
|
+
|
|
112
|
+
The other frameworks in this table were designed for same-runtime use -- the component model and the renderer share a JS heap. TACO was designed for cases where the producer and renderer are separated by a boundary (server-driven UI via bwserve, embedded devices like ESP32). That same property makes it usable across the WASM/JS boundary without adaptation.
|
|
113
|
+
|
|
114
|
+
### The `o` split
|
|
115
|
+
|
|
116
|
+
The fourth key, `o:`, contains JavaScript-specific concerns: lifecycle hooks (`mounted`, `unmount`), component state (`state`, `render`), and handle methods (`handle`, `slots`). These are functions that can only live in JS.
|
|
117
|
+
|
|
118
|
+
For WASM, this split is a natural fit: the **specification** (what to render) crosses the boundary as data, while the **behavior** (DOM event handling, lifecycle management) stays in JS where DOM access is native.
|
|
119
|
+
|
|
120
|
+
For cases where the WASM module needs to define behavior, bwserve provides `register` and `call` -- send a function string once, invoke it by name later. This keeps the boundary thin while allowing the WASM side to install client-side logic when needed.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 3. bwserve: The Browser as a Display Server
|
|
125
|
+
|
|
126
|
+
bwserve is bitwrench's server-driven UI protocol. It was originally designed for Node.js servers pushing UI to browsers. But the protocol is transport-agnostic -- any process that can send JSON messages can drive a bitwrench UI. Including a WASM module.
|
|
127
|
+
|
|
128
|
+
### The protocol
|
|
129
|
+
|
|
130
|
+
Nine message types. Five for DOM operations, three for code execution, one for component dispatch:
|
|
131
|
+
|
|
132
|
+
| Type | What it does | Example |
|
|
133
|
+
|------|-------------|---------|
|
|
134
|
+
| `replace` | Replace element subtree | `{type:'replace', target:'#app', node:{t:'div', c:'Hello'}}` |
|
|
135
|
+
| `patch` | Update text or attributes | `{type:'patch', target:'counter', content:'42'}` |
|
|
136
|
+
| `append` | Add child element | `{type:'append', target:'#list', node:{t:'li', c:'New'}}` |
|
|
137
|
+
| `remove` | Delete element | `{type:'remove', target:'#old'}` |
|
|
138
|
+
| `batch` | Multiple operations | `{type:'batch', ops:[...]}` |
|
|
139
|
+
| `register` | Send named function | `{type:'register', name:'scroll', body:'function(s){...}'}` |
|
|
140
|
+
| `call` | Invoke function by name | `{type:'call', name:'scroll', args:['#chat']}` |
|
|
141
|
+
| `exec` | Run arbitrary JS | `{type:'exec', code:'document.title="Hi"'}` |
|
|
142
|
+
| `message` | Component dispatch | `{type:'message', target:'#card', action:'setTitle', data:'New'}` |
|
|
143
|
+
|
|
144
|
+
### The full loop
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
WASM Module (application) Browser (display server)
|
|
148
|
+
------------------------- -----------------------
|
|
149
|
+
State + logic bitwrench + bwserve client
|
|
150
|
+
| |
|
|
151
|
+
|-- replace: full render --------> bw.apply() -> bw.DOM()
|
|
152
|
+
|-- patch: targeted update ------> bw.apply() -> bw.patch()
|
|
153
|
+
|-- call: scroll, focus, etc. ---> bw.apply() -> invoke function
|
|
154
|
+
|-- batch: multiple ops ---------> bw.apply() -> sequential
|
|
155
|
+
| |
|
|
156
|
+
<-- event: {target, type, data} -- subscribed listener fires
|
|
157
|
+
<-- screenshot: PNG buffer ------- html2canvas -> POST-back
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The browser renders what it's told, forwards user events back through the protocol, and can capture screenshots. The WASM module owns state, makes decisions, and pushes UI updates.
|
|
161
|
+
|
|
162
|
+
### Where the WASM module runs
|
|
163
|
+
|
|
164
|
+
The bwserve protocol works regardless of where the WASM module lives:
|
|
165
|
+
|
|
166
|
+
**Same page (in-browser WASM):** The WASM module runs in the main thread or a Web Worker. Messages pass through `postMessage` or direct function calls. Lowest latency, simplest deployment -- one HTML file.
|
|
167
|
+
|
|
168
|
+
**Web Worker:** The WASM module runs in a dedicated Worker thread. Messages pass through `postMessage`. The UI thread stays responsive even during heavy computation. Good for CPU-intensive applications (simulation, ML inference, data processing).
|
|
169
|
+
|
|
170
|
+
**Service Worker:** The WASM module intercepts fetch requests and serves bwserve protocol responses. The page opens an EventSource to the Service Worker. Works offline once the Service Worker is installed.
|
|
171
|
+
|
|
172
|
+
**Network server:** The WASM module compiles to WASI and runs as an actual HTTP server (or compiles to native). The browser connects via SSE over the network. This is the standard bwserve architecture -- the transport just happens to be HTTP instead of in-process messaging.
|
|
173
|
+
|
|
174
|
+
All four deployments use the same protocol messages. The WASM module's code doesn't change -- only the transport layer differs.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 4. Architecture Patterns
|
|
179
|
+
|
|
180
|
+
### Pattern A: In-Page WASM with Direct Calls
|
|
181
|
+
|
|
182
|
+
The simplest pattern. The WASM module exposes functions that return TACO JSON strings. JavaScript calls them and renders the result.
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
index.html
|
|
186
|
+
loads bitwrench.umd.js
|
|
187
|
+
loads app.wasm (via wasm-bindgen, emscripten, etc.)
|
|
188
|
+
calls wasm.render() -> returns TACO JSON -> bw.DOM('#app', JSON.parse(result))
|
|
189
|
+
on user event -> calls wasm.handle_event(event_data) -> returns patch JSON
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
JavaScript glue (minimal):
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// Glue code -- the only JS you write
|
|
196
|
+
import init, { render, handle_event } from './app.js'; // wasm-bindgen output
|
|
197
|
+
|
|
198
|
+
async function start() {
|
|
199
|
+
await init();
|
|
200
|
+
|
|
201
|
+
// Initial render -- WASM produces TACO, JS renders it
|
|
202
|
+
var taco = JSON.parse(render());
|
|
203
|
+
bw.DOM('#app', taco);
|
|
204
|
+
|
|
205
|
+
// Forward events to WASM by element ID.
|
|
206
|
+
// The WASM module produces elements with IDs; the glue
|
|
207
|
+
// routes events on those elements back to WASM.
|
|
208
|
+
bw.$('#app')[0].addEventListener('click', function(e) {
|
|
209
|
+
var id = e.target.id;
|
|
210
|
+
if (!id) return;
|
|
211
|
+
|
|
212
|
+
var event = JSON.stringify({
|
|
213
|
+
target: id,
|
|
214
|
+
type: 'click',
|
|
215
|
+
value: e.target.value || ''
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
var messages = JSON.parse(handle_event(event));
|
|
219
|
+
messages.forEach(function(msg) { bw.apply(msg); });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
start();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The Rust side (see Section 5 for full detail) builds JSON strings. The glue handles rendering and event forwarding.
|
|
227
|
+
|
|
228
|
+
### Pattern B: Worker-Based WASM with Message Passing
|
|
229
|
+
|
|
230
|
+
For CPU-intensive applications. The WASM module runs in a Worker. The main thread handles rendering and event dispatch.
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
main thread Worker thread
|
|
234
|
+
----------- -------------
|
|
235
|
+
bitwrench.js app.wasm
|
|
236
|
+
bw.DOM(), bw.apply() state + logic
|
|
237
|
+
| |
|
|
238
|
+
|<-- postMessage: bwserve msg ------|
|
|
239
|
+
|--- postMessage: user action ----->|
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Main thread (glue):
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
var worker = new Worker('worker.js');
|
|
246
|
+
|
|
247
|
+
worker.onmessage = function(e) {
|
|
248
|
+
// Worker sends bwserve protocol messages
|
|
249
|
+
var messages = e.data;
|
|
250
|
+
messages.forEach(function(msg) { bw.apply(msg); });
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Forward user events to Worker by element ID
|
|
254
|
+
document.addEventListener('click', function(e) {
|
|
255
|
+
if (e.target.id) {
|
|
256
|
+
worker.postMessage({ target: e.target.id, type: 'click' });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Worker:
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
// worker.js
|
|
265
|
+
importScripts('app_wasm_glue.js');
|
|
266
|
+
|
|
267
|
+
// WASM module sends rendering commands back to main thread
|
|
268
|
+
function send(messages) {
|
|
269
|
+
postMessage(messages);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
onmessage = function(e) {
|
|
273
|
+
var result = wasm_handle_event(JSON.stringify(e.data));
|
|
274
|
+
send(JSON.parse(result));
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Initial render
|
|
278
|
+
wasm_init().then(function() {
|
|
279
|
+
send(JSON.parse(wasm_render()));
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The WASM module in the Worker has the same interface as Pattern A -- it produces JSON, the glue forwards it. The Worker boundary is invisible to both sides.
|
|
284
|
+
|
|
285
|
+
### Pattern C: WASM Server via bwserve (Network)
|
|
286
|
+
|
|
287
|
+
The WASM module compiles to a server binary (native or WASI). It speaks HTTP and serves the bwserve protocol. The browser connects via SSE -- standard bwserve, no special WASM integration needed.
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
WASM server binary Browser
|
|
291
|
+
(compiled to native) (any browser)
|
|
292
|
+
| |
|
|
293
|
+
|-- GET / -> HTML shell ---------->|
|
|
294
|
+
|-- GET /bw/events -> SSE -------->|
|
|
295
|
+
|-- SSE: replace/patch/etc ------->| bw.apply() -> DOM
|
|
296
|
+
|<-- POST /bw/return/action -------|
|
|
297
|
+
|<-- POST /bw/return/screenshot ---|
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
This pattern has the broadest capabilities. The WASM module:
|
|
301
|
+
- Runs as a real server process (can access filesystem, databases, hardware)
|
|
302
|
+
- Gets auto-reconnect for free (SSE spec)
|
|
303
|
+
- Supports multiple browser clients simultaneously
|
|
304
|
+
- Can capture screenshots via `client.screenshot()`
|
|
305
|
+
- Works behind a reverse proxy, in Docker, on a Raspberry Pi
|
|
306
|
+
|
|
307
|
+
The same Rust/C++ code that runs as an in-page WASM module can compile to a native server binary with a different transport layer. The application logic is identical.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 5. Rust Integration
|
|
312
|
+
|
|
313
|
+
### Producing TACO from Rust
|
|
314
|
+
|
|
315
|
+
TACO is JSON. Rust's `serde_json` produces JSON. The integration is straightforward.
|
|
316
|
+
|
|
317
|
+
```rust
|
|
318
|
+
use serde_json::json;
|
|
319
|
+
|
|
320
|
+
fn render_dashboard(state: &AppState) -> String {
|
|
321
|
+
json!({
|
|
322
|
+
"t": "div", "a": {"class": "bw-container"}, "c": [
|
|
323
|
+
{"t": "h1", "c": &state.title},
|
|
324
|
+
{"t": "div", "a": {"class": "bw-row"}, "c": [
|
|
325
|
+
stat_card("Users", &state.users.to_string(), "primary"),
|
|
326
|
+
stat_card("Revenue", &format!("${}", state.revenue), "success"),
|
|
327
|
+
stat_card("Orders", &state.orders.to_string(), "info")
|
|
328
|
+
]},
|
|
329
|
+
{"t": "button", "a": {
|
|
330
|
+
"id": "refresh-btn",
|
|
331
|
+
"class": "bw-btn bw-btn-primary"
|
|
332
|
+
}, "c": "Refresh"}
|
|
333
|
+
]
|
|
334
|
+
}).to_string()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fn stat_card(label: &str, value: &str, variant: &str) -> serde_json::Value {
|
|
338
|
+
json!({
|
|
339
|
+
"t": "div", "a": {"class": format!("bw-stat-card bw-stat-card-{}", variant)}, "c": [
|
|
340
|
+
{"t": "div", "a": {"class": "bw-stat-card-value"}, "c": value},
|
|
341
|
+
{"t": "div", "a": {"class": "bw-stat-card-label"}, "c": label}
|
|
342
|
+
]
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### A helper crate (optional)
|
|
348
|
+
|
|
349
|
+
For ergonomics, a thin wrapper around `serde_json::Value`:
|
|
350
|
+
|
|
351
|
+
```rust
|
|
352
|
+
// taco.rs -- ~50 lines, not a framework
|
|
353
|
+
|
|
354
|
+
use serde_json::{json, Value};
|
|
355
|
+
|
|
356
|
+
pub fn tag(t: &str) -> Value {
|
|
357
|
+
json!({"t": t})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
pub fn el(t: &str, attrs: Value, children: Vec<Value>) -> Value {
|
|
361
|
+
json!({"t": t, "a": attrs, "c": children})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
pub fn text(t: &str, content: &str) -> Value {
|
|
365
|
+
json!({"t": t, "c": content})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
pub fn div(attrs: Value, children: Vec<Value>) -> Value {
|
|
369
|
+
el("div", attrs, children)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
pub fn button(label: &str, id: &str) -> Value {
|
|
373
|
+
json!({
|
|
374
|
+
"t": "button",
|
|
375
|
+
"a": {"id": id, "class": "bw-btn bw-btn-primary"},
|
|
376
|
+
"c": label
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// bwserve protocol messages
|
|
381
|
+
pub fn msg_replace(target: &str, node: Value) -> Value {
|
|
382
|
+
json!({"type": "replace", "target": target, "node": node})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
pub fn msg_patch(target: &str, content: &str) -> Value {
|
|
386
|
+
json!({"type": "patch", "target": target, "content": content})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
pub fn msg_batch(ops: Vec<Value>) -> Value {
|
|
390
|
+
json!({"type": "batch", "ops": ops})
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
With this helper:
|
|
395
|
+
|
|
396
|
+
```rust
|
|
397
|
+
use taco::*;
|
|
398
|
+
|
|
399
|
+
fn render(state: &AppState) -> String {
|
|
400
|
+
msg_replace("#app", div(
|
|
401
|
+
json!({"class": "container"}),
|
|
402
|
+
vec![
|
|
403
|
+
text("h1", &state.title),
|
|
404
|
+
text("p", &format!("Count: {}", state.count)),
|
|
405
|
+
button("+1", "inc-btn"),
|
|
406
|
+
button("Reset", "reset-btn"),
|
|
407
|
+
]
|
|
408
|
+
)).to_string()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
fn handle_event(state: &mut AppState, target_id: &str) -> String {
|
|
412
|
+
match target_id {
|
|
413
|
+
"inc-btn" => {
|
|
414
|
+
state.count += 1;
|
|
415
|
+
msg_patch("count", &state.count.to_string()).to_string()
|
|
416
|
+
}
|
|
417
|
+
"reset-btn" => {
|
|
418
|
+
state.count = 0;
|
|
419
|
+
msg_patch("count", "0").to_string()
|
|
420
|
+
}
|
|
421
|
+
_ => "[]".to_string()
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The WASM module produces JSON and receives JSON. DOM bindings (`web-sys`) are not needed.
|
|
427
|
+
|
|
428
|
+
### wasm-bindgen export (in-page pattern)
|
|
429
|
+
|
|
430
|
+
```rust
|
|
431
|
+
use wasm_bindgen::prelude::*;
|
|
432
|
+
use std::sync::Mutex;
|
|
433
|
+
|
|
434
|
+
static STATE: Mutex<AppState> = Mutex::new(AppState::new());
|
|
435
|
+
|
|
436
|
+
#[wasm_bindgen]
|
|
437
|
+
pub fn render() -> String {
|
|
438
|
+
let state = STATE.lock().unwrap();
|
|
439
|
+
render_dashboard(&state)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#[wasm_bindgen]
|
|
443
|
+
pub fn handle_event(event_json: &str) -> String {
|
|
444
|
+
let event: serde_json::Value = serde_json::from_str(event_json).unwrap();
|
|
445
|
+
let target = event["target"].as_str().unwrap_or("");
|
|
446
|
+
let mut state = STATE.lock().unwrap();
|
|
447
|
+
handle_event_inner(&mut state, target)
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Axum/Actix server (network pattern)
|
|
452
|
+
|
|
453
|
+
The same rendering code compiles to a native server:
|
|
454
|
+
|
|
455
|
+
```rust
|
|
456
|
+
use axum::{routing::get, Router};
|
|
457
|
+
use axum::response::sse::{Event, Sse};
|
|
458
|
+
use futures::stream::Stream;
|
|
459
|
+
|
|
460
|
+
async fn sse_handler() -> Sse<impl Stream<Item = Result<Event, std::io::Error>>> {
|
|
461
|
+
let state = AppState::new();
|
|
462
|
+
let initial = render_dashboard(&state);
|
|
463
|
+
let event = Event::default().data(initial);
|
|
464
|
+
// ... SSE stream setup
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#[tokio::main]
|
|
468
|
+
async fn main() {
|
|
469
|
+
let app = Router::new()
|
|
470
|
+
.route("/bw/events", get(sse_handler))
|
|
471
|
+
.route("/", get(serve_shell));
|
|
472
|
+
// ...
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Same `render_dashboard()`, same `handle_event()`. Different transport. The rendering logic is independent of deployment target.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## 6. C/C++ Integration
|
|
481
|
+
|
|
482
|
+
C and C++ developers face the same boundary problem, often with tighter constraints -- embedded targets, no allocator, fixed-size buffers. Bitwrench includes a relaxed JSON format (`r-prefix`) that addresses C/C++ string-building ergonomics.
|
|
483
|
+
|
|
484
|
+
### Relaxed JSON for C strings
|
|
485
|
+
|
|
486
|
+
Standard JSON in C requires escaping every double quote:
|
|
487
|
+
|
|
488
|
+
```c
|
|
489
|
+
// Standard JSON -- escape nightmare
|
|
490
|
+
char msg[] = "{\"type\":\"patch\",\"target\":\"temp\",\"content\":\"23.5 C\"}";
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Bitwrench's r-prefix format uses single quotes:
|
|
494
|
+
|
|
495
|
+
```c
|
|
496
|
+
// r-prefix relaxed JSON -- natural C strings
|
|
497
|
+
char msg[] = "r{'type':'patch','target':'temp','content':'23.5 C'}";
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`bw.parseJSONFlex()` on the browser side converts single quotes to double quotes before parsing. The `r` prefix signals the format.
|
|
501
|
+
|
|
502
|
+
### C helper macros
|
|
503
|
+
|
|
504
|
+
```c
|
|
505
|
+
#include <stdio.h>
|
|
506
|
+
#include <string.h>
|
|
507
|
+
|
|
508
|
+
// Simple TACO builder macros for fixed-size buffers
|
|
509
|
+
#define BW_PATCH(buf, target, content) \
|
|
510
|
+
snprintf(buf, sizeof(buf), \
|
|
511
|
+
"r{'type':'patch','target':'%s','content':'%s'}", \
|
|
512
|
+
target, content)
|
|
513
|
+
|
|
514
|
+
#define BW_REPLACE(buf, target, taco) \
|
|
515
|
+
snprintf(buf, sizeof(buf), \
|
|
516
|
+
"r{'type':'replace','target':'%s','node':%s}", \
|
|
517
|
+
target, taco)
|
|
518
|
+
|
|
519
|
+
// TACO builders
|
|
520
|
+
#define TACO_TEXT(buf, tag, text) \
|
|
521
|
+
snprintf(buf, sizeof(buf), "{'t':'%s','c':'%s'}", tag, text)
|
|
522
|
+
|
|
523
|
+
#define TACO_DIV(buf, cls, children) \
|
|
524
|
+
snprintf(buf, sizeof(buf), \
|
|
525
|
+
"{'t':'div','a':{'class':'%s'},'c':[%s]}", cls, children)
|
|
526
|
+
|
|
527
|
+
// Usage
|
|
528
|
+
void update_display(float temp, int humidity) {
|
|
529
|
+
char temp_str[16], humid_str[16];
|
|
530
|
+
char card1[128], card2[128], row[512], msg[1024];
|
|
531
|
+
|
|
532
|
+
snprintf(temp_str, sizeof(temp_str), "%.1f C", temp);
|
|
533
|
+
snprintf(humid_str, sizeof(humid_str), "%d%%", humidity);
|
|
534
|
+
|
|
535
|
+
TACO_TEXT(card1, "p", temp_str);
|
|
536
|
+
TACO_TEXT(card2, "p", humid_str);
|
|
537
|
+
|
|
538
|
+
snprintf(row, sizeof(row), "%s,%s", card1, card2);
|
|
539
|
+
TACO_DIV(msg, "readings", row);
|
|
540
|
+
|
|
541
|
+
// Send via whatever transport (HTTP response, WebSocket, serial)
|
|
542
|
+
send_to_browser(msg);
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Emscripten WASM
|
|
547
|
+
|
|
548
|
+
For C/C++ compiled to WASM via Emscripten:
|
|
549
|
+
|
|
550
|
+
```c
|
|
551
|
+
#include <emscripten.h>
|
|
552
|
+
|
|
553
|
+
// Called from JS glue
|
|
554
|
+
EMSCRIPTEN_KEEPALIVE
|
|
555
|
+
const char* render() {
|
|
556
|
+
static char buf[4096];
|
|
557
|
+
float temp = read_sensor();
|
|
558
|
+
|
|
559
|
+
snprintf(buf, sizeof(buf),
|
|
560
|
+
"r{'type':'replace','target':'#app','node':"
|
|
561
|
+
"{'t':'div','c':["
|
|
562
|
+
"{'t':'h1','c':'Sensor Dashboard'},"
|
|
563
|
+
"{'t':'p','a':{'id':'temp'},'c':'%.1f C'},"
|
|
564
|
+
"{'t':'button','a':{'id':'toggle-btn','class':'bw-btn'},"
|
|
565
|
+
"'c':'Toggle LED'}"
|
|
566
|
+
"]}}", temp);
|
|
567
|
+
|
|
568
|
+
return buf;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
EMSCRIPTEN_KEEPALIVE
|
|
572
|
+
const char* handle_event(const char* target_id) {
|
|
573
|
+
static char buf[256];
|
|
574
|
+
|
|
575
|
+
if (strcmp(target_id, "toggle-btn") == 0) {
|
|
576
|
+
toggle_led();
|
|
577
|
+
BW_PATCH(buf, "led-status", led_state() ? "ON" : "OFF");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return buf;
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The JS glue calls `render()` and `handle_event()`, gets back JSON strings, and passes them to `bw.apply()`. The C code does not interact with the DOM.
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## 7. AI and Agent-Driven UI
|
|
589
|
+
|
|
590
|
+
### The premise
|
|
591
|
+
|
|
592
|
+
A local WASM module (or a native process compiled from the same source) runs an AI model or hosts an agent. It needs to present a UI to the user. The UI is not a fixed layout with dynamic data -- it's a **dynamically generated interface** that the AI constructs based on context, user history, task state, and its own reasoning.
|
|
593
|
+
|
|
594
|
+
This is different from traditional web apps where the UI is designed at development time and data fills in at runtime. Here, the UI itself is generated at runtime.
|
|
595
|
+
|
|
596
|
+
### TACO as AI output format
|
|
597
|
+
|
|
598
|
+
AI models typically output structured data -- JSON, function calls, tool use. TACO is structured data that maps directly to UI. The model needs to produce objects with four keys: `t`, `a`, `c`, `o`. No HTML syntax, JSX conventions, or framework-specific component APIs.
|
|
599
|
+
|
|
600
|
+
```json
|
|
601
|
+
{"t": "div", "c": [
|
|
602
|
+
{"t": "h2", "c": "Analysis Results"},
|
|
603
|
+
{"t": "p", "c": "Found 3 anomalies in the dataset."},
|
|
604
|
+
{"t": "div", "a": {"class": "bw-card"}, "c": [
|
|
605
|
+
{"t": "h3", "c": "Anomaly #1"},
|
|
606
|
+
{"t": "p", "c": "Temperature spike at 14:32 UTC"}
|
|
607
|
+
]},
|
|
608
|
+
{"t": "button", "a": {"id": "investigate-btn"}, "c": "Investigate"}
|
|
609
|
+
]}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
This is a straightforward structured output for an LLM. The BCCL component vocabulary (makeCard, makeTable, makeAlert, makeStatCard) provides higher-level building blocks that a model can reference by name. The token count is lower than equivalent HTML or JSX because there is less syntactic overhead.
|
|
613
|
+
|
|
614
|
+
### The visual feedback loop
|
|
615
|
+
|
|
616
|
+
bwserve's screenshot capability closes the loop. The AI generates UI, the browser renders it, the AI captures a screenshot, evaluates the result visually (via a vision model or heuristics), and refines:
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
AI Model (WASM) Browser
|
|
620
|
+
----------- -------
|
|
621
|
+
Generate TACO spec |
|
|
622
|
+
|-- replace: render UI --------> bw.DOM() -> visible page
|
|
623
|
+
| |
|
|
624
|
+
|-- screenshot request --------> html2canvas -> capture
|
|
625
|
+
<-- PNG image data -------------|
|
|
626
|
+
| |
|
|
627
|
+
Evaluate screenshot |
|
|
628
|
+
(too cramped? colors wrong? |
|
|
629
|
+
text overflow? wrong layout?) |
|
|
630
|
+
| |
|
|
631
|
+
Generate refined TACO spec |
|
|
632
|
+
|-- replace: updated UI -------> bw.DOM() -> improved page
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
bwserve implements `client.screenshot()` for this purpose -- it sends a `call` message that triggers `html2canvas` on the client, which captures the DOM to a canvas, converts to PNG, and POSTs the image data back to the server. The server resolves a Promise with the image buffer.
|
|
636
|
+
|
|
637
|
+
### Agent architecture
|
|
638
|
+
|
|
639
|
+
A more complete agent pattern:
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
+-----------------+
|
|
643
|
+
| AI Agent (WASM)|
|
|
644
|
+
| |
|
|
645
|
+
| Reasoning loop:|
|
|
646
|
+
| 1. Observe | <-- screenshot, user actions
|
|
647
|
+
| 2. Plan | <-- internal state, goals
|
|
648
|
+
| 3. Act | --> TACO specs, patches
|
|
649
|
+
| 4. Evaluate | <-- screenshot feedback
|
|
650
|
+
+-----------------+
|
|
651
|
+
|
|
|
652
|
+
bwserve protocol
|
|
653
|
+
|
|
|
654
|
+
+-----------------+
|
|
655
|
+
| Browser |
|
|
656
|
+
| (display) |
|
|
657
|
+
| |
|
|
658
|
+
| bw.apply() |
|
|
659
|
+
| event dispatch |
|
|
660
|
+
| screenshot |
|
|
661
|
+
+-----------------+
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
The agent observes the UI state (via screenshots or DOM queries), plans updates based on its goals, acts by sending bwserve messages, and evaluates the result.
|
|
665
|
+
|
|
666
|
+
### Multi-surface agent UI
|
|
667
|
+
|
|
668
|
+
bwserve targets are CSS selectors. An agent can manage multiple independent UI regions:
|
|
669
|
+
|
|
670
|
+
```rust
|
|
671
|
+
// Agent manages three surfaces simultaneously
|
|
672
|
+
fn agent_step(state: &AgentState) -> Vec<Value> {
|
|
673
|
+
let mut messages = vec![];
|
|
674
|
+
|
|
675
|
+
// Main content area -- task-specific UI
|
|
676
|
+
messages.push(msg_replace("#main", render_task_ui(&state.current_task)));
|
|
677
|
+
|
|
678
|
+
// Sidebar -- agent's reasoning visible to user
|
|
679
|
+
messages.push(msg_replace("#sidebar", render_reasoning_log(&state.log)));
|
|
680
|
+
|
|
681
|
+
// Status bar -- progress and controls
|
|
682
|
+
messages.push(msg_patch("status", &format!(
|
|
683
|
+
"Step {}/{} -- {}", state.step, state.total, state.phase
|
|
684
|
+
)));
|
|
685
|
+
|
|
686
|
+
messages
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Each surface is independently addressable. The agent can update the reasoning sidebar without touching the main content. Targeted patches mean only the changed region re-renders.
|
|
691
|
+
|
|
692
|
+
### Local-first AI
|
|
693
|
+
|
|
694
|
+
A WASM-based AI agent running locally (in the browser or on localhost) has properties that cloud-based agents don't:
|
|
695
|
+
|
|
696
|
+
- **Privacy**: Data never leaves the device. The model runs in WASM, the UI runs in the browser, the communication is localhost or in-process.
|
|
697
|
+
- **Latency**: No network round-trip for UI updates. The WASM module and the browser are on the same machine. Sub-millisecond message delivery.
|
|
698
|
+
- **Offline**: Once the page is loaded and the WASM module is cached, everything works without a network connection.
|
|
699
|
+
- **Inspectable**: The bwserve protocol is JSON. You can log every message, replay sessions, debug with browser DevTools. The bwcli attach REPL can inspect the DOM in real time.
|
|
700
|
+
|
|
701
|
+
These properties are relevant for local AI assistants, on-device ML tools, privacy-sensitive applications, and developer tools where the data should not leave the machine.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## 8. Why Not DOM Diffing?
|
|
706
|
+
|
|
707
|
+
The virtual DOM was React's central contribution to UI architecture. The idea: the application describes the entire UI on every state change, and the framework diffs the new description against the previous one to determine the minimum set of DOM mutations. This relieves the developer from tracking what changed.
|
|
708
|
+
|
|
709
|
+
This is a reasonable tradeoff for a JavaScript-only application where closures and mutable state make dependency tracking hard. But the tradeoff looks different when the state authority and the renderer are separated by a WASM/JS boundary.
|
|
710
|
+
|
|
711
|
+
### Diffing across the WASM boundary
|
|
712
|
+
|
|
713
|
+
A WASM module that manages application state in linear memory typically knows what changed -- it just ran the computation that caused the change. Re-describing the entire UI tree so a JS-side differ can identify the delta has several costs:
|
|
714
|
+
|
|
715
|
+
1. **Wasteful**: Serialize the full tree across the WASM/JS boundary (O(n) data transfer) so the differ can do O(n) comparison to find the one thing that changed
|
|
716
|
+
2. **Redundant**: The WASM module already knows the answer. It computed `state.count += 1`. It knows `#counter` needs to update to `"43"`.
|
|
717
|
+
3. **Architecturally wrong**: The diff requires maintaining two copies of the virtual tree on the JS side -- the previous and the current. For a WASM app, that's state duplication across the boundary.
|
|
718
|
+
|
|
719
|
+
With bitwrench:
|
|
720
|
+
|
|
721
|
+
```rust
|
|
722
|
+
// The WASM module knows exactly what changed
|
|
723
|
+
state.count += 1;
|
|
724
|
+
msg_patch("counter", &state.count.to_string()) // one message, one DOM update
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
One patch message, one DOM mutation. The WASM module communicates what it already knows.
|
|
728
|
+
|
|
729
|
+
### Historical context
|
|
730
|
+
|
|
731
|
+
Desktop UI toolkits (MFC, Swing, Qt, Cocoa) used targeted updates rather than tree diffing. In MFC, `SetWindowText("New Title")` repainted the control's title. In Swing, `label.setText("43")` updated one label. In Qt, signal-slot connections trigger specific updates. The application tracked what changed and told the framework directly.
|
|
732
|
+
|
|
733
|
+
bitwrench follows the same pattern: `el.bw.setTitle('New')` updates one component, `bw.patch('#counter', '43')` updates one element, `client.patch('counter', '43')` sends one message. The application (or the WASM module) is the source of truth about what changed.
|
|
734
|
+
|
|
735
|
+
React's diffing approach solved a real problem -- in large JS applications with shared mutable state, manually tracking dependencies is error-prone. Whether that tradeoff applies to a given WASM application depends on the application's complexity and state management approach.
|
|
736
|
+
|
|
737
|
+
### When diffing would be needed
|
|
738
|
+
|
|
739
|
+
If your WASM module genuinely doesn't know what changed -- for example, if it receives an opaque data blob from an external source and re-derives the entire UI from scratch -- then a diff-based approach would be useful. bitwrench doesn't provide this. Some alternatives:
|
|
740
|
+
|
|
741
|
+
1. Diff the **data** in WASM (compare old state to new state)
|
|
742
|
+
2. Emit targeted patches for what changed
|
|
743
|
+
3. Or, for small enough UIs, just re-render with `replace` -- `bw.DOM()` is fast enough for most subtrees
|
|
744
|
+
|
|
745
|
+
Diffing data in WASM (where you have typed structs and linear memory) is cheaper than diffing virtual DOM trees in JS (where everything is heap-allocated objects with GC pressure).
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## 9. Comparison to Other Approaches
|
|
750
|
+
|
|
751
|
+
### Yew / Leptos / Dioxus (Rust WASM frameworks)
|
|
752
|
+
|
|
753
|
+
These are the most direct alternatives. They run a component framework inside WASM and call `web-sys` for DOM operations.
|
|
754
|
+
|
|
755
|
+
| Concern | Yew/Leptos/Dioxus | bitwrench + WASM |
|
|
756
|
+
|---------|-------------------|------------------|
|
|
757
|
+
| Component model | Rust macros + framework | JSON objects (TACO) |
|
|
758
|
+
| DOM access | Per-operation via web-sys | Batched via bw.apply() |
|
|
759
|
+
| Boundary crossings | 100s per render | 1 per render |
|
|
760
|
+
| Virtual DOM / signals | In WASM (Rust) | None (explicit patches) |
|
|
761
|
+
| CSS | Framework-specific | bw.css() / bw.loadStyles() |
|
|
762
|
+
| Accessibility | Manual | Browser-native (real HTML) |
|
|
763
|
+
| Server rendering | Framework-specific SSR | bw.html() or bwserve |
|
|
764
|
+
| Learning curve | Rust + framework | Rust + TACO (4 keys) |
|
|
765
|
+
| Bundle size | ~200KB+ (framework + app) | ~40KB (bitwrench) + app WASM |
|
|
766
|
+
|
|
767
|
+
The fundamental difference is where the component model lives. Yew/Leptos/Dioxus place it in WASM (Rust macros, reactive primitives, virtual DOM). The bitwrench approach places it in JS (TACO rendering, CSS generation, component handles) and uses JSON as the interface between the application language and the renderer.
|
|
768
|
+
|
|
769
|
+
### Blazor (.NET WASM)
|
|
770
|
+
|
|
771
|
+
| Concern | Blazor | bitwrench + WASM |
|
|
772
|
+
|---------|--------|------------------|
|
|
773
|
+
| Runtime payload | 2-5MB (.NET in WASM) | ~40KB (bitwrench) |
|
|
774
|
+
| Component model | Razor templates (.NET) | TACO JSON |
|
|
775
|
+
| DOM access | JS interop bridge | Batched via bw.apply() |
|
|
776
|
+
| Language | C# only | Any (Rust, C, C++, Go, ...) |
|
|
777
|
+
| Server mode | Blazor Server (SignalR) | bwserve (SSE) |
|
|
778
|
+
|
|
779
|
+
Blazor bundles a language runtime in WASM. The bitwrench approach does not require a language-specific runtime on the client -- the client library renders JSON regardless of what produced it.
|
|
780
|
+
|
|
781
|
+
### Tauri / Electron
|
|
782
|
+
|
|
783
|
+
| Concern | Tauri/Electron | bitwrench + WASM |
|
|
784
|
+
|---------|---------------|------------------|
|
|
785
|
+
| Deployment | Desktop app installer | URL (web page) |
|
|
786
|
+
| Backend | Native process (Rust/Node) | WASM (in-browser or server) |
|
|
787
|
+
| Frontend | You write JS/React/Vue | TACO from any language |
|
|
788
|
+
| Distribution | App store / download | Link sharing |
|
|
789
|
+
| Bundle size | 10-100MB+ | Page weight (~40KB + WASM) |
|
|
790
|
+
|
|
791
|
+
Tauri and Electron are designed for desktop application distribution. If the UI can be served as a web page, the packaging and distribution step is not needed -- the tradeoff is losing native OS integration (system tray, menus, file system access without permissions).
|
|
792
|
+
|
|
793
|
+
### Canvas-based (SDL + Emscripten, egui)
|
|
794
|
+
|
|
795
|
+
| Concern | Canvas/SDL/egui | bitwrench + WASM |
|
|
796
|
+
|---------|----------------|------------------|
|
|
797
|
+
| Rendering | Canvas pixels | Native HTML/CSS |
|
|
798
|
+
| Text selection | Not available | Browser-native |
|
|
799
|
+
| Accessibility | Not available | Browser-native (semantic HTML) |
|
|
800
|
+
| Forms/inputs | Custom reimplementation | Browser-native |
|
|
801
|
+
| CSS | Not applicable | Full CSS support |
|
|
802
|
+
| Right-click, find-in-page | Not available | Browser-native |
|
|
803
|
+
|
|
804
|
+
Canvas-based approaches bypass the browser's built-in UI capabilities. Generating real HTML elements preserves them, at the cost of not having pixel-level rendering control (which matters for games, CAD, and other graphics-intensive applications).
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## 10. What You Get and What You Don't
|
|
809
|
+
|
|
810
|
+
### What you get
|
|
811
|
+
|
|
812
|
+
- Browser-native UI (real HTML/CSS) from any language that produces JSON
|
|
813
|
+
- Batched boundary crossing -- one message per render, not one per DOM op
|
|
814
|
+
- 9-verb protocol (bwserve) for remote UI control including screenshots
|
|
815
|
+
- ~40KB client library with 30+ components, CSS generation, theming
|
|
816
|
+
- No build step on the JS side
|
|
817
|
+
- Same application code can target in-page WASM, Worker, or native server
|
|
818
|
+
- Browser capabilities preserved: accessibility, text selection, CSS layout, forms
|
|
819
|
+
- Relaxed JSON (r-prefix) for C/C++ string-building ergonomics
|
|
820
|
+
- Debug tools: bwcli attach REPL, protocol logging, DOM inspection
|
|
821
|
+
|
|
822
|
+
### What you don't get
|
|
823
|
+
|
|
824
|
+
- **No automatic change detection.** You must know what changed and say so. (This is usually trivial in WASM where you just ran the mutation.)
|
|
825
|
+
- **No type-safe TACO builder in Rust/C** -- you're building JSON. A helper crate/header can add convenience, but there's no compile-time guarantee that your TACO is valid. (bitwrench is lenient -- invalid fields are silently ignored.)
|
|
826
|
+
- **No client-side routing from WASM.** The browser-side `bw.router()` exists for JS, but a WASM server would manage navigation via bwserve's `replace` messages.
|
|
827
|
+
- **No two-way data binding.** User events come back as structured messages (element ID, event type, value). You update your state and send a patch. This is the expected pattern for a boundary architecture.
|
|
828
|
+
- **No animation primitives from WASM.** CSS animations and transitions work (they're in the browser), but you'd define them via `bw.css()` on the JS side or in a stylesheet, not from WASM.
|
|
829
|
+
|
|
830
|
+
### The mental model
|
|
831
|
+
|
|
832
|
+
Think of it this way:
|
|
833
|
+
|
|
834
|
+
- **Your WASM module** is the **application**. It owns state, runs logic, makes decisions.
|
|
835
|
+
- **Bitwrench** is the **display server**. It takes rendering commands and presents them to the user.
|
|
836
|
+
- **The bwserve protocol** is the **wire format** between them.
|
|
837
|
+
- **The browser** is the **display hardware**. It provides pixels, layout, input devices, accessibility.
|
|
838
|
+
|
|
839
|
+
This separation -- application, window manager, display -- is common in GUI architectures (X11, Wayland, Windows GDI). The adaptation for the web platform is that the wire format is JSON and the display server is a browser tab.
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
**Related:**
|
|
844
|
+
- [Thinking in Bitwrench](thinking-in-bitwrench.md) -- core philosophy and TACO patterns
|
|
845
|
+
- [bwserve](bwserve.md) -- full protocol reference and server API
|
|
846
|
+
- [App Patterns](app-patterns.md) -- five canonical app architectures
|
|
847
|
+
- [Embedded Tutorial](tutorial-embedded.md) -- ESP32/Arduino integration (same patterns, C focus)
|
|
848
|
+
- [Component Library](component-library.md) -- all BCCL `make*()` factories
|
|
849
|
+
- [State Management](state-management.md) -- levels 0-2, pub/sub, handles
|
|
850
|
+
|
|
851
|
+
*Bitwrench is maintained by [Manu Chatterjee](https://github.com/deftio) (deftio). BSD-2-Clause license.*
|