11/**
22 * Mutation Queue
33 * Collects graph changes (add/remove blocks, connections, parameter/setting changes)
4- * as Python code strings . Changes are NOT applied automatically — the user
4+ * as structured command objects . Changes are NOT applied automatically — the user
55 * explicitly stages them via a "Stage Changes" action.
66 *
77 * On "Run": queue is cleared, mappings initialized from code generation result.
1111 * Design:
1212 * - Structural mutations (add/remove block/connection) are queued in order.
1313 * - Parameter and setting updates are coalesced: only the latest value per key.
14- * - Each mutation is wrapped in try/except for error isolation on flush.
14+ * - Mutations are serialized as JSON and dispatched by a Python-side handler
15+ * (_apply_mutations) which handles per-mutation error isolation.
1516 * - pendingMutationCount is a Svelte store for UI reactivity (badge on stage button).
1617 */
1718
@@ -21,6 +22,16 @@ import { nodeRegistry } from '$lib/nodes/registry';
2122import { isSubsystem } from '$lib/nodes/shapes' ;
2223import { sanitizeName } from './codeBuilder' ;
2324
25+ // --- Command types ---
26+
27+ type MutationCommand =
28+ | { type : 'set_param' ; var : string ; param : string ; value : string }
29+ | { type : 'set_setting' ; code : string }
30+ | { type : 'add_block' ; var : string ; blockClass : string ; params : Record < string , string > ; nodeId : string ; nodeName : string }
31+ | { type : 'remove_block' ; var : string ; nodeId : string }
32+ | { type : 'add_connection' ; var : string ; sourceVar : string ; sourcePort : number ; targetVar : string ; targetPort : number }
33+ | { type : 'remove_connection' ; var : string } ;
34+
2435// --- Internal state ---
2536
2637/** Active variable name mappings from the last run */
@@ -31,13 +42,13 @@ let activeConnVars = new Map<string, string>(); // connectionId → Python va
3142let dynamicVarCounter = 0 ;
3243
3344/** Ordered structural mutations (add/remove block/connection) */
34- const structuralQueue : string [ ] = [ ] ;
45+ const structuralQueue : MutationCommand [ ] = [ ] ;
3546
36- /** Coalesced parameter updates: "nodeId:paramName" → Python assignment */
37- const paramUpdates = new Map < string , string > ( ) ;
47+ /** Coalesced parameter updates: "nodeId:paramName" → command */
48+ const paramUpdates = new Map < string , MutationCommand > ( ) ;
3849
39- /** Coalesced setting updates: key → Python code */
40- const settingUpdates = new Map < string , string > ( ) ;
50+ /** Coalesced setting updates: key → command */
51+ const settingUpdates = new Map < string , MutationCommand > ( ) ;
4152
4253/** Reactive store: number of pending mutations */
4354export const pendingMutationCount = writable ( 0 ) ;
@@ -83,34 +94,38 @@ export function clearQueue(): void {
8394
8495/**
8596 * Get all pending mutations as a Python code string and clear the queue.
86- * Each mutation is wrapped in try/except for error isolation .
97+ * Mutations are serialized as JSON and dispatched via _apply_mutations() .
8798 * Order: settings first, then structural mutations, then parameter updates.
8899 */
89100export function flushQueue ( ) : string | null {
90- const allCode : string [ ] = [ ] ;
101+ const allCommands : MutationCommand [ ] = [ ] ;
91102
92103 // 1. Settings (apply before structural changes)
93- for ( const code of settingUpdates . values ( ) ) {
94- allCode . push ( wrapTryExcept ( code ) ) ;
104+ for ( const cmd of settingUpdates . values ( ) ) {
105+ allCommands . push ( cmd ) ;
95106 }
96107
97108 // 2. Structural mutations (add/remove in order)
98- for ( const code of structuralQueue ) {
99- allCode . push ( wrapTryExcept ( code ) ) ;
109+ for ( const cmd of structuralQueue ) {
110+ allCommands . push ( cmd ) ;
100111 }
101112
102113 // 3. Parameter updates (apply after blocks exist)
103- for ( const code of paramUpdates . values ( ) ) {
104- allCode . push ( wrapTryExcept ( code ) ) ;
114+ for ( const cmd of paramUpdates . values ( ) ) {
115+ allCommands . push ( cmd ) ;
105116 }
106117
107118 structuralQueue . length = 0 ;
108119 paramUpdates . clear ( ) ;
109120 settingUpdates . clear ( ) ;
110121 updateCount ( ) ;
111122
112- if ( allCode . length === 0 ) return null ;
113- return allCode . join ( '\n' ) ;
123+ if ( allCommands . length === 0 ) return null ;
124+
125+ // Double stringify: inner produces the JSON array,
126+ // outer wraps it as a Python string literal with proper escaping
127+ const jsonPayload = JSON . stringify ( JSON . stringify ( allCommands ) ) ;
128+ return `_apply_mutations(${ jsonPayload } )` ;
114129}
115130
116131/**
@@ -141,23 +156,22 @@ export function queueAddBlock(node: NodeInstance): void {
141156 activeNodeVars . set ( node . id , varName ) ;
142157
143158 const validParamNames = new Set ( typeDef . params . map ( p => p . name ) ) ;
144- const paramParts : string [ ] = [ ] ;
159+ const params : Record < string , string > = { } ;
145160 for ( const [ name , value ] of Object . entries ( node . params ) ) {
146161 if ( value === null || value === undefined || value === '' ) continue ;
147162 if ( name . startsWith ( '_' ) ) continue ;
148163 if ( ! validParamNames . has ( name ) ) continue ;
149- paramParts . push ( ` ${ name } = ${ value } ` ) ;
164+ params [ name ] = String ( value ) ;
150165 }
151- const params = paramParts . join ( ', ' ) ;
152- const constructor = params ? `${ typeDef . blockClass } (${ params } )` : `${ typeDef . blockClass } ()` ;
153-
154- structuralQueue . push ( [
155- `${ varName } = ${ constructor } ` ,
156- `sim.add_block(${ varName } )` ,
157- `blocks.append(${ varName } )` ,
158- `_node_id_map[id(${ varName } )] = "${ node . id } "` ,
159- `_node_name_map["${ node . id } "] = "${ node . name . replace ( / " / g, '\\"' ) } "`
160- ] . join ( '\n' ) ) ;
166+
167+ structuralQueue . push ( {
168+ type : 'add_block' ,
169+ var : varName ,
170+ blockClass : typeDef . blockClass ,
171+ params,
172+ nodeId : node . id ,
173+ nodeName : node . name
174+ } ) ;
161175 updateCount ( ) ;
162176}
163177
@@ -168,12 +182,11 @@ export function queueRemoveBlock(nodeId: string): void {
168182 const varName = activeNodeVars . get ( nodeId ) ;
169183 if ( ! varName ) return ;
170184
171- structuralQueue . push ( [
172- `sim.remove_block(${ varName } )` ,
173- `blocks.remove(${ varName } )` ,
174- `_node_id_map.pop(id(${ varName } ), None)` ,
175- `_node_name_map.pop("${ nodeId } ", None)`
176- ] . join ( '\n' ) ) ;
185+ structuralQueue . push ( {
186+ type : 'remove_block' ,
187+ var : varName ,
188+ nodeId
189+ } ) ;
177190 activeNodeVars . delete ( nodeId ) ;
178191
179192 // Remove any coalesced param updates for this block
@@ -198,11 +211,14 @@ export function queueAddConnection(conn: Connection): void {
198211 const varName = `conn_dyn_${ dynamicVarCounter ++ } ` ;
199212 activeConnVars . set ( conn . id , varName ) ;
200213
201- structuralQueue . push ( [
202- `${ varName } = Connection(${ sourceVar } [${ conn . sourcePortIndex } ], ${ targetVar } [${ conn . targetPortIndex } ])` ,
203- `sim.add_connection(${ varName } )` ,
204- `connections.append(${ varName } )`
205- ] . join ( '\n' ) ) ;
214+ structuralQueue . push ( {
215+ type : 'add_connection' ,
216+ var : varName ,
217+ sourceVar,
218+ sourcePort : conn . sourcePortIndex ,
219+ targetVar,
220+ targetPort : conn . targetPortIndex
221+ } ) ;
206222 updateCount ( ) ;
207223}
208224
@@ -213,10 +229,10 @@ export function queueRemoveConnection(connId: string): void {
213229 const varName = activeConnVars . get ( connId ) ;
214230 if ( ! varName ) return ;
215231
216- structuralQueue . push ( [
217- `sim. remove_connection( ${ varName } )` ,
218- `connections.remove( ${ varName } )`
219- ] . join ( '\n' ) ) ;
232+ structuralQueue . push ( {
233+ type : ' remove_connection' ,
234+ var : varName
235+ } ) ;
220236 activeConnVars . delete ( connId ) ;
221237 updateCount ( ) ;
222238}
@@ -229,7 +245,12 @@ export function queueUpdateParam(nodeId: string, paramName: string, value: strin
229245 const varName = activeNodeVars . get ( nodeId ) ;
230246 if ( ! varName ) return ;
231247
232- paramUpdates . set ( `${ nodeId } :${ paramName } ` , `${ varName } .${ paramName } = ${ value } ` ) ;
248+ paramUpdates . set ( `${ nodeId } :${ paramName } ` , {
249+ type : 'set_param' ,
250+ var : varName ,
251+ param : paramName ,
252+ value
253+ } ) ;
233254 updateCount ( ) ;
234255}
235256
@@ -242,7 +263,10 @@ export function queueUpdateParam(nodeId: string, paramName: string, value: strin
242263export function queueUpdateSetting ( key : string , code : string ) : void {
243264 if ( ! isActive ( ) ) return ;
244265
245- settingUpdates . set ( key , code ) ;
266+ settingUpdates . set ( key , {
267+ type : 'set_setting' ,
268+ code
269+ } ) ;
246270 updateCount ( ) ;
247271}
248272
@@ -255,10 +279,3 @@ export function getNodeVar(nodeId: string): string | undefined {
255279export function getConnVar ( connId : string ) : string | undefined {
256280 return activeConnVars . get ( connId ) ;
257281}
258-
259- // --- Internal helpers ---
260-
261- function wrapTryExcept ( code : string ) : string {
262- const indented = code . split ( '\n' ) . map ( line => ` ${ line } ` ) . join ( '\n' ) ;
263- return `try:\n${ indented } \nexcept Exception as _e:\n print(f"Mutation error: {_e}", file=__import__('sys').stderr)` ;
264- }
0 commit comments