33using System . Runtime . CompilerServices ;
44using System . Runtime . InteropServices ;
55using System . Runtime . InteropServices . JavaScript ;
6+ using System . Threading ;
67using Abies ;
78using Abies . DOM ;
89
@@ -164,6 +165,11 @@ public record Program<TApplication, TArguments, TModel> : Program
164165 private Node ? _dom ;
165166 // todo: clean up handlers when they are no longer needed
166167 private readonly ConcurrentDictionary < string , Message > _handlers = new ( ) ;
168+ // Dispatch may be triggered from multiple threads (JavaScript events or
169+ // asynchronous API commands). DOM updates must run sequentially to keep
170+ // patches in order, otherwise operations like add/remove attribute can
171+ // target missing nodes.
172+ private readonly SemaphoreSlim _dispatchLock = new ( 1 , 1 ) ;
167173
168174 public async Task Run ( TArguments arguments )
169175 {
@@ -210,8 +216,57 @@ private void RegisterHandlers(Node node)
210216 }
211217 }
212218
219+ private static Node PreserveIds ( Node ? oldNode , Node newNode )
220+ {
221+ if ( oldNode is Element oldElement && newNode is Element newElement && oldElement . Tag == newElement . Tag )
222+ {
223+ // Preserve attribute IDs so DiffAttributes can emit UpdateAttribute
224+ // instead of a remove/add pair. This avoids wiping attributes when
225+ // remove is processed after add.
226+ var attrs = new Abies . DOM . Attribute [ newElement . Attributes . Length ] ;
227+ for ( int i = 0 ; i < newElement . Attributes . Length ; i ++ )
228+ {
229+ var attr = newElement . Attributes [ i ] ;
230+ var oldAttr = Array . Find ( oldElement . Attributes , a => a . Name == attr . Name ) ;
231+ var attrId = oldAttr ? . Id ?? attr . Id ;
232+
233+ if ( attr . Name == "id" )
234+ {
235+ attrs [ i ] = attr with { Id = attrId , Value = oldElement . Id } ;
236+ }
237+ else
238+ {
239+ attrs [ i ] = attr with { Id = attrId } ;
240+ }
241+ }
242+
243+ var children = new Node [ newElement . Children . Length ] ;
244+ for ( int i = 0 ; i < newElement . Children . Length ; i ++ )
245+ {
246+ var oldChild = i < oldElement . Children . Length ? oldElement . Children [ i ] : null ;
247+ children [ i ] = PreserveIds ( oldChild , newElement . Children [ i ] ) ;
248+ }
249+
250+ return new Element ( oldElement . Id , newElement . Tag , attrs , children ) ;
251+ }
252+ else if ( newNode is Element newElem )
253+ {
254+ var children = new Node [ newElem . Children . Length ] ;
255+ for ( int i = 0 ; i < newElem . Children . Length ; i ++ )
256+ {
257+ children [ i ] = PreserveIds ( null , newElem . Children [ i ] ) ;
258+ }
259+ return new Element ( newElem . Id , newElem . Tag , newElem . Attributes , children ) ;
260+ }
261+
262+ return newNode ;
263+ }
264+
213265 public async Task Dispatch ( Message message )
214266 {
267+ await _dispatchLock . WaitAsync ( ) ;
268+ try
269+ {
215270 if ( model is null )
216271 {
217272 await Interop . WriteToConsole ( "Model not initialized" ) ;
@@ -232,8 +287,10 @@ public async Task Dispatch(Message message)
232287 // Generate new virtual DOM
233288 var newDocument = TApplication . View ( newModel ) ;
234289
290+ var alignedBody = PreserveIds ( _dom , newDocument . Body ) ;
291+
235292 // Compute the patches
236- var patches = Operations . Diff ( _dom , newDocument . Body ) ;
293+ var patches = Operations . Diff ( _dom , alignedBody ) ;
237294
238295 // Apply patches and (de)register handlers
239296 foreach ( var patch in patches )
@@ -257,14 +314,21 @@ public async Task Dispatch(Message message)
257314 }
258315
259316 // Update the current virtual DOM
260- _dom = newDocument . Body ;
317+ _dom = alignedBody ;
261318 await Interop . SetTitle ( newDocument . Title ) ;
262319
263320 foreach ( var command in commands )
264321 {
265322 await HandleCommand ( command ) ;
266323 }
267- } private static async Task HandleCommand ( Command command )
324+ }
325+ finally
326+ {
327+ _dispatchLock . Release ( ) ;
328+ }
329+ }
330+
331+ private static async Task HandleCommand ( Command command )
268332 {
269333 switch ( command )
270334 {
@@ -432,7 +496,7 @@ private static void RenderNode(Node node, System.Text.StringBuilder sb)
432496 switch ( node )
433497 {
434498 case Element element :
435- sb . Append ( $ "<{ element . Tag } ") ;
499+ sb . Append ( $ "<{ element . Tag } id= \" { element . Id } \" ") ;
436500 foreach ( var attr in element . Attributes )
437501 {
438502 if ( attr is Handler handler )
@@ -450,7 +514,7 @@ private static void RenderNode(Node node, System.Text.StringBuilder sb)
450514 sb . Append ( $ "</{ element . Tag } >") ;
451515 break ;
452516 case Text text :
453- sb . Append ( $ "<span id={ text . Id } >{ System . Web . HttpUtility . HtmlEncode ( text . Value ) } </span>") ;
517+ sb . Append ( $ "<span id=\" { text . Id } \" >{ System . Web . HttpUtility . HtmlEncode ( text . Value ) } </span>") ;
454518 break ;
455519 // Handle other node types if necessary
456520 default :
0 commit comments