import React from "react"; import { LanguageProvider } from "../languages/types"; const RENDER_INTERVAL_MS = 50; export interface RendererRef { /** Update runtime state to renderer */ updateState: (state: RS | null) => void; } /** * React component that acts as an imperatively controller wrapper * around the actual language renderer. This is to pull renderer state updates * outside of Mainframe and debouncing rerenders for performance reasons. */ const RendererWrapperComponent = ( { renderer }: { renderer: LanguageProvider["Renderer"] }, ref: React.Ref> ) => { const [renderedState, setRenderedState] = React.useState(null); /** * Re-rendering on each state update becomes a bottleneck for running code at high speed. * The main browser thread will take too much time handling each message and the message queue * will pile up, leading to very visible delay in actions like pause and stop. * * To avoid this, we do some debouncing here. We only truly re-render at every X ms: on * other updates we just store the latest state in a ref. */ // Timer for re-rendering with the latest state const debounceTimerRef = React.useRef(null); // Ref to store the very latest state, that may be rendered upto X ms later const latestState = React.useRef(null); /** * Update the current renderer state. Debounces re-render by default, * pass `immediate` as true to immediately re-render with the given state. */ const updateState = (state: RS | null) => { latestState.current = state; if (debounceTimerRef.current) return; // Timeout will automatically render liveState // Set a timer to re-render with the latest result after X ms debounceTimerRef.current = setTimeout(() => { setRenderedState(latestState.current); debounceTimerRef.current = null; }, RENDER_INTERVAL_MS); }; React.useImperativeHandle(ref, () => ({ updateState })); return renderer({ state: renderedState }); }; export const RendererWrapper = React.forwardRef(RendererWrapperComponent);