Skip to content

Commit c13040e

Browse files
authored
fix(functions): prevent collision when listening multiple times to the same stream (#18052)
1 parent 8d715a7 commit c13040e

File tree

2 files changed

+33
-11
lines changed

2 files changed

+33
-11
lines changed

packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,12 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform {
1616
/// Creates a new [MethodChannelHttpsCallable] instance.
1717
MethodChannelHttpsCallable(FirebaseFunctionsPlatform functions,
1818
String? origin, String? name, HttpsCallableOptions options, Uri? uri)
19-
: _transformedUri = uri?.pathSegments.join('_').replaceAll('.', '_'),
20-
super(functions, origin, name, options, uri) {
21-
_eventChannelId = name ?? _transformedUri ?? '';
22-
_channel =
23-
EventChannel('plugins.flutter.io/firebase_functions/$_eventChannelId');
24-
}
19+
: _baseEventChannelId =
20+
name ?? uri?.pathSegments.join('_').replaceAll('.', '_') ?? '',
21+
super(functions, origin, name, options, uri);
2522

26-
late final EventChannel _channel;
27-
final String? _transformedUri;
28-
late String _eventChannelId;
23+
static int _streamIdCounter = 0;
24+
final String _baseEventChannelId;
2925

3026
@override
3127
Future<dynamic> call([Object? parameters]) async {
@@ -54,10 +50,15 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform {
5450

5551
@override
5652
Stream<dynamic> stream(Object? parameters) async* {
53+
// Each stream() call gets a unique channel ID to prevent collisions
54+
// when invoking the same function concurrently. See #18036.
55+
final eventChannelId = '${_baseEventChannelId}_${_streamIdCounter++}';
56+
final channel =
57+
EventChannel('plugins.flutter.io/firebase_functions/$eventChannelId');
5758
try {
5859
await MethodChannelFirebaseFunctions.pigeonChannel
5960
.registerEventChannel(<String, Object>{
60-
'eventChannelId': _eventChannelId,
61+
'eventChannelId': eventChannelId,
6162
'appName': functions.app!.name,
6263
'region': functions.region,
6364
});
@@ -69,7 +70,7 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform {
6970
'limitedUseAppCheckToken': options.limitedUseAppCheckToken,
7071
'timeout': options.timeout.inMilliseconds,
7172
};
72-
yield* _channel.receiveBroadcastStream(eventData).map((message) {
73+
yield* channel.receiveBroadcastStream(eventData).map((message) {
7374
if (message is Map) {
7475
return Map<String, dynamic>.from(message);
7576
}

tests/integration_test/cloud_functions/cloud_functions_e2e_test.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,27 @@ void main() {
346346
await expectLater(stream, emits(isA<StreamResponse>()));
347347
});
348348

349+
test(
350+
'concurrent streams on the same callable do not collide',
351+
() async {
352+
// Regression test for https://github.com/firebase/flutterfire/issues/18036
353+
final stream1 = callable
354+
.stream('foo')
355+
.where((event) => event is Chunk)
356+
.map((event) => (event as Chunk).partialData)
357+
.first;
358+
final stream2 = callable
359+
.stream(123)
360+
.where((event) => event is Chunk)
361+
.map((event) => (event as Chunk).partialData)
362+
.first;
363+
364+
final results = await Future.wait([stream1, stream2]);
365+
expect(results[0], equals('string'));
366+
expect(results[1], equals('number'));
367+
},
368+
);
369+
349370
test('should emit a [Result] as last value', () async {
350371
final stream = await callable.stream().last;
351372
expect(

0 commit comments

Comments
 (0)