diff --git a/modules/ensemble/lib/framework/apiproviders/api_provider.dart b/modules/ensemble/lib/framework/apiproviders/api_provider.dart index f17867d9e..2dd658927 100644 --- a/modules/ensemble/lib/framework/apiproviders/api_provider.dart +++ b/modules/ensemble/lib/framework/apiproviders/api_provider.dart @@ -31,7 +31,7 @@ class APIProviders extends InheritedWidget { } else if (provider == 'firebaseFunction') { return FirebaseFunctionsAPIProvider(); } else if (provider == 'sse') { - return SSEAPIProvider(); + return providers[provider] ?? SSEAPIProvider(); } else { return providers[provider] ?? httpProvider; } diff --git a/modules/ensemble/lib/framework/apiproviders/sse_api_provider.dart b/modules/ensemble/lib/framework/apiproviders/sse_api_provider.dart index e06fe4c22..33df0d418 100644 --- a/modules/ensemble/lib/framework/apiproviders/sse_api_provider.dart +++ b/modules/ensemble/lib/framework/apiproviders/sse_api_provider.dart @@ -24,9 +24,9 @@ import 'package:crypto/crypto.dart'; /// Server-Sent Events (SSE) API Provider /// Handles streaming HTTP connections class SSEAPIProvider extends APIProvider with LiveAPIProvider { - static final Map _subscriptions = {}; - static final Map _activeClients = {}; - static final Set _manuallyDisconnected = {}; + final Map _subscriptions = {}; + final Map _activeClients = {}; + final Set _manuallyDisconnected = {}; @override Future init(String appId, Map config) async { @@ -530,22 +530,30 @@ class SSEAPIProvider extends APIProvider with LiveAPIProvider { @override dispose() { - _manuallyDisconnected.addAll(_subscriptions.keys); + for (final apiName in _subscriptions.keys.toList()) { + _manuallyDisconnected.add(apiName); + } - // Cancel all subscriptions - for (var subscription in _subscriptions.values) { + for (final subscription in _subscriptions.values) { subscription.cancel(); } _subscriptions.clear(); - // Close all active clients - for (var client in _activeClients.values) { + for (final client in _activeClients.values) { client.close(); } _activeClients.clear(); _manuallyDisconnected.clear(); } + + @visibleForTesting + int get subscriptionCountForTesting => _subscriptions.length; + + @visibleForTesting + void trackSubscriptionForTesting(String apiName, StreamSubscription sub) { + _subscriptions[apiName] = sub; + } } /// Configuration options for SSE connections diff --git a/modules/ensemble/test/sse_provider_dispose_test.dart b/modules/ensemble/test/sse_provider_dispose_test.dart new file mode 100644 index 000000000..e1201f786 --- /dev/null +++ b/modules/ensemble/test/sse_provider_dispose_test.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:ensemble/framework/apiproviders/api_provider.dart'; +import 'package:ensemble/framework/apiproviders/http_api_provider.dart'; +import 'package:ensemble/framework/apiproviders/sse_api_provider.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SSEAPIProvider.dispose', () { + test('cancels only this instance subscriptions', () async { + final providerA = SSEAPIProvider(); + final providerB = SSEAPIProvider(); + + var aCanceled = false; + var bCanceled = false; + final controllerA = StreamController(onCancel: () => aCanceled = true); + final controllerB = StreamController(onCancel: () => bCanceled = true); + + providerA.trackSubscriptionForTesting( + 'liveFeed', + controllerA.stream.listen((_) {}), + ); + providerB.trackSubscriptionForTesting( + 'metrics', + controllerB.stream.listen((_) {}), + ); + + providerA.dispose(); + + expect(providerA.subscriptionCountForTesting, 0); + expect(providerB.subscriptionCountForTesting, 1); + expect(aCanceled, isTrue); + expect(bCanceled, isFalse); + + await controllerA.close(); + await controllerB.close(); + }); + }); + + group('APIProviders.getProvider', () { + test('returns cloned sse provider from screen map', () { + final sse = SSEAPIProvider(); + final providers = APIProviders( + providers: {'sse': sse, 'http': HTTPAPIProvider()}, + child: const SizedBox.shrink(), + ); + + expect(identical(providers.getProvider('sse'), sse), isTrue); + }); + }); +}