MATLAB ↔ JS Communication
The IC framework runs MATLAB logic on one side and a Svelte 5 UI on the other. Between them sits a single uihtml control that acts as the communication bridge. Every property change, user interaction, component insertion, and method call flows through this one channel as serialized JSON events.
Architecture Overview
The system has five layers. MATLAB components publish events up a parent chain until they reach the View, which holds the uihtml control. On the JS side, the Bridge singleton receives events, batches them, and dispatches them through the Registry to the correct Component instance.
uihtml transport
MATLAB’s uihtml component embeds an iframe inside the CEF window where a uifigure lives. It loads an HTML page from disk and provides two functions for cross-boundary communication. These are the only two primitives the entire IC framework builds on:
- MATLAB to JS:
sendEventToHTMLSource(htmlElement, channelName, data)fires aCustomEventon the JS side. The JS code listens for it withaddEventListener(channelName, callback), and reads the payload fromevent.Data. - JS to MATLAB:
htmlComponent.sendEventToMATLAB(channelName, data)triggers theHTMLEventReceivedFcncallback on the MATLAB side. MATLAB reads the channel fromevt.HTMLEventNameand the payload fromevt.HTMLEventData.
The data payload is always travels serialized as JSON, and gets converted back to native types on the receiving end.
Startup Handshake
Before any component data flows, the MATLAB side must acknowledge that the iframe has been created. Otherwise some events could be dispatched before the frontend is ready, and get lost. While waiting for the initial handshake, MATLAB queues all events, and defers them until it receives the “ready” signal.
MATLAB to JS
This path carries property updates, component insertions, style operations, method calls, and more. Every event crossing the bridge towards the view follows the same route.
The MATLAB side: publish and bubble
A component wraps the data to bes sent into a ic.event.JsEvent, usually through publish(), and hands it to send(). The component does not know about the View or the uihtml control, it just delegates the sending to its parent: this.Parent.send(event). The parent does the same, and so on, until the event reaches the Frame. The Frame holds a reference to the View, which serializes the payload with toTransport() and calls sendEventToHTMLSource().
If a component publishes before it has a parent, the event doesn’t get lost: it goes into an internal queue instead of propagating. When the component is later attached, the queue is drained upwards until it reaches the Frame and gets send; or until it finds another unattached container, and is queued again.
Once the component is inserted into a container via addChild(), the container calls flush(), which drains the queue and sends all accumulated events up the parent chain.
The JS side: batching and dispatch
On the browser side, the Bridge singleton listens for events from sendEventToHTMLSource. Each call from MATLAB arrives as a separate browser macrotask, so many events sent from MATLAB in quick succession (accumulated before a drawnow event) will arrive as separate calls to the event listener.
The Bridge does not process them one by one. Instead, it pushes each event into a queue and schedules a single setTimeout(0). The timeout callback fires only after all pending macrotasks have completed, so by that point the queue contains every event from that MATLAB call.
Batching allows the Bridge to minimize expensive DOM updates by coalescing multiple events into one synchronous dispatch. If it processed each event immediately, every single one would trigger its own DOM update, which would show in the figure as a series of small, janky updates.
When the timeout fires, processQueue() runs the batch in three phases:
Preload — Walk all
@insertevents in the batch, collect the component types they reference and callFactory.preload()to lazy-import their Svelte modules in parallel. Lazy-importing is the relevant part here: the IC framework doesn’t load all components on startup (that would be too slow). Instead, it loads them on demand, the first time they’re inserted. Dynamically importing components is an async operation, so the Bridge must wait for it to complete before dispatching any events that reference those components.Synchronous dispatch — Enable batch mode (
setBatchMode(true)) and loop through every event. For each one, it looks up the target component by ID and calls the receiver for the event. The component fires the matching subscription callback. None of this is happens asynchronously, which means that callbacks will run back-to-back before any DOM updates happen. This results in a smooth flush of all changes from that batch.Flush — Call
flushSync()to apply every pending Svelte$statechange as a single DOM update.
New events can arrive from MATLAB during the async preload phase. The Bridge does not mix them into the current batch: after flushSync(), it checks the queue; if new events accumulated, it schedules another setTimeout(0) to handle them in a fresh cycle.
JS to MATLAB
The JS side: publish and send
Unlike the MATLAB-to-JS direction, thehere there is no batching and communication is much simpler. Each message from the frontend is sent immediately and individually.
The MATLAB side: receive and route
On the MATLAB side, the uihtml control fires HTMLEventReceivedFcn when data arrives. View.onHTMLEvent() reads the component field from the JsEvent and looks up the target in Frame.Registry, a flat map of component ID to component reference. It then calls component.receive(name, data).
The component’s receive method works the same way as on the JS side: it looks up name in a subscription map and fires the matching callback.