DOCS / INTERNALS
Architecture
One render cycle, end to end: React commits a change → the reconciler’s host config serializes it as mutation ops → the JSON batch crosses the C bridge → the shadow tree applies it, Yoga recomputes layout, and the painter repaints the dirty region. Events travel the other way: JUCE mouse events hit-test into the tree and dispatch to JS listeners.
The mutation protocol
| OP | SHAPE |
|---|---|
create | ["create", id, type] — vs-view, vs-text, vs-image, vs-textinput, vs-native |
setProps | ["setProps", id, {style, hoverStyle, activeStyle, …}] — a node's first full props: resolved styles, listener flags, text, scrollTop |
patchProps | ["patchProps", id, {changed keys only}] — re-renders (since 0.0.28): only the top-level keys that changed; null removes a key. A style tweak never re-ships an unchanged image src, and an unchanged re-render sends nothing. |
appendChild / insertBefore / removeChild | Tree mutations, mirroring the reconciler exactly |
setText | ["setText", id, value] |
clearContainer | Root unmount |
Ops are batched per React commit and flushed once — a full remount of the StashTrack UI is a single bridge crossing, not hundreds.
Version handshake
Your UI bundle and the native module are two separately versioned halves of one program: @vsreact/core comes from npm, the module from the GIT_TAG in your FetchContent block. Bumping one and forgetting the other is a one-line mistake, so the module publishes the protocol level it speaks as __vsreact_protocol before your bundle is evaluated, and features that need a newer level fall back instead.
Without it the mismatch is invisible: a module that doesn't know an op ignores it, and the assertion that catches this compiles out in Release — so a bundle newer than its module would paint its first frame and then freeze, with no error, no overlay and nothing in the log. Instead, patchProps degrades to a full setProps (a strict superset) and one warning names both versions.
| LEVEL | ADDED |
|---|---|
1 | The op table above minus patchProps. Modules before 0.0.30 publish no level at all and are read as 1 — including 0.0.28/0.0.29, which implement the level-2 ops but predate the handshake. Under-reporting them is deliberate: a newer bundle can't tell them from 0.0.27, and setProps works on all of them. |
2 | patchProps; interned "img:N" handles as an <Image src>. Reported by modules from 0.0.30. |
Read the module's level at runtime with nativeProtocol() and your bundle's with PROTOCOL_VERSION — both worth putting in a support dump alongside VERSION.
The engine
The engine is QuickJS-ng — a complete ES2023 interpreter around one megabyte, running in-process on the message thread. Timers (setTimeout, setInterval) are provided by the native scheduler; promise rejections and uncaught exceptions route to the error overlay. There is no JIT, and there doesn’t need to be: JS runs only when your components render, and everything hot (layout, painting, hover) is C++.
JS gets a stack budget with real headroom below the thread it runs on, so runaway recursion raises a RangeError you see in the error overlay instead of faulting the process. That matters more here than in a browser: the process is your user’s DAW, and a crashed plugin can take an unsaved session with it. QuickJS’s own default is the full thread stack, which is the same as having no guard at all — the native stack is gone before the check runs.
Layout & painting
Yoga v2 provides the exact flexbox semantics React Native uses, so layout intuition transfers directly. The shadow tree owns one Yoga node per element; text nodes install measure functions so type sets its own size. The painter walks the laid-out tree with juce::Graphics: fills, borders, corner radii, shadows, knob arcs, glyph runs, scroll clipping. Hosted components (NativeView, TextInput) are real JUCE children positioned from layout results and visibility-synced with painted opacity.
The C bridge
Five host functions are all that connect the two worlds: __vsreact_flush (ops out), __vsreact_dispatch (events in), __vsreact_nativeCall (synchronous app calls), __vsreact_setTimer/clearTimer (scheduler), and __vsreact_log (console). Everything else — components, styling, parameters — is built on top of those in TypeScript and C++.