My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add design doc for widget/FRP bridge experiments

Captures decisions from brainstorming: standalone project structure,
OCaml renderer with Brr, scope limited to tasks 4.1-4.3 (Lwd vs Note
counter comparison).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+174
+174
docs/plans/2026-02-24-widget-frp-experiments-design.md
··· 1 + # Widget/FRP Bridge Experiments — Design 2 + 3 + *2026-02-24* 4 + 5 + ## Goal 6 + 7 + Evaluate **Lwd** vs **Note** as the reactivity library for driving 8 + interactive widgets from an OCaml Web Worker. Produce three 9 + deliverables: shared scaffolding with serializable view types, a Lwd 10 + counter, and a Note counter — enough to compare ergonomics, 11 + serialization, and latency. 12 + 13 + ## Decisions 14 + 15 + - **Scope**: Tasks 4.1–4.3 from the tutorials plan (scaffolding, Lwd 16 + counter, Note counter). Tasks 4.4–4.5 deferred. 17 + - **Project structure**: Standalone dune project in 18 + `experiments/widget-bridge/`, independent of the monorepo build. 19 + Integrate the winner later. 20 + - **Renderer language**: OCaml compiled with js_of_ocaml, using Brr 21 + for DOM APIs. 22 + - **Serialization**: JSON via `postMessage`. No CBOR, no custom binary 23 + format. 24 + 25 + ## Architecture 26 + 27 + ``` 28 + Worker (OCaml) Main Thread (OCaml/Brr) 29 + ┌──────────────────┐ ┌──────────────────────┐ 30 + │ FRP library │ view │ │ 31 + │ (Lwd or Note) │ JSON │ Renderer: │ 32 + │ ↓ │ ───────> │ decode view JSON │ 33 + │ Serializable │ │ reconcile DOM │ 34 + │ view description │ event │ wire event handlers │ 35 + │ ↓ │ JSON │ │ 36 + │ JSON encode │ <─────── │ User interaction │ 37 + └──────────────────┘ └──────────────────────┘ 38 + ``` 39 + 40 + Communication uses `postMessage` with simple JSON objects. 41 + 42 + ## Directory Layout 43 + 44 + ``` 45 + experiments/widget-bridge/ 46 + ├── dune-project 47 + ├── lib/ 48 + │ ├── view.ml(i) # Serializable view description type 49 + │ ├── view_json.ml(i) # JSON encoding/decoding 50 + │ └── event.ml(i) # Event message type 51 + ├── renderer/ 52 + │ └── renderer.ml(i) # Shared DOM renderer (Brr) 53 + ├── lwd_counter/ 54 + │ ├── worker.ml # Lwd counter, compiled to JS worker 55 + │ └── main.ml # Main-thread entry, compiled to JS 56 + ├── note_counter/ 57 + │ ├── worker.ml # Note counter, compiled to JS worker 58 + │ └── main.ml # Main-thread entry, compiled to JS 59 + ├── html/ 60 + │ ├── lwd_counter.html 61 + │ └── note_counter.html 62 + └── RESULTS.md # Comparison notes 63 + ``` 64 + 65 + ## Serializable View Type 66 + 67 + ```ocaml 68 + type event_id = string 69 + 70 + type attr = 71 + | Property of string * string 72 + | Style of string * string 73 + | Class of string 74 + | Handler of string * event_id (* event name, handler id *) 75 + 76 + type node = 77 + | Text of string 78 + | Element of { tag : string; attrs : attr list; children : node list } 79 + ``` 80 + 81 + No closures, no JS references. Event handlers are symbolic identifiers 82 + resolved by the worker. 83 + 84 + ## Event Message Type 85 + 86 + ```ocaml 87 + type event_msg = { 88 + handler_id : event_id; 89 + event_type : string; 90 + value : string option; (* for input elements *) 91 + } 92 + ``` 93 + 94 + ## Worker ↔ Main Thread Protocol 95 + 96 + Worker → main: 97 + ```json 98 + { "tag": "render", "view": { ... } } 99 + ``` 100 + 101 + Main → worker: 102 + ```json 103 + { "tag": "event", "handler_id": "inc", "event_type": "click", "value": null } 104 + ``` 105 + 106 + ## Renderer 107 + 108 + A shared OCaml module (compiled for the main thread with Brr) that: 109 + 110 + 1. Receives view JSON from the worker, decodes to `node` 111 + 2. Replaces the root DOM element (naive reconciliation — sufficient 112 + for experiments, optimize later if needed) 113 + 3. Attaches DOM event listeners for `Handler` attrs that post event 114 + messages back to the worker 115 + 116 + ## Experiment A — Lwd Counter 117 + 118 + ```ocaml 119 + let count = Lwd.var 0 120 + 121 + let view = 122 + Lwd.map (fun n -> 123 + Element { tag = "div"; attrs = []; children = [ 124 + Element { tag = "button"; 125 + attrs = [Handler ("click", "inc")]; 126 + children = [Text "+"] }; 127 + Element { tag = "span"; attrs = []; children = [ 128 + Text (string_of_int n) 129 + ] }; 130 + ] }) 131 + (Lwd.get count) 132 + ``` 133 + 134 + On receiving an "inc" event: `Lwd.set count (Lwd.peek count + 1)`, 135 + then re-evaluate and send the new view. 136 + 137 + ## Experiment B — Note Counter 138 + 139 + ```ocaml 140 + let inc_e, send_inc = Note.E.create () 141 + let count = Note.S.accum 0 (Note.E.map (fun () n -> n + 1) inc_e) 142 + 143 + let view = 144 + Note.S.map (fun n -> 145 + Element { tag = "div"; attrs = []; children = [ 146 + Element { tag = "button"; 147 + attrs = [Handler ("click", "inc")]; 148 + children = [Text "+"] }; 149 + Element { tag = "span"; attrs = []; children = [ 150 + Text (string_of_int n) 151 + ] }; 152 + ] }) 153 + count 154 + ``` 155 + 156 + On receiving an "inc" event: `send_inc ()`. The signal network 157 + propagates automatically. 158 + 159 + ## Evaluation Criteria 160 + 161 + For each experiment, record in RESULTS.md: 162 + 163 + | Criterion | How measured | 164 + |-----------|-------------| 165 + | Lines of OCaml (worker) | `wc -l` | 166 + | Ergonomics | Subjective notes on readability, composability | 167 + | Serialization | Sample JSON output for the counter view | 168 + | Round-trip latency | `Performance.now()` around click → DOM update | 169 + | Bundle size | `ls -lh` on compiled worker.js | 170 + 171 + ## References 172 + 173 + - [Tutorials design](2026-02-20-ocaml-interactive-tutorials-design.md) — Section 5 174 + - [Tutorials plan](2026-02-20-ocaml-interactive-tutorials-plan.md) — Stream 4