@@ -11,10 +11,22 @@ export interface UseExternalApiParams {
1111 onStartSimulation : ( ) => void ;
1212 /** Called when a STOP_SIMULATION message is received. */
1313 onStopSimulation : ( ) => void ;
14+ /** Called when a PAUSE_SIMULATION message is received. */
15+ onPauseSimulation : ( ) => void ;
16+ /** Called when a RESUME_SIMULATION message is received. */
17+ onResumeSimulation : ( ) => void ;
1418 /** Called when a SET_PIN_STATE message is received. */
1519 onSetPinState : ( pin : number , value : number ) => void ;
1620 /** Returns the current value of a pin (used for GET_PIN_STATE responses). */
1721 getPinState : ( pin : number ) => number ;
22+ /** Called when a SERIAL_INPUT message is received. */
23+ onSerialInput : ( data : string ) => void ;
24+ /** Called when a SET_SIMULATION_TIMEOUT message is received. */
25+ onSetSimulationTimeout : ( timeout : number ) => void ;
26+ /** Called when a SET_OUTPUT_TAB message is received. */
27+ onSetOutputTab : ( tab : "compiler" | "messages" | "registry" | "debug" ) => void ;
28+ /** Returns the current simulation state (used for GET_SIMULATION_STATE responses). */
29+ getSimulationState : ( ) => string ;
1830}
1931
2032// Global storage for the allowed origin (set by useExternalApi hook)
@@ -37,7 +49,7 @@ export function sendMessageToParent(
3749 ...response ,
3850 version : API_VERSION ,
3951 } as SimulatorResponse ;
40- globalThis . postMessage ( withVersion , targetOrigin ) ;
52+ ( globalThis . parent ?? globalThis ) . postMessage ( withVersion , targetOrigin ) ;
4153}
4254
4355/**
@@ -51,7 +63,7 @@ export function sendEventToParent(
5163 event : SimulatorEventMessage ,
5264 targetOrigin : string ,
5365) : void {
54- globalThis . postMessage ( event , targetOrigin ) ;
66+ ( globalThis . parent ?? globalThis ) . postMessage ( event , targetOrigin ) ;
5567}
5668
5769/**
@@ -67,85 +79,157 @@ export function useExternalApi(params: UseExternalApiParams): void {
6779 onLoadCode,
6880 onStartSimulation,
6981 onStopSimulation,
82+ onPauseSimulation,
83+ onResumeSimulation,
7084 onSetPinState,
7185 getPinState,
86+ onSerialInput,
87+ onSetSimulationTimeout,
88+ onSetOutputTab,
89+ getSimulationState,
7290 } = params ;
7391
7492 // Store the allowed origin globally for use by event-sending functions
7593 useEffect ( ( ) => {
7694 _allowedOriginRef . value = allowedOrigin ;
7795 } , [ allowedOrigin ] ) ;
7896
79- useEffect ( ( ) => {
80- const handleMessage = ( event : MessageEvent ) : void => {
81- // ── Security check ──────────────────────────────────────────────────
82- if ( allowedOrigin !== "*" && event . origin !== allowedOrigin ) {
83- return ;
84- }
97+ // ── Action handler dispatchers ──────────────────────────────────────────
98+ const handleLoadCode = ( payload : unknown ) : void => {
99+ if ( typeof ( payload as { code ?: unknown } ) ?. code !== "string" ) {
100+ sendMessageToParent ( { type : SimulatorActionType . LOAD_CODE , success : false , error : "payload.code must be a string" } , allowedOrigin ) ;
101+ return ;
102+ }
103+ onLoadCode ( ( payload as { code : string } ) . code ) ;
104+ sendMessageToParent ( { type : SimulatorActionType . LOAD_CODE , success : true } , allowedOrigin ) ;
105+ } ;
85106
86- const msg = event . data ;
107+ const handlePinState = ( payload : unknown ) : void => {
108+ const p = payload as { pin ?: unknown ; value ?: unknown } ;
109+ if ( typeof p ?. pin !== "number" || typeof p ?. value !== "number" ) {
110+ sendMessageToParent ( { type : SimulatorActionType . SET_PIN_STATE , success : false , error : "payload.pin and payload.value must be numbers" } , allowedOrigin ) ;
111+ return ;
112+ }
113+ onSetPinState ( p . pin , p . value ) ;
114+ sendMessageToParent ( { type : SimulatorActionType . SET_PIN_STATE , success : true } , allowedOrigin ) ;
115+ } ;
116+
117+ const handleGetPinState = ( payload : unknown ) : void => {
118+ if ( typeof ( payload as { pin ?: unknown } ) ?. pin !== "number" ) {
119+ sendMessageToParent ( { type : SimulatorActionType . GET_PIN_STATE , success : false , error : "payload.pin must be a number" } , allowedOrigin ) ;
120+ return ;
121+ }
122+ const value = getPinState ( ( payload as { pin : number } ) . pin ) ;
123+ sendMessageToParent ( { type : SimulatorActionType . GET_PIN_STATE , success : true , data : value } , allowedOrigin ) ;
124+ } ;
87125
88- // ── Guard: must be a plain object with a `type` string ───────────────
89- if ( typeof msg !== "object" || msg === null || typeof msg . type !== "string" ) {
90- return ;
126+ const handleBatchSetPinState = ( payload : unknown ) : void => {
127+ if ( ! Array . isArray ( ( payload as { pins ?: unknown } ) ?. pins ) ) {
128+ sendMessageToParent ( { type : SimulatorActionType . BATCH_SET_PIN_STATE , success : false , error : "payload.pins must be an array" } , allowedOrigin ) ;
129+ return ;
130+ }
131+ for ( const pinState of ( payload as { pins : Array < { pin ?: unknown ; value ?: unknown } > } ) . pins ) {
132+ if ( typeof pinState ?. pin === "number" && typeof pinState ?. value === "number" ) {
133+ onSetPinState ( pinState . pin , pinState . value ) ;
91134 }
135+ }
136+ sendMessageToParent ( { type : SimulatorActionType . BATCH_SET_PIN_STATE , success : true } , allowedOrigin ) ;
137+ } ;
138+
139+ const handleSerialInput = ( payload : unknown ) : void => {
140+ if ( typeof ( payload as { data ?: unknown } ) ?. data !== "string" ) {
141+ sendMessageToParent ( { type : SimulatorActionType . SERIAL_INPUT , success : false , error : "payload.data must be a string" } , allowedOrigin ) ;
142+ return ;
143+ }
144+ onSerialInput ( ( payload as { data : string } ) . data ) ;
145+ sendMessageToParent ( { type : SimulatorActionType . SERIAL_INPUT , success : true } , allowedOrigin ) ;
146+ } ;
92147
148+ const handleSetTimeout = ( payload : unknown ) : void => {
149+ if ( typeof ( payload as { timeout ?: unknown } ) ?. timeout !== "number" || ( payload as { timeout : number } ) . timeout < 0 ) {
150+ sendMessageToParent ( { type : SimulatorActionType . SET_SIMULATION_TIMEOUT , success : false , error : "payload.timeout must be a non-negative number" } , allowedOrigin ) ;
151+ return ;
152+ }
153+ onSetSimulationTimeout ( ( payload as { timeout : number } ) . timeout / 1000 ) ;
154+ sendMessageToParent ( { type : SimulatorActionType . SET_SIMULATION_TIMEOUT , success : true } , allowedOrigin ) ;
155+ } ;
156+
157+ const handleSetOutputTab = ( payload : unknown ) : void => {
158+ const validTabs = [ "compiler" , "messages" , "registry" , "debug" ] ;
159+ if ( typeof ( payload as { tab ?: unknown } ) ?. tab !== "string" || ! validTabs . includes ( ( payload as { tab : string } ) . tab ) ) {
160+ sendMessageToParent ( { type : SimulatorActionType . SET_OUTPUT_TAB , success : false , error : `payload.tab must be one of: ${ validTabs . join ( ", " ) } ` } , allowedOrigin ) ;
161+ return ;
162+ }
163+ onSetOutputTab ( ( payload as { tab : "compiler" | "messages" | "registry" | "debug" } ) . tab ) ;
164+ sendMessageToParent ( { type : SimulatorActionType . SET_OUTPUT_TAB , success : true } , allowedOrigin ) ;
165+ } ;
166+
167+ const handleGetState = ( ) : void => {
168+ const state = getSimulationState ( ) ;
169+ sendMessageToParent ( { type : SimulatorActionType . GET_SIMULATION_STATE , success : true , data : state } , allowedOrigin ) ;
170+ } ;
171+
172+ useEffect ( ( ) => {
173+ const handleMessage = ( event : MessageEvent ) : void => {
174+ if ( allowedOrigin !== "*" && event . origin !== allowedOrigin ) return ;
175+ const msg = event . data ;
176+ if ( typeof msg !== "object" || msg === null || typeof msg . type !== "string" ) return ;
93177 const message = msg as SimulatorMessage ;
94178
95179 switch ( message . type ) {
96- case SimulatorActionType . LOAD_CODE : {
97- const payload = message . payload as { code : string } ;
98- onLoadCode ( payload . code ) ;
180+ case SimulatorActionType . LOAD_CODE :
181+ handleLoadCode ( message . payload ) ;
99182 break ;
100- }
101-
102- case SimulatorActionType . START_SIMULATION : {
183+ case SimulatorActionType . START_SIMULATION :
103184 onStartSimulation ( ) ;
185+ sendMessageToParent ( { type : SimulatorActionType . START_SIMULATION , success : true } , allowedOrigin ) ;
104186 break ;
105- }
106-
107- case SimulatorActionType . STOP_SIMULATION : {
187+ case SimulatorActionType . STOP_SIMULATION :
108188 onStopSimulation ( ) ;
189+ sendMessageToParent ( { type : SimulatorActionType . STOP_SIMULATION , success : true } , allowedOrigin ) ;
109190 break ;
110- }
111-
112- case SimulatorActionType . SET_PIN_STATE : {
113- const payload = message . payload as { pin : number ; value : number } ;
114- onSetPinState ( payload . pin , payload . value ) ;
191+ case SimulatorActionType . PAUSE_SIMULATION :
192+ onPauseSimulation ( ) ;
193+ sendMessageToParent ( { type : SimulatorActionType . PAUSE_SIMULATION , success : true } , allowedOrigin ) ;
115194 break ;
116- }
117-
118- case SimulatorActionType . GET_PIN_STATE : {
119- const payload = message . payload as { pin : number } ;
120- const value = getPinState ( payload . pin ) ;
121- sendMessageToParent (
122- { type : SimulatorActionType . GET_PIN_STATE , success : true , data : value } ,
123- allowedOrigin ,
124- ) ;
195+ case SimulatorActionType . RESUME_SIMULATION :
196+ onResumeSimulation ( ) ;
197+ sendMessageToParent ( { type : SimulatorActionType . RESUME_SIMULATION , success : true } , allowedOrigin ) ;
125198 break ;
126- }
127-
128- case SimulatorActionType . BATCH_SET_PIN_STATE : {
129- const payload = message . payload as { pins : Array < { pin : number ; value : number } > } ;
130- if ( Array . isArray ( payload . pins ) ) {
131- for ( const pinState of payload . pins ) {
132- onSetPinState ( pinState . pin , pinState . value ) ;
133- }
134- }
199+ case SimulatorActionType . SET_PIN_STATE :
200+ handlePinState ( message . payload ) ;
135201 break ;
136- }
137-
138- default :
139- // Unknown action — silently ignore
202+ case SimulatorActionType . GET_PIN_STATE :
203+ handleGetPinState ( message . payload ) ;
204+ break ;
205+ case SimulatorActionType . BATCH_SET_PIN_STATE :
206+ handleBatchSetPinState ( message . payload ) ;
207+ break ;
208+ case SimulatorActionType . SERIAL_INPUT :
209+ handleSerialInput ( message . payload ) ;
210+ break ;
211+ case SimulatorActionType . SET_SIMULATION_TIMEOUT :
212+ handleSetTimeout ( message . payload ) ;
213+ break ;
214+ case SimulatorActionType . SET_OUTPUT_TAB :
215+ handleSetOutputTab ( message . payload ) ;
216+ break ;
217+ case SimulatorActionType . GET_SIMULATION_STATE :
218+ handleGetState ( ) ;
140219 break ;
220+ // default: silently ignore unknown actions
141221 }
142222 } ;
143223
144- window . addEventListener ( "message" , handleMessage ) ;
224+ globalThis . addEventListener ( "message" , handleMessage ) ;
145225 return ( ) => {
146- window . removeEventListener ( "message" , handleMessage ) ;
226+ globalThis . removeEventListener ( "message" , handleMessage ) ;
147227 } ;
148- } , [ allowedOrigin , onLoadCode , onStartSimulation , onStopSimulation , onSetPinState , getPinState ] ) ;
228+ } , [
229+ allowedOrigin , onLoadCode , onStartSimulation , onStopSimulation ,
230+ onPauseSimulation , onResumeSimulation , onSetPinState , getPinState ,
231+ onSerialInput , onSetSimulationTimeout , onSetOutputTab , getSimulationState ,
232+ ] ) ;
149233}
150234
151235/**
0 commit comments