Skip to content

Commit d0e298c

Browse files
committed
DTDManager handles the subscription to the service stream
1 parent 4ddd14e commit d0e298c

6 files changed

Lines changed: 91 additions & 31 deletions

File tree

packages/devtools_app/lib/src/shared/editor/editor_client.dart

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:async';
66

7+
import 'package:devtools_app_shared/service.dart';
78
import 'package:devtools_app_shared/utils.dart';
89
import 'package:devtools_shared/devtools_shared.dart';
910
import 'package:dtd/dtd.dart';
@@ -20,32 +21,28 @@ import 'api_classes.dart';
2021
/// ensure they are not breaking changes to already-shipped editors.
2122
class EditorClient extends DisposableController
2223
with AutoDisposeControllerMixin {
23-
EditorClient(this._dtd) {
24+
EditorClient(this._dtdManager) {
2425
unawaited(initialized); // Trigger async initialization.
2526
}
2627

27-
final DartToolingDaemon _dtd;
28+
final DTDManager _dtdManager;
2829
late final initialized = _initialize();
2930

31+
DartToolingDaemon get _dtd => _dtdManager.connection.value!;
32+
3033
String get gaId => EditorSidebar.id;
3134

3235
Future<void> _initialize() async {
3336
autoDisposeStreamSubscription(
34-
_dtd.onEvent(CoreDtdServiceConstants.servicesStreamId).listen((data) {
35-
final kind = data.kind;
36-
if (kind != CoreDtdServiceConstants.serviceRegisteredKind &&
37-
kind != CoreDtdServiceConstants.serviceUnregisteredKind) {
38-
return;
39-
}
40-
37+
_dtdManager.serviceRegistrationBroadcastStream.listen((data) {
4138
final service = data.data[DtdParameters.service] as String?;
4239
if (service == null ||
4340
(service != editorServiceName && service != lspServiceName)) {
4441
return;
4542
}
4643

4744
final isRegistered =
48-
kind == CoreDtdServiceConstants.serviceRegisteredKind;
45+
data.kind == CoreDtdServiceConstants.serviceRegisteredKind;
4946
final method = data.data[DtdParameters.method] as String;
5047
final capabilities =
5148
data.data[DtdParameters.capabilities] as Map<String, Object?>?;
@@ -127,7 +124,6 @@ class EditorClient extends DisposableController
127124
}),
128125
);
129126
await [
130-
_dtd.streamListen(CoreDtdServiceConstants.servicesStreamId),
131127
_dtd.streamListen(editorStreamName).catchError((_) {
132128
// Because we currently call streamListen in two places (here and
133129
// ThemeManager) this can fail. It doesn't matter if this happens,

packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
import 'dart:async';
66

7+
import 'package:devtools_app_shared/service.dart';
78
import 'package:devtools_app_shared/ui.dart';
89
import 'package:devtools_app_shared/utils.dart';
9-
import 'package:dtd/dtd.dart';
1010
import 'package:flutter/material.dart';
1111

1212
import '../../../framework/scaffold/report_feedback_button.dart';
@@ -20,9 +20,9 @@ import 'property_editor_view.dart';
2020

2121
/// The side panel for the Property Editor.
2222
class PropertyEditorPanel extends StatefulWidget {
23-
const PropertyEditorPanel(this.dtd, {super.key});
23+
const PropertyEditorPanel(this.dtdManager, {super.key});
2424

25-
final DartToolingDaemon dtd;
25+
final DTDManager dtdManager;
2626

2727
@override
2828
State<PropertyEditorPanel> createState() => _PropertyEditorPanelState();
@@ -38,7 +38,7 @@ class _PropertyEditorPanelState extends State<PropertyEditorPanel> {
3838
void initState() {
3939
super.initState();
4040

41-
final editor = EditorClient(widget.dtd);
41+
final editor = EditorClient(widget.dtdManager);
4242
ga.screen(gac.PropertyEditorSidebar.id);
4343
unawaited(
4444
_editor = editor.initialized.then((_) {

packages/devtools_app/lib/src/standalone_ui/standalone_screen.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ enum StandaloneScreenType {
3737
),
3838
StandaloneScreenType.editorSidebar => _DtdConnectedScreen(
3939
dtdManager: dtdManager,
40-
builder: (dtd) => EditorSidebarPanel(dtd),
40+
builder: (dtdManager) => EditorSidebarPanel(dtdManager),
4141
),
4242
StandaloneScreenType.propertyEditor => _DtdConnectedScreen(
4343
dtdManager: dtdManager,
44-
builder: (dtd) => PropertyEditorPanel(dtd),
44+
builder: (dtdManager) => PropertyEditorPanel(dtdManager),
4545
),
4646
};
4747
}
@@ -58,7 +58,7 @@ class _DtdConnectedScreen extends StatelessWidget {
5858
const _DtdConnectedScreen({required this.dtdManager, required this.builder});
5959

6060
final DTDManager dtdManager;
61-
final Widget Function(DartToolingDaemon) builder;
61+
final Widget Function(DTDManager) builder;
6262

6363
@override
6464
Widget build(BuildContext context) {
@@ -80,7 +80,7 @@ class _DtdConnectedScreen extends StatelessWidget {
8080
// reconnect occurs.
8181
KeyedSubtree(
8282
key: ValueKey(connection),
83-
child: builder(connection),
83+
child: builder(dtdManager),
8484
),
8585
if (connectionState is! ConnectedDTDState)
8686
NotConnectedOverlay(connectionState),

packages/devtools_app/lib/src/standalone_ui/vs_code/flutter_panel.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
import 'dart:async';
66

7+
import 'package:devtools_app_shared/service.dart';
78
import 'package:devtools_app_shared/ui.dart';
89
import 'package:devtools_app_shared/utils.dart';
9-
import 'package:dtd/dtd.dart';
1010
import 'package:flutter/material.dart';
1111

1212
import '../../shared/analytics/analytics.dart' as ga;
@@ -22,9 +22,9 @@ import 'devtools/devtools_view.dart';
2222
/// Provides some basic functionality to improve discoverability of features
2323
/// such as creation of new projects, device selection and DevTools features.
2424
class EditorSidebarPanel extends StatefulWidget {
25-
const EditorSidebarPanel(this.dtd, {super.key});
25+
const EditorSidebarPanel(this.dtdManager, {super.key});
2626

27-
final DartToolingDaemon dtd;
27+
final DTDManager dtdManager;
2828

2929
@override
3030
State<EditorSidebarPanel> createState() => _EditorSidebarPanelState();
@@ -39,7 +39,7 @@ class _EditorSidebarPanelState extends State<EditorSidebarPanel> {
3939
void initState() {
4040
super.initState();
4141

42-
final editor = EditorClient(widget.dtd);
42+
final editor = EditorClient(widget.dtdManager);
4343
ga.screen(editor.gaId);
4444
unawaited(_editor = editor.initialized.then((_) => editor));
4545
}

packages/devtools_app/test/shared/editor/editor_client_test.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import 'package:devtools_app/src/shared/editor/api_classes.dart';
88
import 'package:devtools_app/src/shared/editor/editor_client.dart';
99
import 'package:devtools_test/devtools_test.dart';
1010
import 'package:dtd/dtd.dart';
11+
import 'package:flutter/foundation.dart';
1112
import 'package:flutter_test/flutter_test.dart';
1213
import 'package:mockito/mockito.dart';
1314

1415
void main() {
16+
late MockDTDManager mockDTDManager;
1517
late MockDartToolingDaemon mockDtd;
1618
late EditorClient editorClient;
1719

@@ -23,7 +25,9 @@ void main() {
2325
};
2426

2527
setUp(() {
28+
mockDTDManager = MockDTDManager();
2629
mockDtd = MockDartToolingDaemon();
30+
when(mockDTDManager.connection).thenReturn(ValueNotifier(mockDtd));
2731

2832
for (final MapEntry(key: method, value: responseJson)
2933
in methodToResponseJson.entries) {
@@ -40,7 +44,7 @@ void main() {
4044
method.isRegistered = true;
4145
}
4246

43-
editorClient = EditorClient(mockDtd);
47+
editorClient = EditorClient(mockDTDManager);
4448
});
4549

4650
group('getRefactors', () {

packages/devtools_app_shared/lib/src/service/dtd_manager.dart

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ class DTDManager {
3232
Uri? get uri => _uri;
3333
Uri? _uri;
3434

35+
/// A stream of [CoreDtdServiceConstants.serviceRegisteredKind] and
36+
/// [CoreDtdServiceConstants.serviceUnregisteredKind] events.
37+
///
38+
/// Since this is a broadcast stream, it supports multiple subscribers.
39+
Stream<DTDEvent> get serviceRegistrationBroadcastStream =>
40+
_serviceRegistrationController.stream;
41+
final _serviceRegistrationController = StreamController<DTDEvent>.broadcast();
42+
43+
/// The subscription to the current service registration stream.
44+
///
45+
/// This is canceled and reset with the DTD connection changes.
46+
StreamSubscription<DTDEvent>? _currentServiceRegistrationSubscription;
47+
3548
/// Whether or not to automatically reconnect if disconnected.
3649
///
3750
/// This will happen by default as long as the disconnect wasn't
@@ -74,7 +87,7 @@ class DTDManager {
7487
}
7588

7689
/// Triggers a reconnect to the last connected URI if the current state is
77-
/// [ConnectionFailedDTDState] (and there was a pervious connection).
90+
/// [ConnectionFailedDTDState] (and there was a previous connection).
7891
Future<void> reconnect() {
7992
final reconnectFunc = _lastConnectFunc;
8093
if (_connectionState.value is! ConnectionFailedDTDState ||
@@ -149,11 +162,19 @@ class DTDManager {
149162

150163
try {
151164
final connection = await _connectWithRetries(uri, maxRetries: maxRetries);
165+
await _listenForServiceRegistrationEvents(connection);
152166

167+
// Save the previous connection so that we can close it after the new
168+
// connection is reestablished.
169+
final previousConnection = _connection.value;
153170
_uri = uri;
154171
// Set this after setting the value of [_uri] so that [_uri] can be used
155172
// by any listeners of the [_connection] notifier.
156173
_connection.value = connection;
174+
// Close the previous connection.
175+
if (previousConnection != null) {
176+
await previousConnection.close();
177+
}
157178
_connectionState.value = ConnectedDTDState();
158179
_log.info('Successfully connected to DTD at: $uri');
159180

@@ -230,16 +251,17 @@ class DTDManager {
230251
// an explicit disconnect.
231252
_automaticallyReconnect = false;
232253

233-
// We only clear the connection if we are explicitly disconnecting. In the
234-
// case where the connection just dropped, we leave it so that we can
235-
// continue to render a page (usually with an overlay).
254+
// We only close and clear the connection if we are explicitly
255+
// disconnecting. In the case where the connection just dropped, we leave
256+
// it so that we can continue to render a page (usually with an overlay).
257+
// We only close it once the new connection is established.
258+
if (_connection.value case final connection?) {
259+
await connection.close();
260+
}
236261
_connection.value = null;
237262
}
238263

239264
_periodicConnectionCheck?.cancel();
240-
if (_connection.value case final connection?) {
241-
await connection.close();
242-
}
243265

244266
_connectionState.value = NotConnectedDTDState();
245267
_uri = null;
@@ -249,9 +271,47 @@ class DTDManager {
249271

250272
Future<void> dispose() async {
251273
await disconnect();
274+
await _currentServiceRegistrationSubscription?.cancel();
275+
await _serviceRegistrationController.close();
252276
_connection.dispose();
253277
}
254278

279+
/// Listens for service registration events on the [dtd] connection.
280+
Future<void> _listenForServiceRegistrationEvents(
281+
DartToolingDaemon dtd) async {
282+
// Note: We immediately begin listening for service registration events on
283+
// on the new DTD connection before canceling the previous subscription.
284+
// This guarantees that we don't miss any events across reconnects.
285+
// ignore: cancel_subscriptions, false positive, it is canceled below.
286+
final nextServiceRegistrationSubscription = dtd
287+
.onEvent(CoreDtdServiceConstants.servicesStreamId)
288+
.listen(_forwardServiceRegistrationEvents,
289+
onError: _logServiceStreamError);
290+
await dtd.streamListen(CoreDtdServiceConstants.servicesStreamId);
291+
292+
// Cancel the previous subscription.
293+
await _currentServiceRegistrationSubscription?.cancel();
294+
_currentServiceRegistrationSubscription =
295+
nextServiceRegistrationSubscription;
296+
}
297+
298+
/// Forwards service registration events to the
299+
/// [_serviceRegistrationController].
300+
void _forwardServiceRegistrationEvents(DTDEvent event) {
301+
final kind = event.kind;
302+
final isRegistrationEvent =
303+
kind == CoreDtdServiceConstants.serviceRegisteredKind ||
304+
kind == CoreDtdServiceConstants.serviceUnregisteredKind;
305+
306+
if (isRegistrationEvent) {
307+
_serviceRegistrationController.add(event);
308+
}
309+
}
310+
311+
void _logServiceStreamError(Object error) {
312+
_log.warning('Error in DTD service stream', error);
313+
}
314+
255315
/// Returns the workspace roots for the Dart Tooling Daemon connection.
256316
///
257317
/// These roots are set by the tool that started DTD, which may be the IDE,

0 commit comments

Comments
 (0)