# The LeanUI protocol (version 1) Lean never touches the DOM. Every update produces one **command buffer**; a JavaScript runtime (`runtime/leanui.js`) applies it. The same bytes are produced in the browser (wasm64) and on a native server, and `LeanUI/Testing/Dom.lean` is an executable reference model of the semantics below. This document is normative for both. ## 1. Framing A buffer is two byte arrays (and, in the wasm host, the byte arrays its pixel and file commands refer to: *blobs*, section 8): - **words**: little-endian unsigned 32-bit integers, a sequence of *commands*. Each command is an opcode word followed by its operand words (section 3): a fixed number, except for `TEMPLATE` and `CLONE`, whose operands end with a count and that many node handles. - **strings**: UTF-8 text. A string operand is two words, `(offset, length)`, counted in **UTF-16 code units** from the start of the decoded string area. The receiver decodes the whole area once (`TextDecoder`) and slices it (`substr(offset, length)`), so a buffer costs one decode no matter how many strings it carries. Writers compute UTF-16 lengths as 1 unit per code point below U+10000 and 2 above. In the browser, the wasm module passes both arrays as views into its memory (no copy). Over HTTP (server-driven mode) they travel as `[u32 wordsBytes][words][strings]` (`Buf.encode`). ## 2. Identifiers - **Node handles** (`u32`, ≥ 1) name DOM nodes. Lean allocates them (with reuse after `FREE`), so commands never need answers. Handle 0 is never used. - **Names** (`u32`, ≥ 1) stand for tag, attribute, property and event names. A name is defined by `INTERN id str` *before* the first command that uses it; definitions last for the session. - **Slots** (`u32`, ≥ 1) identify event handlers; **callbacks** (`u32`, ≥ 1) identify effect continuations and subscriptions. Both are allocated by Lean and reused. ## 3. Commands `n` = node handle, `nm` = name id, `s` = string (2 words), `cb`/`sub` = callback id, `blob` = blob index (one word, section 8), `file` = file id (payload 5, section 4); other operands are plain numbers (`w`, `h`: a bitmap's size; `x`, `y`, `dw`, `dh`: a rectangle in it). | Op | Name | Operands | Browser semantics | |---|---|---|---| | 1 | CREATE_ELEMENT | n, ns, nm | `nodes[n] = createElement(nm)`; ns 1 = SVG, 2 = MathML (`createElementNS`) | | 2 | CREATE_TEXT | n, s | `nodes[n] = createTextNode(s)` | | 3 | SET_ATTR | n, nm, s | `setAttribute(nm, s)` | | 4 | REMOVE_ATTR | n, nm | `removeAttribute(nm)` | | 5 | SET_PROP_STR | n, nm, s | `el[nm] = s` (skipped when equal, so a focused input keeps its caret) | | 6 | SET_PROP_BOOL | n, nm, b | `el[nm] = (b == 1)` | | 7 | SET_PROP_NUM | n, nm, s | `el[nm] = Number(s)` | | 8 | REMOVE_PROP | n, nm | `el[nm] = false` if the property is boolean, else `""` | | 9 | SET_TEXT | n, s | `nodeValue = s` | | 10 | APPEND_CHILD | parent, child | `parent.appendChild(child)` (moves it if attached) | | 11 | INSERT_BEFORE | parent, child, ref | `parent.insertBefore(child, ref)`; `ref` is a child of `parent` | | 12 | REMOVE | n | detach `n` from its parent | | 13 | REPLACE | old, new | `old.replaceWith(new)` | | 14 | FREE | n | forget handle `n`; the node is detached or inside a detached subtree | | 15 | SET_LISTENER | n, nm, slot, payload, flags | `n` now reports event `nm` as `slot` (section 4) | | 16 | REMOVE_LISTENER | n, nm | `n` no longer reports `nm` | | 17 | INTERN | id, s | define name `id` | | 18 | MOUNT | s, n | `querySelector(s).replaceChildren(n)`; `s` becomes the app's container | | 19 | HYDRATE | s, first, count | adopt the container's existing nodes (section 5) | | 20 | TEMPLATE | tid, count, n1..ncount | keep a copy of the subtree rooted at `n1`, whose nodes are `n1..ncount` in pre-order, as template `tid` (section 7) | | 21 | CLONE | tid, count, n1..ncount | `nodes[n1..ncount]` = a copy of template `tid`, in pre-order (section 7) | | 32 | FETCH | cb, s method, s url, s body, s headers | `fetch`; headers are `name: value` lines; reply `cb` with ok = 2xx and payload `"\n"` (status 0 on network error) | | 33 | TIMEOUT | cb, ms | reply `cb` (ok, `""`) after `ms` | | 34 | INTERVAL_SET | sub, ms | reply `sub` (ok, `""`) every `ms` until cleared | | 35 | SUB_CLEAR | sub | stop subscription `sub` (interval, URL or keys) | | 36 | STORAGE_SET | s key, s value | `localStorage.setItem` | | 37 | STORAGE_GET | cb, s key | reply `cb` with ok = present, payload = value | | 38 | STORAGE_REMOVE | s key | `localStorage.removeItem` | | 39 | URL_PUSH | s url | `history.pushState` | | 40 | URL_REPLACE | s url | `history.replaceState` | | 41 | URL_LISTEN | sub | reply `sub` with the new path+query+hash after `popstate` and after a primary-button click on `a[data-link]` (which is then handled without a reload) | | 42 | FOCUS | s selector | after this buffer is applied, focus the first match | | 43 | LOG | s | `console.log` | | 44 | TITLE | s | `document.title = s` | | 45 | FRAME | cb | reply `cb` on the next animation frame, payload = rounded timestamp (ms) | | 46 | BLUR | s selector | after this buffer, blur the first match | | 47 | SCROLL_INTO_VIEW | s selector | after this buffer, scroll the first match into view | | 48 | PUT_IMAGE | s selector, blob, w, h, x, y, dw, dh | at the end of this buffer, `putImageData` blob (`w`×`h` RGBA pixels) at (0, 0) of the first `` matching `selector`, copying only the rectangle (`x`, `y`, `dw`, `dh`) (section 8) | | 49 | FILE_READ | cb, file | reply `cb` with the bytes of file `file` (section 8) | | 50 | IMAGE_DECODE | cb, file | reply `cb` with file `file` decoded to RGBA pixels, payload `" "` (section 8) | | 51 | DOWNLOAD | s name, s type, blob | offer blob's bytes as a download named `name` of media type `type` (section 8) | | 52 | KEYS_LISTEN | sub | reply `sub` for every `keydown` on the document except in text fields (`textarea`, `select`, editable content, and `input`s other than checkboxes, radio buttons, ranges, buttons, file and colour pickers), payload `Ctrl+`, `Alt+`, `Shift+`, `Meta+` prefixes (in that order) then `KeyboardEvent.key`; presses of a modifier key alone are not reported, and no default is prevented | | 53 | FRAME_GRAB | cb, s selector | reply `cb` with the frame the first `