@@ -104,8 +104,12 @@ class WebSocketProxyService implements VmServiceInterface {
104
104
/// Active hot reload trackers by request ID.
105
105
final Map <String , _HotReloadTracker > _pendingHotReloads = {};
106
106
107
- /// App connection cleanup subscription.
108
- StreamSubscription <void >? _appConnectionDoneSubscription;
107
+ /// App connection cleanup subscriptions by connection instance ID.
108
+ final Map <String , StreamSubscription <void >> _appConnectionDoneSubscriptions =
109
+ {};
110
+
111
+ /// Active connection count for this service.
112
+ int _activeConnectionCount = 0 ;
109
113
110
114
/// Event stream controllers.
111
115
final Map <String , StreamController <vm_service.Event >> _streamControllers = {};
@@ -153,15 +157,42 @@ class WebSocketProxyService implements VmServiceInterface {
153
157
// Update app connection if override provided
154
158
appConnection = appConnectionOverride ?? appConnection;
155
159
160
+ // Track this connection
161
+ final connectionId = appConnection.request.instanceId;
162
+
163
+ // Check if this connection is already being tracked
164
+ final isNewConnection =
165
+ ! _appConnectionDoneSubscriptions.containsKey (connectionId);
166
+
167
+ if (isNewConnection) {
168
+ _activeConnectionCount++ ;
169
+ _logger.fine (
170
+ 'Adding new connection: $connectionId (total: $_activeConnectionCount )' ,
171
+ );
172
+ } else {
173
+ _logger.fine (
174
+ 'Reconnecting existing connection: $connectionId (total: $_activeConnectionCount )' ,
175
+ );
176
+ }
177
+
156
178
// Auto-cleanup on connection close
157
- await _appConnectionDoneSubscription? .cancel ();
158
- _appConnectionDoneSubscription = appConnection.onDone.asStream ().listen ((
159
- _,
160
- ) {
161
- destroyIsolate ();
162
- });
179
+ final existingSubscription = _appConnectionDoneSubscriptions[connectionId];
180
+ await existingSubscription? .cancel ();
181
+ _appConnectionDoneSubscriptions[connectionId] = appConnection.onDone
182
+ .asStream ()
183
+ .listen ((_) {
184
+ _handleConnectionClosed (connectionId);
185
+ });
186
+
187
+ // If we already have a running isolate, just update the connection and return
188
+ if (_isIsolateRunning && _isolateRef != null ) {
189
+ _logger.fine (
190
+ 'Reusing existing isolate ${_isolateRef !.id } for connection: $connectionId ' ,
191
+ );
192
+ return ;
193
+ }
163
194
164
- // Create isolate reference with unique ID that changes on each page refresh
195
+ // Create isolate reference with unique ID
165
196
final isolateId = '${++_globalIsolateIdCounter }' ;
166
197
final isolateRef = vm_service.IsolateRef (
167
198
id: isolateId,
@@ -175,6 +206,10 @@ class WebSocketProxyService implements VmServiceInterface {
175
206
_vm.isolates? .add (isolateRef);
176
207
final timestamp = DateTime .now ().millisecondsSinceEpoch;
177
208
209
+ _logger.fine (
210
+ 'Created new isolate: $isolateId for connection: $connectionId ' ,
211
+ );
212
+
178
213
// Send lifecycle events
179
214
_streamNotify (
180
215
vm_service.EventStreams .kIsolate,
@@ -208,15 +243,55 @@ class WebSocketProxyService implements VmServiceInterface {
208
243
if (! _initializedCompleter.isCompleted) _initializedCompleter.complete ();
209
244
}
210
245
246
+ /// Handles a connection being closed.
247
+ void _handleConnectionClosed (String connectionId) {
248
+ _logger.fine ('Connection closed: $connectionId ' );
249
+
250
+ // Only decrement if this connection was actually being tracked
251
+ if (_appConnectionDoneSubscriptions.containsKey (connectionId)) {
252
+ // Remove the subscription for this connection
253
+ _appConnectionDoneSubscriptions[connectionId]? .cancel ();
254
+ _appConnectionDoneSubscriptions.remove (connectionId);
255
+
256
+ // Decrease active connection count
257
+ _activeConnectionCount-- ;
258
+ _logger.fine (
259
+ 'Removed connection: $connectionId (remaining: $_activeConnectionCount )' ,
260
+ );
261
+
262
+ // Only destroy the isolate if there are no more active connections
263
+ if (_activeConnectionCount <= 0 ) {
264
+ _logger.fine ('No more active connections, destroying isolate' );
265
+ destroyIsolate ();
266
+ } else {
267
+ _logger.fine (
268
+ 'Still have $_activeConnectionCount active connections, keeping isolate alive' ,
269
+ );
270
+ }
271
+ } else {
272
+ _logger.warning (
273
+ 'Attempted to close connection that was not tracked: $connectionId ' ,
274
+ );
275
+ }
276
+ }
277
+
211
278
/// Destroys the isolate and cleans up state.
212
279
void destroyIsolate () {
213
280
_logger.fine ('Destroying isolate' );
214
- if (! _isIsolateRunning) return ;
281
+
282
+ if (! _isIsolateRunning) {
283
+ _logger.fine ('Isolate already destroyed, ignoring' );
284
+ return ;
285
+ }
215
286
216
287
final isolateRef = _isolateRef;
217
288
218
- _appConnectionDoneSubscription? .cancel ();
219
- _appConnectionDoneSubscription = null ;
289
+ // Cancel all connection subscriptions
290
+ for (final subscription in _appConnectionDoneSubscriptions.values) {
291
+ subscription.cancel ();
292
+ }
293
+ _appConnectionDoneSubscriptions.clear ();
294
+ _activeConnectionCount = 0 ;
220
295
221
296
// Send exit event
222
297
if (isolateRef != null ) {
@@ -391,6 +466,19 @@ class WebSocketProxyService implements VmServiceInterface {
391
466
392
467
Future <vm_service.VM > _getVM () {
393
468
return captureElapsedTime (() async {
469
+ // Ensure the VM's isolate list is synchronized with our actual state
470
+ if (_isIsolateRunning && _isolateRef != null ) {
471
+ // Make sure our isolate is in the VM's isolate list
472
+ final isolateExists =
473
+ _vm.isolates? .any ((ref) => ref.id == _isolateRef! .id) ?? false ;
474
+ if (! isolateExists) {
475
+ _vm.isolates? .add (_isolateRef! );
476
+ }
477
+ } else {
478
+ // If no isolate is running, make sure the list is empty
479
+ _vm.isolates? .clear ();
480
+ }
481
+
394
482
return _vm;
395
483
}, (result) => DwdsEvent .getVM ());
396
484
}
0 commit comments