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 protocol integration with js_top_worker

Defines how the widget/FRP bridge integrates into the existing JSON
message protocol: WidgetUpdate/WidgetEvent/WidgetClear message types,
explicit widget IDs, handler maps, and cross-cell signal propagation.

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

+203
+203
docs/plans/2026-02-24-widget-protocol-integration-design.md
··· 1 + # Widget Protocol Integration Design 2 + 3 + **Goal:** Integrate the widget/FRP bridge from the experiments into 4 + js_top_worker's existing JSON message protocol, enabling interactive 5 + widgets in OCaml toplevel output. 6 + 7 + **Context:** Experiments A–D (on the `widget-frp` branch) validated 8 + that Note + TyXML + serializable View.node trees work well for 9 + worker-side widget authoring. This design integrates that into the 10 + real toplevel. 11 + 12 + ## Architecture 13 + 14 + User OCaml code running in the toplevel creates Note signals and 15 + calls `Widget.display` to register a live widget. The Widget module 16 + attaches a `Note.S.log` to the view signal. Whenever the signal 17 + changes, the logger serializes the View.node tree to JSON and sends 18 + a `WidgetUpdate` message through the existing worker→client message 19 + protocol. User interactions in the frontend produce `WidgetEvent` 20 + messages that flow back to the worker, where the Widget module 21 + routes them to the registered handler callbacks. Those callbacks 22 + fire Note events, which propagate through the signal graph, which 23 + triggers view updates for all affected widgets automatically. 24 + 25 + ``` 26 + ┌─────────────────── Worker ───────────────────┐ 27 + │ │ 28 + │ User code: │ 29 + │ let e, send = Note.E.create () │ 30 + │ let s = Note.S.hold 50 e │ 31 + │ Widget.display ~id:"my-widget" │ 32 + │ ~handlers:["x", fun v -> send ...] │ 33 + │ (Note.S.map view_fn s) │ 34 + │ │ 35 + │ Widget module: │ 36 + │ S.log → serialize View.node → send msg │ 37 + │ receive WidgetEvent → lookup handler → call │ 38 + │ │ 39 + └──────────── JSON over postMessage ────────────┘ 40 + 41 + ┌─────────────── Frontend ─────────────────────┐ 42 + │ │ 43 + │ Receive WidgetUpdate → render View.node DOM │ 44 + │ User interaction → send WidgetEvent back │ 45 + │ │ 46 + └───────────────────────────────────────────────┘ 47 + ``` 48 + 49 + ## Key Design Decisions 50 + 51 + **Explicit widget IDs.** User code names each widget with a string 52 + ID: `Widget.display ~id:"my-slider" ...`. The frontend uses this ID 53 + to identify where to render the widget and where to route events. 54 + No auto-generated IDs — the user controls naming. 55 + 56 + **Handler map co-located with display.** `Widget.display` takes both 57 + a view signal and a handler map: 58 + `~handlers:["x", (fun v -> send_x ...)]`. This keeps the view 59 + definition and its event routing together. 60 + 61 + **Signals span cells, widgets render per-ID.** Note signals are 62 + OCaml values that persist in the toplevel environment across evals. 63 + Cell 1 can define a signal, cell 2 can derive from it and display 64 + a widget. When cell 1's widget fires an event, cell 2's widget 65 + updates automatically via the signal graph. Widget rendering is 66 + identified by widget_id, not cell_id. 67 + 68 + **Generic protocol.** The protocol carries View.node trees and 69 + string events. No widget-type-specific messages. A slider, a 70 + button, a data table — all just different View.node trees. 71 + Convenience functions (e.g., `Widget.slider`) are built on top 72 + in user-space, not in the protocol. 73 + 74 + **JSON serialization.** View.node trees are encoded as JSON using 75 + the same `Js.Unsafe` / `JSON.stringify` pattern as the existing 76 + protocol, for cross-jsoo-version compatibility. 77 + 78 + ## Protocol Additions 79 + 80 + ### New message types in message.ml 81 + 82 + **Worker → Client:** 83 + 84 + ``` 85 + WidgetUpdate { widget_id: string; view: <View.node as JSON> } 86 + WidgetClear { widget_id: string } 87 + ``` 88 + 89 + **Client → Worker:** 90 + 91 + ``` 92 + WidgetEvent { widget_id: string; handler_id: string; 93 + event_type: string; value: string option } 94 + ``` 95 + 96 + ### View.node JSON encoding 97 + 98 + ```json 99 + {"t":"el","tag":"div","a":[{"t":"cls","v":"counter"}], 100 + "c":[{"t":"txt","v":"hello"}]} 101 + ``` 102 + 103 + Attribute variants: 104 + - `{"t":"prop","k":"type","v":"range"}` 105 + - `{"t":"style","k":"margin","v":"0 1em"}` 106 + - `{"t":"cls","v":"active"}` 107 + - `{"t":"handler","ev":"input","id":"x"}` 108 + 109 + Short keys (`t`, `k`, `v`, `c`, `a`, `ev`, `id`) to minimize 110 + payload size since these are sent on every signal change. 111 + 112 + ## Components 113 + 114 + ### 1. View types (shared library) 115 + 116 + Move `View.node`, `View.attr`, `View.event_msg` from 117 + `experiments/widget-bridge/lib/` into a standalone opam package 118 + (like `mime_printer`). Zero dependencies. Used by both the Widget 119 + module (worker-side) and the message protocol. 120 + 121 + ### 2. JSON encoding (in message.ml) 122 + 123 + Add `json_of_view_node`, `view_node_of_json`, `json_of_view_attr` 124 + functions to `message.ml` alongside the existing JSON helpers. 125 + These use the same `Js.Unsafe` / `json_of_obj` pattern. 126 + 127 + ### 3. Widget module (toplevel-loadable library) 128 + 129 + Similar to `mime_printer` — a small library with no heavy 130 + dependencies that gets loaded into the toplevel. Provides: 131 + 132 + ```ocaml 133 + module Widget : sig 134 + val display : 135 + id:string -> 136 + handlers:(string * (string option -> unit)) list -> 137 + View.node Note.S.t -> 138 + unit 139 + 140 + val clear : id:string -> unit 141 + end 142 + ``` 143 + 144 + `display` attaches a `Note.S.log` to the view signal. The logger 145 + calls a `send` function (injected at init time by the worker) to 146 + emit WidgetUpdate messages. It also registers the handler map so 147 + incoming WidgetEvent messages can be routed. 148 + 149 + `clear` detaches the logger and sends a WidgetClear message. 150 + 151 + **Dependency consideration:** This module depends on `note` and 152 + `widget-view`. It does NOT depend on js_of_ocaml or brr — it 153 + only calls a `send` callback injected by the worker. This keeps 154 + it portable and avoids polluting the toplevel namespace. 155 + 156 + ### 4. Worker integration (worker.ml) 157 + 158 + - Handle `WidgetEvent` in `handle_message`: look up widget_id 159 + in the Widget module's registry, find the handler_id callback, 160 + call it. 161 + - Initialize the Widget module's send function at startup so it 162 + can emit messages. 163 + 164 + ### 5. JS client (ocaml-worker.js) 165 + 166 + - Add `onWidgetUpdate` callback option (like `onOutputAt`) 167 + - Add `onWidgetClear` callback option 168 + - Add `sendWidgetEvent(widgetId, handlerId, eventType, value)` 169 + method 170 + - Handle new message types in `_handleMessage` 171 + 172 + ### 6. Frontend renderer 173 + 174 + A small JS/OCaml module that takes a View.node JSON tree and 175 + renders it to DOM elements, attaching event listeners that call 176 + `sendWidgetEvent`. This is essentially the experiment's 177 + `renderer.ml` adapted to work with the JSON representation. 178 + 179 + For the prototype, this can be a standalone HTML test page rather 180 + than full x-ocaml integration. 181 + 182 + ## Prototype Scope 183 + 184 + The prototype validates the full round-trip: user code creates a 185 + widget with Note signals → view updates flow to the frontend as 186 + JSON → frontend renders DOM → user interaction flows back → signal 187 + graph updates → all affected widgets re-render. 188 + 189 + **In scope:** 190 + - View types as shared library 191 + - View.node JSON encoding in message.ml 192 + - Widget module with display/clear/handler routing 193 + - WidgetUpdate/WidgetEvent/WidgetClear message types 194 + - JS client additions (onWidgetUpdate, sendWidgetEvent) 195 + - Worker-side event routing 196 + - Test HTML page with JS renderer 197 + 198 + **Out of scope (follow-up work):** 199 + - x-ocaml WebComponent integration 200 + - TyXML functor backend integration (convenience, not required) 201 + - Widget convenience library (Widget.slider, Widget.button, etc.) 202 + - CSS styling / theming 203 + - Widget persistence / serialization