@@ -69,10 +69,11 @@ public class MobileControlWebsocketServer : EssentialsDevice
6969 private readonly ConcurrentDictionary < string , string > pendingClientRegistrations = new ConcurrentDictionary < string , string > ( ) ;
7070
7171 /// <summary>
72- /// Stores queues of pending client IDs per token for legacy clients (FIFO)
73- /// This ensures thread-safety when multiple legacy clients use the same token
72+ /// Stores pending client registrations with timestamp for legacy clients
73+ /// Key is token, Value is list of (clientId, timestamp) tuples
74+ /// Most recent registration is used to handle duplicate join requests
7475 /// </summary>
75- private readonly ConcurrentDictionary < string , ConcurrentQueue < string > > legacyClientIdQueues = new ConcurrentDictionary < string , ConcurrentQueue < string > > ( ) ;
76+ private readonly ConcurrentDictionary < string , ConcurrentBag < ( string clientId , DateTime timestamp ) > > legacyClientRegistrations = new ConcurrentDictionary < string , ConcurrentBag < ( string , DateTime ) > > ( ) ;
7677
7778 /// <summary>
7879 /// Gets the collection of UI clients
@@ -736,15 +737,23 @@ private void GenerateClientTokenFromConsole(string s)
736737
737738 private UiClient BuildUiClient ( string roomKey , JoinToken token , string key )
738739 {
739- // Dequeue the next clientId for legacy client support (FIFO per token )
740+ // Get the most recent unused clientId for this token (legacy support )
740741 // New clients will override this ID in OnOpen with the validated query parameter value
741742 var clientId = "pending" ;
742- if ( legacyClientIdQueues . TryGetValue ( key , out var queue ) && queue . TryDequeue ( out var dequeuedId ) )
743+ if ( legacyClientRegistrations . TryGetValue ( key , out var registrations ) )
743744 {
744- clientId = dequeuedId ;
745- this . LogVerbose ( "Dequeued legacy clientId {clientId} for token {token}" , clientId , key ) ;
745+ // Get most recent registration
746+ var sorted = registrations . OrderByDescending ( r => r . timestamp ) . ToList ( ) ;
747+ if ( sorted . Any ( ) )
748+ {
749+ clientId = sorted . First ( ) . clientId ;
750+ // Remove it from the bag
751+ var newBag = new ConcurrentBag < ( string , DateTime ) > ( sorted . Skip ( 1 ) ) ;
752+ legacyClientRegistrations . TryUpdate ( key , newBag , registrations ) ;
753+ this . LogVerbose ( "Assigned most recent legacy clientId {clientId} for token {token}" , clientId , key ) ;
754+ }
746755 }
747-
756+
748757 var c = new UiClient ( $ "uiclient-{ key } -{ roomKey } -{ clientId } ", clientId , token . Token , token . TouchpanelKey ) ;
749758 this . LogInformation ( "Constructing UiClient with key {key} and temporary ID (will be set from query param)" , key ) ;
750759 c . Controller = _parent ;
@@ -753,8 +762,8 @@ private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
753762 c . Server = this ; // Give UiClient access to server for ID registration
754763
755764 // Don't add to uiClients yet - will be added in OnOpen after ID is set from query param
756-
757- c . ConnectionClosed += ( o , a ) =>
765+
766+ c . ConnectionClosed += ( o , a ) =>
758767 {
759768 uiClients . TryRemove ( a . ClientId , out _ ) ;
760769 // Clean up any pending registrations for this token
@@ -765,11 +774,11 @@ private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
765774 {
766775 pendingClientRegistrations . TryRemove ( k , out _ ) ;
767776 }
768-
769- // Clean up legacy queue if empty
770- if ( legacyClientIdQueues . TryGetValue ( key , out var legacyQueue ) && legacyQueue . IsEmpty )
777+
778+ // Clean up legacy registrations if empty
779+ if ( legacyClientRegistrations . TryGetValue ( key , out var legacyBag ) && legacyBag . IsEmpty )
771780 {
772- legacyClientIdQueues . TryRemove ( key , out _ ) ;
781+ legacyClientRegistrations . TryRemove ( key , out _ ) ;
773782 }
774783 } ;
775784 return c ;
@@ -785,7 +794,7 @@ private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
785794 public bool RegisterUiClient ( UiClient client , string clientId , string tokenKey )
786795 {
787796 var registrationKey = $ "{ tokenKey } -{ clientId } ";
788-
797+
789798 // Verify this clientId was generated during a join request for this token
790799 if ( ! pendingClientRegistrations . TryRemove ( registrationKey , out _ ) )
791800 {
@@ -799,11 +808,63 @@ public bool RegisterUiClient(UiClient client, string clientId, string tokenKey)
799808 this . LogWarning ( "Replacing existing client with duplicate id {id}" , id ) ;
800809 return client ;
801810 } ) ;
802-
811+
803812 this . LogInformation ( "Successfully registered UiClient with ID {clientId} for token {token}" , clientId , tokenKey ) ;
804813 return true ;
805814 }
806815
816+ /// <summary>
817+ /// Updates a client's ID when a mismatch is detected between stored ID and message ID
818+ /// </summary>
819+ /// <param name="oldClientId">The current/old client ID</param>
820+ /// <param name="newClientId">The new client ID from the message</param>
821+ /// <param name="tokenKey">The token key for validation</param>
822+ /// <returns>True if update successful, false otherwise</returns>
823+ public bool UpdateClientId ( string oldClientId , string newClientId , string tokenKey )
824+ {
825+ if ( string . IsNullOrEmpty ( oldClientId ) || string . IsNullOrEmpty ( newClientId ) )
826+ {
827+ this . LogWarning ( "Cannot update client ID with null or empty values" ) ;
828+ return false ;
829+ }
830+
831+ if ( oldClientId == newClientId )
832+ {
833+ return true ; // No update needed
834+ }
835+
836+ // Verify the new clientId was registered for this token
837+ var registrationKey = $ "{ tokenKey } -{ newClientId } ";
838+ if ( ! pendingClientRegistrations . TryRemove ( registrationKey , out _ ) )
839+ {
840+ this . LogWarning ( "Cannot update to unregistered clientId {newClientId} for token {token}" , newClientId , tokenKey ) ;
841+ return false ;
842+ }
843+
844+ // Get the existing client
845+ if ( ! uiClients . TryRemove ( oldClientId , out var client ) )
846+ {
847+ this . LogWarning ( "Cannot find client with old ID {oldClientId}" , oldClientId ) ;
848+ return false ;
849+ }
850+
851+ // Update the client's ID
852+ client . UpdateId ( newClientId ) ;
853+
854+ // Re-add with new ID
855+ if ( ! uiClients . TryAdd ( newClientId , client ) )
856+ {
857+ // If add fails, try to restore old entry
858+ uiClients . TryAdd ( oldClientId , client ) ;
859+ client . UpdateId ( oldClientId ) ;
860+ this . LogError ( "Failed to update client ID from {oldClientId} to {newClientId}" , oldClientId , newClientId ) ;
861+ return false ;
862+ }
863+
864+ this . LogInformation ( "Successfully updated client ID from {oldClientId} to {newClientId}" , oldClientId , newClientId ) ;
865+ return true ;
866+ }
867+
807868 /// <summary>
808869 /// Registers a UiClient using legacy flow (for backwards compatibility with older clients)
809870 /// </summary>
@@ -821,7 +882,7 @@ public void RegisterLegacyUiClient(UiClient client)
821882 this . LogWarning ( "Replacing existing client with duplicate id {id} (legacy flow)" , id ) ;
822883 return client ;
823884 } ) ;
824-
885+
825886 this . LogInformation ( "Successfully registered UiClient with ID {clientId} using legacy flow" , client . Id ) ;
826887 }
827888
@@ -1133,16 +1194,17 @@ private void HandleJoinRequest(HttpListenerRequest req, HttpListenerResponse res
11331194
11341195 // Generate a client ID for this join request
11351196 var clientId = $ "{ Utilities . GetNextClientId ( ) } ";
1136-
1197+ var now = DateTime . UtcNow ;
1198+
11371199 // Store in pending registrations for new clients that send clientId via query param
11381200 var registrationKey = $ "{ token } -{ clientId } ";
11391201 pendingClientRegistrations . TryAdd ( registrationKey , clientId ) ;
1140-
1141- // Also enqueue for legacy clients (thread-safe FIFO per token)
1142- var queue = legacyClientIdQueues . GetOrAdd ( token , _ => new ConcurrentQueue < string > ( ) ) ;
1143- queue . Enqueue ( clientId ) ;
11441202
1145- this . LogVerbose ( "Assigning ClientId: {clientId} for token: {token}" , clientId , token ) ;
1203+ // For legacy clients, store with timestamp instead of FIFO queue
1204+ var legacyBag = legacyClientRegistrations . GetOrAdd ( token , _ => new ConcurrentBag < ( string , DateTime ) > ( ) ) ;
1205+ legacyBag . Add ( ( clientId , now ) ) ;
1206+
1207+ this . LogVerbose ( "Assigning ClientId: {clientId} for token: {token} at {timestamp}" , clientId , token , now ) ;
11461208
11471209 // Construct WebSocket URL with clientId query parameter
11481210 var wsProtocol = "ws" ;
0 commit comments