My aggregated monorepo of OCaml code, automaintained

Clean up monorepo dependencies

- Remove bisect_ppx from odoc (opam dep + 11 instrumentation stanzas)
- Remove unix and findlib from js_top_worker-unix.opam (not opam packages)
- Remove bisect_ppx, findlib, unix from root dune-project
- Add mdx with :with-test guard
- Delete experiments/widget-bridge (stale, removed lwd/note deps)

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

+5 -943
+1 -3
dune-project
··· 13 13 astring 14 14 base64 15 15 bigstringaf 16 - bisect_ppx 17 16 bos 18 17 brr 19 18 camlp-streams ··· 29 28 dune-site 30 29 eio 31 30 eio_main 32 - findlib 33 31 fmt 34 32 fpath 35 33 js_of_ocaml ··· 39 37 js_of_ocaml-toplevel 40 38 logs 41 39 lwt 40 + (mdx :with-test) 42 41 menhir 43 42 merlin-lib 44 43 ocaml ··· 58 57 sexplib 59 58 sexplib0 60 59 tyxml 61 - unix 62 60 uri 63 61 yojson 64 62 ))
-332
experiments/widget-bridge/RESULTS.md
··· 1 - # Widget/FRP Bridge — Experiment Results 2 - 3 - *2026-02-24* 4 - 5 - Both experiments implement an identical counter widget (decrement, 6 - display, increment) with the same architecture: FRP-driven view 7 - derivation in a Web Worker, serialized via Marshal over postMessage, 8 - rendered to DOM by a shared Brr renderer on the main thread. 9 - 10 - ## Measurements 11 - 12 - | Metric | Lwd (Exp A) | Note (Exp B) | 13 - |--------------------------------|-------------|--------------| 14 - | Worker lines of OCaml | 42 | 49 | 15 - | Main lines of OCaml | 24 | 24 | 16 - | Worker JS bundle (uncompressed)| 2.2 MB | 2.7 MB | 17 - | Main JS bundle (uncompressed) | 2.7 MB | 2.7 MB | 18 - | Initial render latency | ~0.5 ms | ~0.6 ms | 19 - | Update render latency (median) | ~0.2 ms | ~0.3 ms | 20 - 21 - Shared code: `view.ml` (32 lines), `renderer.ml` (29 lines). 22 - 23 - Bundle sizes are dominated by js_of_ocaml runtime + stdlib (~2 MB 24 - baseline). The FRP library contribution is small: Lwd adds ~200 KB, 25 - Note adds ~700 KB over the base worker. 26 - 27 - Render latencies are effectively identical — both sub-millisecond for 28 - this trivial view tree. The bottleneck would be postMessage 29 - serialization and DOM replacement, not the FRP library. 30 - 31 - ## Ergonomics 32 - 33 - ### Lwd 34 - 35 - **State model:** Mutable variables (`Lwd.var`) with explicit 36 - get/set/peek. Feels like working with `ref` cells, but with automatic 37 - dependency tracking. 38 - 39 - ```ocaml 40 - let count = Lwd.var 0 41 - let view = 42 - Lwd.map (Lwd.get count) ~f:(fun n -> ... view tree ...) 43 - let root = Lwd.observe view 44 - 45 - (* Update: explicit set + explicit re-sample *) 46 - Lwd.set count (Lwd.peek count + 1); 47 - let v = Lwd.quick_sample root in 48 - post_message v 49 - ``` 50 - 51 - **Pros:** 52 - - Familiar imperative style — `set`/`peek` on vars 53 - - Pull-based: view is only computed when sampled (explicit control) 54 - - Smaller bundle size 55 - - Simple mental model for workers: update state, sample, send 56 - 57 - **Cons:** 58 - - Must manually trigger re-sampling after state changes 59 - - `Lwd.map` uses labeled `~f` argument (minor style preference) 60 - - The `observe`/`quick_sample` ceremony is slightly verbose 61 - - No built-in mechanism to react to changes — must wire it yourself 62 - 63 - ### Note 64 - 65 - **State model:** Events (`E`) and signals (`S`) with automatic 66 - propagation. Classic FRP: define the dataflow graph, then push events 67 - into it. 68 - 69 - ```ocaml 70 - let inc_e, send_inc = Note.E.create () 71 - let dec_e, send_dec = Note.E.create () 72 - let count = Note.S.accum 0 (Note.E.select [ 73 - Note.E.map (fun () n -> n + 1) inc_e; 74 - Note.E.map (fun () n -> n - 1) dec_e; 75 - ]) 76 - let view = Note.S.map (fun n -> ... view tree ...) count 77 - 78 - (* Observe: automatic push on signal change *) 79 - let logr = Note.S.log view send_to_main in 80 - Note.Logr.hold logr 81 - 82 - (* Update: just fire the event, propagation is automatic *) 83 - send_inc () 84 - ``` 85 - 86 - **Pros:** 87 - - Push-based: signal changes propagate automatically to the logger 88 - - No manual re-sampling — the `S.log` callback fires on every change 89 - - Cleaner separation between event sources and derived state 90 - - Better fit for the worker model: events come in, views go out 91 - automatically 92 - - `E.select` / `E.map` compose naturally for multiple event sources 93 - 94 - **Cons:** 95 - - More boilerplate for event source creation (one `E.create` per 96 - event type) 97 - - Must remember `Logr.hold` or loggers get GC'd (silent failure) 98 - - `S.accum` takes `('a -> 'a) event` — must map events to functions 99 - first, which is slightly indirect 100 - - 7 more lines of worker code than Lwd 101 - 102 - ## View Description (sample output) 103 - 104 - Both produce identical view trees. For count=3: 105 - 106 - ``` 107 - <div .counter> 108 - <button on:click->dec> 109 - "-" 110 - </button> 111 - <span style:margin="0 1em"> 112 - "3" 113 - </span> 114 - <button on:click->inc> 115 - "+" 116 - </button> 117 - </div> 118 - ``` 119 - 120 - Serialized via `Marshal.to_bytes` — binary OCaml marshalling, not 121 - JSON. For production use, a JSON or structured-clone-safe encoding 122 - would be needed. 123 - 124 - ## Recommendation 125 - 126 - **Note is the better fit for the worker bridge architecture.** 127 - 128 - The key differentiator is push-based propagation. In the worker 129 - model, the natural flow is: 130 - 131 - 1. Event arrives from main thread 132 - 2. State updates 133 - 3. New view is computed 134 - 4. View is sent to main thread 135 - 136 - With Note, steps 2–4 happen automatically via the signal graph and 137 - `S.log`. With Lwd, step 4 requires explicit re-sampling after every 138 - state change — easy to forget and a source of bugs as complexity 139 - grows. 140 - 141 - For a counter this doesn't matter much. But for Experiment C (multiple 142 - interacting sliders), Note's automatic propagation would compose 143 - better: add a new event source, wire it into the signal graph, and 144 - the view updates automatically. With Lwd, every new event handler 145 - would need to call `send_view()` explicitly. 146 - 147 - The bundle size difference (2.2 vs 2.7 MB) is negligible given that 148 - both are dominated by the js_of_ocaml baseline. The 7-line code 149 - difference is also insignificant. 150 - 151 - **Next steps if proceeding with Note:** 152 - - Experiment C: Multi-slider widget to validate composition ✓ 153 - - Experiment D: TyXML functor integration for type-safe HTML ✓ 154 - - Replace `Marshal` with structured-clone-safe encoding 155 - - Investigate `note.brr` for tighter Brr integration on the main 156 - thread side (reactive DOM updates without full re-render) 157 - 158 - --- 159 - 160 - ## Experiment C — Multi-Slider Widget 161 - 162 - A two-slider widget (X and Y, both range inputs 0–100) that displays 163 - the product X × Y in real time. Tests Note's signal composition with 164 - `S.hold` and `S.l2`, and validates that the renderer correctly 165 - extracts `value` from `<input>` elements. 166 - 167 - ### Measurements (Exp C) 168 - 169 - | Metric | Note Sliders (Exp C) | 170 - |--------------------------------|---------------------| 171 - | Worker lines of OCaml | 53 | 172 - | Main lines of OCaml | 24 | 173 - | Worker JS bundle (uncompressed)| 2.7 MB | 174 - | Main JS bundle (uncompressed) | 2.7 MB | 175 - | Render latency (median) | ~0.3 ms | 176 - 177 - Shared code: `view.ml` (32 lines), `renderer.ml` (38 lines — 9 lines 178 - added for input value extraction). 179 - 180 - ### Ergonomics 181 - 182 - **Signal composition works cleanly.** Each slider's value is a signal 183 - created with `S.hold`: 184 - 185 - ```ocaml 186 - let x_input, send_x = Note.E.create () 187 - let x = Note.S.hold 50 x_input 188 - ``` 189 - 190 - The combined view uses `S.l2` to lift a function over two signals: 191 - 192 - ```ocaml 193 - let view = Note.S.l2 (fun x y -> ... view tree ...) x y 194 - ``` 195 - 196 - Adding a third slider would mean `S.l3` — or, for more signals, 197 - `S.map` over `S.Pair`. The composition scales linearly. 198 - 199 - **Input value forwarding** required extending the renderer to extract 200 - `.value` from `<input>`, `<select>`, and `<textarea>` elements. The 201 - `event_msg.value` field (already `string option`) carries the value 202 - from the DOM to the worker, where it's parsed: 203 - 204 - ```ocaml 205 - match ev.handler_id, ev.value with 206 - | "x", Some v -> send_x (int_of_string v) 207 - | "y", Some v -> send_y (int_of_string v) 208 - ``` 209 - 210 - This is the minimum viable event payload — sufficient for sliders and 211 - text inputs. Richer widgets (checkboxes, multi-selects) would need a 212 - more structured event payload. 213 - 214 - **Key takeaway:** Note's push-based propagation composes well. Adding 215 - new event sources and signals requires no changes to the view-sending 216 - machinery — `S.log` handles it automatically, exactly as predicted 217 - after Experiments A/B. 218 - 219 - --- 220 - 221 - ## Experiment D — TyXML Functor Integration 222 - 223 - Uses TyXML's functor API (`Html_f.Make`) over a custom XML backend 224 - that produces `View.node` values, giving type-safe HTML construction 225 - while maintaining the same serializable output format. 226 - 227 - ### Measurements (Exp D) 228 - 229 - | Metric | Note Sliders (Exp C) | TyXML Sliders (Exp D) | 230 - |--------------------------------|---------------------|----------------------| 231 - | Worker lines of OCaml | 53 | 48 | 232 - | Main lines of OCaml | 24 | 24 | 233 - | Worker JS bundle (uncompressed)| 2.7 MB | 3.9 MB | 234 - | Main JS bundle (uncompressed) | 2.7 MB | 2.7 MB | 235 - | Render latency (median) | ~0.3 ms | ~0.3 ms | 236 - 237 - TyXML backend code: `view_xml.ml` (54 lines) + `view_html.ml` (5 238 - lines) = 59 lines of one-time setup. 239 - 240 - ### Bundle Size Impact 241 - 242 - TyXML adds **+1.2 MB** to the worker bundle (2.7 → 3.9 MB). This is 243 - the TyXML functor code and its generated HTML module. The main thread 244 - bundle is unchanged since TyXML is only used in the worker. 245 - 246 - For a worker that's already 2.7 MB (js_of_ocaml baseline + Note), 247 - this is a ~44% increase. Whether that's acceptable depends on the 248 - deployment context — for an educational tool where workers are loaded 249 - once and cached, it's likely fine. 250 - 251 - ### Ergonomics 252 - 253 - **The TyXML DSL is more concise.** Compare a slider row: 254 - 255 - Manual (Exp C, 8 lines): 256 - ```ocaml 257 - Element { tag = "div"; attrs = [Class "slider-row"]; children = [ 258 - Element { tag = "label"; attrs = []; children = [ 259 - Text (Printf.sprintf "%s: %d" label value) 260 - ] }; 261 - Element { tag = "input"; attrs = [ 262 - Property ("type", "range"); Property ("min", "0"); 263 - Property ("max", "100"); Property ("value", ...); 264 - Handler ("input", handler_id); 265 - ]; children = [] }; 266 - ] } 267 - ``` 268 - 269 - TyXML (Exp D, 5 lines): 270 - ```ocaml 271 - div ~a:[a_class ["slider-row"]] [ 272 - label [txt (Printf.sprintf "%s: %d" label_text value)]; 273 - input ~a:[ 274 - a_input_type `Range; a_value (string_of_int value); 275 - a_oninput handler_id; 276 - ] () 277 - ] |> toelt 278 - ``` 279 - 280 - **Type safety catches mistakes.** TyXML enforces valid HTML at compile 281 - time — you can't put a `<div>` inside a `<span>`, use an invalid 282 - attribute on an element, or forget required attributes. With manual 283 - `View.node` construction, any string goes. 284 - 285 - **The backend is straightforward.** `view_xml.ml` implements TyXML's 286 - `Xml_sigs.T` module type by mapping each operation to `View.node` / 287 - `View.attr` constructors. Event handler attributes strip the "on" 288 - prefix (e.g., `oninput` → `input`) to produce the event name expected 289 - by the renderer. The full backend is 59 lines of mechanical code that 290 - only needs to be written once. 291 - 292 - **One friction point:** TyXML's `toelt` is needed to convert typed 293 - HTML elements back to the underlying `View.node` type when mixing 294 - TyXML-constructed subtrees with manually-constructed parent nodes. 295 - This is a minor inconvenience, not a blocker. 296 - 297 - ### TyXML Recommendation 298 - 299 - **Use TyXML for widget authors, keep raw View.node for the runtime.** 300 - 301 - The type safety and conciseness gains are worth the +1.2 MB bundle 302 - cost for an educational tool. Widget authors writing in the worker 303 - benefit most from TyXML's compile-time HTML validation. The renderer 304 - and main thread code remain unchanged — they only see `View.node`. 305 - 306 - --- 307 - 308 - ## Overall Conclusions 309 - 310 - | | Exp A (Lwd) | Exp B (Note) | Exp C (Sliders) | Exp D (TyXML) | 311 - |---|---|---|---|---| 312 - | FRP lib | Lwd | Note | Note | Note | 313 - | View DSL | Manual | Manual | Manual | TyXML | 314 - | Worker LOC | 42 | 49 | 53 | 48 | 315 - | Worker bundle | 2.2 MB | 2.7 MB | 2.7 MB | 3.9 MB | 316 - 317 - **Architecture validated.** The worker bridge pattern — FRP-driven 318 - view derivation in a worker, serialized view trees, Brr rendering on 319 - the main thread — works well across all four experiments. Sub- 320 - millisecond render latencies, clean separation of concerns, and 321 - straightforward composition. 322 - 323 - **Recommended stack:** Note + TyXML for widget authoring, raw 324 - View.node + Brr for the runtime. 325 - 326 - **Remaining work:** 327 - - Replace `Marshal` with structured-clone-safe encoding (JSON or 328 - custom binary) for production safety 329 - - Investigate `note.brr` for incremental DOM updates (currently the 330 - entire DOM subtree is replaced on every view change) 331 - - Integrate with the toplevel worker to enable user-authored widgets 332 - - Add CSS styling support beyond inline styles
-2
experiments/widget-bridge/dune-project
··· 1 - (lang dune 3.21) 2 - (name widget-bridge-experiments)
-17
experiments/widget-bridge/html/lwd_counter.html
··· 1 - <!DOCTYPE html> 2 - <html> 3 - <head> 4 - <meta charset="utf-8"> 5 - <title>Experiment A: Lwd Counter</title> 6 - <style> 7 - body { font-family: system-ui, sans-serif; padding: 2rem; } 8 - .counter { display: flex; align-items: center; font-size: 1.5rem; } 9 - .counter button { font-size: 1.5rem; padding: 0.25em 0.75em; cursor: pointer; } 10 - </style> 11 - </head> 12 - <body> 13 - <h1>Experiment A: Lwd Counter</h1> 14 - <div id="app">Loading...</div> 15 - <script src="../lwd_counter/main.bc.js"></script> 16 - </body> 17 - </html>
-17
experiments/widget-bridge/html/note_counter.html
··· 1 - <!DOCTYPE html> 2 - <html> 3 - <head> 4 - <meta charset="utf-8"> 5 - <title>Experiment B: Note Counter</title> 6 - <style> 7 - body { font-family: system-ui, sans-serif; padding: 2rem; } 8 - .counter { display: flex; align-items: center; font-size: 1.5rem; } 9 - .counter button { font-size: 1.5rem; padding: 0.25em 0.75em; cursor: pointer; } 10 - </style> 11 - </head> 12 - <body> 13 - <h1>Experiment B: Note Counter</h1> 14 - <div id="app">Loading...</div> 15 - <script src="../note_counter/main.bc.js"></script> 16 - </body> 17 - </html>
-21
experiments/widget-bridge/html/sliders.html
··· 1 - <!DOCTYPE html> 2 - <html> 3 - <head> 4 - <meta charset="utf-8"> 5 - <title>Experiment C: Two Sliders</title> 6 - <style> 7 - body { font-family: system-ui, sans-serif; padding: 2rem; } 8 - .sliders { max-width: 400px; } 9 - .slider-row { margin-bottom: 1rem; } 10 - .slider-row label { display: block; margin-bottom: 0.25rem; font-size: 1.1rem; } 11 - .slider-row input[type="range"] { width: 100%; } 12 - .result { font-size: 1.5rem; margin-top: 1rem; padding: 0.5rem; 13 - background: #f0f0f0; border-radius: 4px; text-align: center; } 14 - </style> 15 - </head> 16 - <body> 17 - <h1>Experiment C: Two Sliders</h1> 18 - <div id="app">Loading...</div> 19 - <script src="../sliders/main.bc.js"></script> 20 - </body> 21 - </html>
-21
experiments/widget-bridge/html/tyxml_sliders.html
··· 1 - <!DOCTYPE html> 2 - <html> 3 - <head> 4 - <meta charset="utf-8"> 5 - <title>Experiment D: TyXML Sliders</title> 6 - <style> 7 - body { font-family: system-ui, sans-serif; padding: 2rem; } 8 - .sliders { max-width: 400px; } 9 - .slider-row { margin-bottom: 1rem; } 10 - .slider-row label { display: block; margin-bottom: 0.25rem; font-size: 1.1rem; } 11 - .slider-row input[type="range"] { width: 100%; } 12 - .result { font-size: 1.5rem; margin-top: 1rem; padding: 0.5rem; 13 - background: #f0f0f0; border-radius: 4px; text-align: center; } 14 - </style> 15 - </head> 16 - <body> 17 - <h1>Experiment D: TyXML Sliders</h1> 18 - <div id="app">Loading...</div> 19 - <script src="../tyxml_sliders/main.bc.js"></script> 20 - </body> 21 - </html>
-3
experiments/widget-bridge/lib/dune
··· 1 - (library 2 - (name view) 3 - (libraries))
-32
experiments/widget-bridge/lib/view.ml
··· 1 - type event_id = string 2 - 3 - type attr = 4 - | Property of string * string 5 - | Style of string * string 6 - | Class of string 7 - | Handler of string * event_id 8 - 9 - type node = 10 - | Text of string 11 - | Element of { tag : string; attrs : attr list; children : node list } 12 - 13 - type event_msg = { 14 - handler_id : event_id; 15 - event_type : string; 16 - value : string option; 17 - } 18 - 19 - let pp_attr fmt = function 20 - | Property (k, v) -> Format.fprintf fmt "%s=%S" k v 21 - | Style (k, v) -> Format.fprintf fmt "style:%s=%S" k v 22 - | Class c -> Format.fprintf fmt ".%s" c 23 - | Handler (ev, id) -> Format.fprintf fmt "on:%s->%s" ev id 24 - 25 - let rec pp_node fmt = function 26 - | Text s -> Format.fprintf fmt "%S" s 27 - | Element { tag; attrs; children } -> 28 - Format.fprintf fmt "@[<v 2><%s" tag; 29 - List.iter (fun a -> Format.fprintf fmt " %a" pp_attr a) attrs; 30 - Format.fprintf fmt ">"; 31 - List.iter (fun c -> Format.fprintf fmt "@,%a" pp_node c) children; 32 - Format.fprintf fmt "@]@,</%s>" tag
-25
experiments/widget-bridge/lib/view.mli
··· 1 - (** Serializable view descriptions for the widget bridge. 2 - 3 - No closures, no JS references. Event handlers are symbolic 4 - identifiers resolved by the worker. *) 5 - 6 - type event_id = string 7 - 8 - type attr = 9 - | Property of string * string 10 - | Style of string * string 11 - | Class of string 12 - | Handler of string * event_id (** event name, handler id *) 13 - 14 - type node = 15 - | Text of string 16 - | Element of { tag : string; attrs : attr list; children : node list } 17 - 18 - type event_msg = { 19 - handler_id : event_id; 20 - event_type : string; 21 - value : string option; 22 - } 23 - 24 - (** Pretty-print a node tree (for evaluation/documentation). *) 25 - val pp_node : Format.formatter -> node -> unit
-11
experiments/widget-bridge/lwd_counter/dune
··· 1 - (executable 2 - (name worker) 3 - (modules worker) 4 - (modes js) 5 - (libraries view lwd js_of_ocaml)) 6 - 7 - (executable 8 - (name main) 9 - (modules main) 10 - (modes js) 11 - (libraries view renderer brr))
-24
experiments/widget-bridge/lwd_counter/main.ml
··· 1 - module Worker = Brr_webworkers.Worker 2 - open Brr 3 - 4 - let () = 5 - let container = 6 - match Document.find_el_by_id G.document (Jstr.v "app") with 7 - | Some el -> el 8 - | None -> failwith "No #app element found" 9 - in 10 - let worker = Worker.create (Jstr.v "../lwd_counter/worker.bc.js") in 11 - let on_event (ev : View.event_msg) = 12 - Worker.post worker (Marshal.to_bytes ev []) 13 - in 14 - ignore @@ Ev.listen Brr_io.Message.Ev.message 15 - (fun msg -> 16 - let msg = Ev.as_type msg in 17 - let data : bytes = Brr_io.Message.Ev.data msg in 18 - let view : View.node = Marshal.from_bytes data 0 in 19 - let t0 = Performance.now_ms G.performance in 20 - let el = Renderer.render ~on_event view in 21 - El.set_children container [el]; 22 - let t1 = Performance.now_ms G.performance in 23 - Console.(log [str "Render:"; (t1 -. t0); str "ms"])) 24 - (Worker.as_target worker)
-42
experiments/widget-bridge/lwd_counter/worker.ml
··· 1 - open View 2 - 3 - (* --- State --- *) 4 - 5 - let count = Lwd.var 0 6 - 7 - (* --- View derivation --- *) 8 - 9 - let view = 10 - Lwd.map (Lwd.get count) ~f:(fun n -> 11 - Element { tag = "div"; attrs = [Class "counter"]; children = [ 12 - Element { tag = "button"; 13 - attrs = [Handler ("click", "dec")]; 14 - children = [Text "-"] }; 15 - Element { tag = "span"; 16 - attrs = [Style ("margin", "0 1em")]; 17 - children = [Text (string_of_int n)] }; 18 - Element { tag = "button"; 19 - attrs = [Handler ("click", "inc")]; 20 - children = [Text "+"] }; 21 - ] }) 22 - 23 - let root = Lwd.observe view 24 - 25 - (* --- Worker loop --- *) 26 - 27 - let send_view () = 28 - let v = Lwd.quick_sample root in 29 - Js_of_ocaml.Worker.post_message (Marshal.to_bytes (v : node) []) 30 - 31 - let handle_event (ev : event_msg) = 32 - (match ev.handler_id with 33 - | "inc" -> Lwd.set count (Lwd.peek count + 1) 34 - | "dec" -> Lwd.set count (Lwd.peek count - 1) 35 - | _ -> ()); 36 - send_view () 37 - 38 - let () = 39 - send_view (); 40 - Js_of_ocaml.Worker.set_onmessage (fun data -> 41 - let ev : event_msg = Marshal.from_bytes data 0 in 42 - handle_event ev)
-11
experiments/widget-bridge/note_counter/dune
··· 1 - (executable 2 - (name worker) 3 - (modules worker) 4 - (modes js) 5 - (libraries view note js_of_ocaml)) 6 - 7 - (executable 8 - (name main) 9 - (modules main) 10 - (modes js) 11 - (libraries view renderer brr))
-24
experiments/widget-bridge/note_counter/main.ml
··· 1 - module Worker = Brr_webworkers.Worker 2 - open Brr 3 - 4 - let () = 5 - let container = 6 - match Document.find_el_by_id G.document (Jstr.v "app") with 7 - | Some el -> el 8 - | None -> failwith "No #app element found" 9 - in 10 - let worker = Worker.create (Jstr.v "../note_counter/worker.bc.js") in 11 - let on_event (ev : View.event_msg) = 12 - Worker.post worker (Marshal.to_bytes ev []) 13 - in 14 - ignore @@ Ev.listen Brr_io.Message.Ev.message 15 - (fun msg -> 16 - let msg = Ev.as_type msg in 17 - let data : bytes = Brr_io.Message.Ev.data msg in 18 - let view : View.node = Marshal.from_bytes data 0 in 19 - let t0 = Performance.now_ms G.performance in 20 - let el = Renderer.render ~on_event view in 21 - El.set_children container [el]; 22 - let t1 = Performance.now_ms G.performance in 23 - Console.(log [str "Render:"; (t1 -. t0); str "ms"])) 24 - (Worker.as_target worker)
-49
experiments/widget-bridge/note_counter/worker.ml
··· 1 - open View 2 - 3 - (* --- Event sources --- *) 4 - 5 - let inc_e, send_inc = Note.E.create () 6 - let dec_e, send_dec = Note.E.create () 7 - 8 - (* --- Reactive state --- *) 9 - 10 - let count = 11 - let delta = Note.E.select [ 12 - Note.E.map (fun () n -> n + 1) inc_e; 13 - Note.E.map (fun () n -> n - 1) dec_e; 14 - ] in 15 - Note.S.accum 0 delta 16 - 17 - (* --- View derivation --- *) 18 - 19 - let view = 20 - Note.S.map (fun n -> 21 - Element { tag = "div"; attrs = [Class "counter"]; children = [ 22 - Element { tag = "button"; 23 - attrs = [Handler ("click", "dec")]; 24 - children = [Text "-"] }; 25 - Element { tag = "span"; 26 - attrs = [Style ("margin", "0 1em")]; 27 - children = [Text (string_of_int n)] }; 28 - Element { tag = "button"; 29 - attrs = [Handler ("click", "inc")]; 30 - children = [Text "+"] }; 31 - ] }) 32 - count 33 - 34 - (* --- Worker loop --- *) 35 - 36 - let send_view (v : node) = 37 - Js_of_ocaml.Worker.post_message (Marshal.to_bytes v []) 38 - 39 - let () = 40 - (* Observe view signal and send updates *) 41 - let logr = Note.S.log view send_view in 42 - Note.Logr.hold logr; 43 - (* Handle incoming events *) 44 - Js_of_ocaml.Worker.set_onmessage (fun data -> 45 - let ev : event_msg = Marshal.from_bytes data 0 in 46 - match ev.handler_id with 47 - | "inc" -> send_inc () 48 - | "dec" -> send_dec () 49 - | _ -> ())
-3
experiments/widget-bridge/renderer/dune
··· 1 - (library 2 - (name renderer) 3 - (libraries view brr))
-38
experiments/widget-bridge/renderer/renderer.ml
··· 1 - open Brr 2 - 3 - let rec render ~on_event (node : View.node) : El.t = 4 - match node with 5 - | Text s -> 6 - El.txt (Jstr.v s) 7 - | Element { tag; attrs; children } -> 8 - let brr_attrs = List.filter_map (fun (a : View.attr) -> 9 - match a with 10 - | Property (k, v) -> Some (At.v (Jstr.v k) (Jstr.v v)) 11 - | Style (k, v) -> Some (At.style (Jstr.v (k ^ ":" ^ v))) 12 - | Class c -> Some (At.class' (Jstr.v c)) 13 - | Handler _ -> None 14 - ) attrs in 15 - let el = 16 - El.v (Jstr.v tag) ~at:brr_attrs 17 - (List.map (render ~on_event) children) 18 - in 19 - List.iter (fun (a : View.attr) -> 20 - match a with 21 - | Handler (event_name, handler_id) -> 22 - let is_input = match tag with 23 - | "input" | "select" | "textarea" -> true 24 - | _ -> false 25 - in 26 - ignore @@ Ev.listen 27 - (Ev.Type.void (Jstr.v event_name)) 28 - (fun _ev -> 29 - let value = 30 - if is_input then 31 - Some (Jstr.to_string (Jv.Jstr.get (El.to_jv el) "value")) 32 - else None 33 - in 34 - on_event { View.handler_id; event_type = event_name; value }) 35 - (El.as_target el) 36 - | _ -> () 37 - ) attrs; 38 - el
-6
experiments/widget-bridge/renderer/renderer.mli
··· 1 - (** Render a {!View.node} tree to DOM and wire event handlers. 2 - 3 - Each call to {!render} produces a fresh DOM subtree. The caller is 4 - responsible for replacing the container's children. *) 5 - 6 - val render : on_event:(View.event_msg -> unit) -> View.node -> Brr.El.t
-11
experiments/widget-bridge/sliders/dune
··· 1 - (executable 2 - (name worker) 3 - (modules worker) 4 - (modes js) 5 - (libraries view note js_of_ocaml)) 6 - 7 - (executable 8 - (name main) 9 - (modules main) 10 - (modes js) 11 - (libraries view renderer brr))
-24
experiments/widget-bridge/sliders/main.ml
··· 1 - module Worker = Brr_webworkers.Worker 2 - open Brr 3 - 4 - let () = 5 - let container = 6 - match Document.find_el_by_id G.document (Jstr.v "app") with 7 - | Some el -> el 8 - | None -> failwith "No #app element found" 9 - in 10 - let worker = Worker.create (Jstr.v "../sliders/worker.bc.js") in 11 - let on_event (ev : View.event_msg) = 12 - Worker.post worker (Marshal.to_bytes ev []) 13 - in 14 - ignore @@ Ev.listen Brr_io.Message.Ev.message 15 - (fun msg -> 16 - let msg = Ev.as_type msg in 17 - let data : bytes = Brr_io.Message.Ev.data msg in 18 - let view : View.node = Marshal.from_bytes data 0 in 19 - let t0 = Performance.now_ms G.performance in 20 - let el = Renderer.render ~on_event view in 21 - El.set_children container [el]; 22 - let t1 = Performance.now_ms G.performance in 23 - Console.(log [str "Render:"; (t1 -. t0); str "ms"])) 24 - (Worker.as_target worker)
-53
experiments/widget-bridge/sliders/worker.ml
··· 1 - open View 2 - 3 - (* --- Event sources --- *) 4 - 5 - let x_input, send_x = Note.E.create () 6 - let y_input, send_y = Note.E.create () 7 - 8 - (* --- Reactive state --- *) 9 - 10 - let x = Note.S.hold 50 x_input 11 - let y = Note.S.hold 50 y_input 12 - 13 - (* --- View derivation --- *) 14 - 15 - let slider ~label ~handler_id value = 16 - Element { tag = "div"; attrs = [Class "slider-row"]; children = [ 17 - Element { tag = "label"; attrs = []; children = [ 18 - Text (Printf.sprintf "%s: %d" label value) 19 - ] }; 20 - Element { tag = "input"; attrs = [ 21 - Property ("type", "range"); 22 - Property ("min", "0"); 23 - Property ("max", "100"); 24 - Property ("value", string_of_int value); 25 - Handler ("input", handler_id); 26 - ]; children = [] }; 27 - ] } 28 - 29 - let view = 30 - Note.S.l2 (fun x y -> 31 - Element { tag = "div"; attrs = [Class "sliders"]; children = [ 32 - slider ~label:"X" ~handler_id:"x" x; 33 - slider ~label:"Y" ~handler_id:"y" y; 34 - Element { tag = "div"; attrs = [Class "result"]; children = [ 35 - Text (Printf.sprintf "X × Y = %d" (x * y)) 36 - ] }; 37 - ] }) 38 - x y 39 - 40 - (* --- Worker loop --- *) 41 - 42 - let send_view (v : node) = 43 - Js_of_ocaml.Worker.post_message (Marshal.to_bytes v []) 44 - 45 - let () = 46 - let logr = Note.S.log view send_view in 47 - Note.Logr.hold logr; 48 - Js_of_ocaml.Worker.set_onmessage (fun data -> 49 - let ev : event_msg = Marshal.from_bytes data 0 in 50 - match ev.handler_id, ev.value with 51 - | "x", Some v -> (try send_x (int_of_string v) with _ -> ()) 52 - | "y", Some v -> (try send_y (int_of_string v) with _ -> ()) 53 - | _ -> ())
-3
experiments/widget-bridge/tyxml_backend/dune
··· 1 - (library 2 - (name view_html) 3 - (libraries view tyxml.functor))
-5
experiments/widget-bridge/tyxml_backend/view_html.ml
··· 1 - module Xml = View_xml 2 - module Svg = Svg_f.Make(View_xml) 3 - module Html = Html_f.Make(View_xml)(Svg) 4 - 5 - include (Html : module type of Html with module Xml := Html.Xml)
-54
experiments/widget-bridge/tyxml_backend/view_xml.ml
··· 1 - module W = Xml_wrap.NoWrap 2 - 3 - type 'a wrap = 'a 4 - type 'a list_wrap = 'a list 5 - 6 - type uri = string 7 - let uri_of_string s = s 8 - let string_of_uri s = s 9 - 10 - type aname = string 11 - type event_handler = string 12 - type mouse_event_handler = string 13 - type keyboard_event_handler = string 14 - type touch_event_handler = string 15 - 16 - type attrib = View.attr 17 - 18 - let float_attrib name value = View.Property (name, string_of_float value) 19 - let int_attrib name value = View.Property (name, string_of_int value) 20 - let string_attrib name value = View.Property (name, value) 21 - let space_sep_attrib name values = View.Property (name, String.concat " " values) 22 - let comma_sep_attrib name values = View.Property (name, String.concat "," values) 23 - 24 - let event_name_of_aname aname = 25 - if String.length aname > 2 && String.sub aname 0 2 = "on" 26 - then String.sub aname 2 (String.length aname - 2) 27 - else aname 28 - 29 - let event_handler_attrib aname handler_id = 30 - View.Handler (event_name_of_aname aname, handler_id) 31 - let mouse_event_handler_attrib = event_handler_attrib 32 - let keyboard_event_handler_attrib = event_handler_attrib 33 - let touch_event_handler_attrib = event_handler_attrib 34 - let uri_attrib name value = View.Property (name, value) 35 - let uris_attrib name values = View.Property (name, String.concat " " values) 36 - 37 - type elt = View.node 38 - type ename = string 39 - 40 - let empty () = View.Text "" 41 - let comment _s = View.Text "" 42 - let pcdata s = View.Text s 43 - let encodedpcdata s = View.Text s 44 - let entity e = View.Text ("&" ^ e ^ ";") 45 - 46 - let leaf ?(a = []) name = 47 - View.Element { tag = name; attrs = a; children = [] } 48 - 49 - let node ?(a = []) name children = 50 - View.Element { tag = name; attrs = a; children } 51 - 52 - let cdata s = View.Text s 53 - let cdata_script s = View.Text s 54 - let cdata_style s = View.Text s
-11
experiments/widget-bridge/tyxml_sliders/dune
··· 1 - (executable 2 - (name worker) 3 - (modules worker) 4 - (modes js) 5 - (libraries view view_html note js_of_ocaml)) 6 - 7 - (executable 8 - (name main) 9 - (modules main) 10 - (modes js) 11 - (libraries view renderer brr))
-24
experiments/widget-bridge/tyxml_sliders/main.ml
··· 1 - module Worker = Brr_webworkers.Worker 2 - open Brr 3 - 4 - let () = 5 - let container = 6 - match Document.find_el_by_id G.document (Jstr.v "app") with 7 - | Some el -> el 8 - | None -> failwith "No #app element found" 9 - in 10 - let worker = Worker.create (Jstr.v "../tyxml_sliders/worker.bc.js") in 11 - let on_event (ev : View.event_msg) = 12 - Worker.post worker (Marshal.to_bytes ev []) 13 - in 14 - ignore @@ Ev.listen Brr_io.Message.Ev.message 15 - (fun msg -> 16 - let msg = Ev.as_type msg in 17 - let data : bytes = Brr_io.Message.Ev.data msg in 18 - let view : View.node = Marshal.from_bytes data 0 in 19 - let t0 = Performance.now_ms G.performance in 20 - let el = Renderer.render ~on_event view in 21 - El.set_children container [el]; 22 - let t1 = Performance.now_ms G.performance in 23 - Console.(log [str "Render:"; (t1 -. t0); str "ms"])) 24 - (Worker.as_target worker)
-48
experiments/widget-bridge/tyxml_sliders/worker.ml
··· 1 - open View 2 - open View_html 3 - 4 - (* --- Event sources (same as Experiment C) --- *) 5 - 6 - let x_input, send_x = Note.E.create () 7 - let y_input, send_y = Note.E.create () 8 - 9 - let x = Note.S.hold 50 x_input 10 - let y = Note.S.hold 50 y_input 11 - 12 - (* --- View derivation with TyXML DSL --- *) 13 - 14 - let slider_row ~label_text ~handler_id value = 15 - div ~a:[a_class ["slider-row"]] [ 16 - label [txt (Printf.sprintf "%s: %d" label_text value)]; 17 - input ~a:[ 18 - a_input_type `Range; 19 - a_value (string_of_int value); 20 - a_oninput handler_id; 21 - ] () 22 - ] |> toelt 23 - 24 - let view = 25 - Note.S.l2 (fun x y -> 26 - Element { tag = "div"; attrs = [Class "sliders"]; children = [ 27 - slider_row ~label_text:"X" ~handler_id:"x" x; 28 - slider_row ~label_text:"Y" ~handler_id:"y" y; 29 - toelt (div ~a:[a_class ["result"]] [ 30 - txt (Printf.sprintf "X * Y = %d" (x * y)) 31 - ]); 32 - ] }) 33 - x y 34 - 35 - (* --- Worker loop (identical to Experiment C) --- *) 36 - 37 - let send_view (v : node) = 38 - Js_of_ocaml.Worker.post_message (Marshal.to_bytes v []) 39 - 40 - let () = 41 - let logr = Note.S.log view send_view in 42 - Note.Logr.hold logr; 43 - Js_of_ocaml.Worker.set_onmessage (fun data -> 44 - let ev : event_msg = Marshal.from_bytes data 0 in 45 - match ev.handler_id, ev.value with 46 - | "x", Some v -> (try send_x (int_of_string v) with _ -> ()) 47 - | "y", Some v -> (try send_y (int_of_string v) with _ -> ()) 48 - | _ -> ())
-2
js_top_worker/js_top_worker-unix.opam
··· 12 12 "logs" 13 13 "fmt" 14 14 "lwt" 15 - "findlib" 16 - "unix" 17 15 ] 18 16 build : [ 19 17 ["dune" "subst"] {pinned}
-1
odoc/odoc.opam
··· 57 57 "conf-jq" {with-test} 58 58 "ppx_expect" {with-test} 59 59 "bos" {with-test} 60 - "bisect_ppx" {with-test & > "2.5.0"} 61 60 ] 62 61 63 62 x-dune-sites: [["lib" "extensions"]]
-2
odoc/src/document/dune
··· 11 11 (name odoc_document) 12 12 (public_name odoc.document) 13 13 (instrumentation 14 - (backend bisect_ppx)) 15 - (instrumentation 16 14 (backend landmarks --auto)) 17 15 (libraries 18 16 odoc_model
-2
odoc/src/html/dune
··· 3 3 (public_name odoc.html) 4 4 (instrumentation 5 5 (backend landmarks --auto)) 6 - (instrumentation 7 - (backend bisect_ppx)) 8 6 (libraries odoc_model odoc_document tyxml base64))
-2
odoc/src/html_support_files/dune
··· 35 35 (public_name odoc.html_support_files) 36 36 (instrumentation 37 37 (backend landmarks --auto)) 38 - (instrumentation 39 - (backend bisect_ppx)) 40 38 (wrapped false)) 41 39 42 40 (install
-2
odoc/src/latex/dune
··· 3 3 (public_name odoc.latex) 4 4 (instrumentation 5 5 (backend landmarks --auto)) 6 - (instrumentation 7 - (backend bisect_ppx)) 8 6 (libraries odoc_model odoc_document fmt fpath odoc_utils))
-2
odoc/src/manpage/dune
··· 3 3 (public_name odoc.manpage) 4 4 (instrumentation 5 5 (backend landmarks --auto)) 6 - (instrumentation 7 - (backend bisect_ppx)) 8 6 (libraries odoc_model odoc_document))
-2
odoc/src/model/dune
··· 26 26 (:standard -w -50)) 27 27 (instrumentation 28 28 (backend landmarks --auto)) 29 - (instrumentation 30 - (backend bisect_ppx)) 31 29 (libraries compiler-libs.common odoc-parser odoc_utils))
+1 -2
odoc/src/model_desc/dune
··· 4 4 (libraries odoc_model) 5 5 (instrumentation 6 6 (backend landmarks --auto)) 7 - (instrumentation 8 - (backend bisect_ppx))) 7 + )
+1 -2
odoc/src/odoc/bin/dune
··· 9 9 (link_flags (-linkall)) 10 10 (instrumentation 11 11 (backend landmarks --auto)) 12 - (instrumentation 13 - (backend bisect_ppx))) 12 + ) 14 13 15 14 (generate_sites_module 16 15 (module sites)
+1 -2
odoc/src/odoc/dune
··· 19 19 unix) 20 20 (instrumentation 21 21 (backend landmarks --auto)) 22 - (instrumentation 23 - (backend bisect_ppx))) 22 + ) 24 23 25 24 (rule 26 25 (targets classify.ml)
-2
odoc/src/parser/dune
··· 5 5 (public_name odoc-parser) 6 6 (instrumentation 7 7 (backend landmarks --auto)) 8 - (instrumentation 9 - (backend bisect_ppx)) 10 8 (flags 11 9 (:standard -w -50)) 12 10 (libraries astring camlp-streams))
-2
odoc/src/xref2/dune
··· 3 3 (public_name odoc.xref2) 4 4 (instrumentation 5 5 (backend landmarks --auto)) 6 - (instrumentation 7 - (backend bisect_ppx)) 8 6 (libraries odoc_model odoc_utils)) 9 7 10 8 (rule
+1 -4
root.opam
··· 30 30 "js_of_ocaml-ppx" 31 31 "js_of_ocaml-toplevel" 32 32 "logs" 33 - "lwd" 34 33 "lwt" 35 - "mdx" 34 + "mdx" {with-test} 36 35 "menhir" 37 36 "merlin-lib" 38 - "note" 39 37 "ocaml" 40 38 "ocamlfind" 41 39 "ocamlformat-lib" ··· 43 41 "opam-0install" 44 42 "opam-format" 45 43 "ppx_blob" 46 - "ppx_deriving" 47 44 "ppx_deriving_yojson" 48 45 "ppx_expect" 49 46 "ppx_sexp_conv"