From c1a67e54894cbfb316b3445505b5803e2d041ed5 Mon Sep 17 00:00:00 2001 From: Russell Wheatley Date: Thu, 6 Jun 2024 14:27:36 +0100 Subject: [PATCH] fix(firestore, web): ensure streams are removed on "hot restart" (#12913) --- .../example/web/wasm_index.html | 14 ++++++ .../lib/src/interop/firestore.dart | 34 +++++++++++--- .../lib/src/query_web.dart | 1 + .../lib/src/interop/auth.dart | 44 ++++--------------- .../lib/src/interop/utils/utils.dart | 28 ++++++++++++ .../firebase_core_web/pubspec.yaml | 2 +- 6 files changed, 82 insertions(+), 41 deletions(-) create mode 100644 packages/cloud_firestore/cloud_firestore/example/web/wasm_index.html diff --git a/packages/cloud_firestore/cloud_firestore/example/web/wasm_index.html b/packages/cloud_firestore/cloud_firestore/example/web/wasm_index.html new file mode 100644 index 000000000000..6123d2822915 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/example/web/wasm_index.html @@ -0,0 +1,14 @@ + + + + + Flutter web app + + + + + + \ No newline at end of file diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart index 45f787ca0206..072492e5e1d5 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart @@ -100,7 +100,11 @@ class Firestore extends JsObjectWrapper { firestore_interop.enableIndexedDbPersistence(jsObject).toDart; } + String get _snapshotInSyncWindowsKey => + 'flutterfire-${app.name}_snapshotInSync'; + Stream snapshotsInSync() { + unsubscribeWindowsListener(_snapshotInSyncWindowsKey); late StreamController controller; late JSFunction onSnapshotsInSyncUnsubscribe; var nextWrapper = ((JSObject? noValue) { @@ -110,11 +114,16 @@ class Firestore extends JsObjectWrapper { void startListen() { onSnapshotsInSyncUnsubscribe = firestore_interop.onSnapshotsInSync(jsObject, nextWrapper); + setWindowsListener( + _snapshotInSyncWindowsKey, + onSnapshotsInSyncUnsubscribe, + ); } void stopListen() { onSnapshotsInSyncUnsubscribe.callAsFunction(); controller.close(); + removeWindowsListener(_snapshotInSyncWindowsKey); } controller = StreamController.broadcast( @@ -364,6 +373,9 @@ class DocumentReference (result)! as firestore_interop.DocumentSnapshotJsImpl); } + String get _documentSnapshotWindowsKey => + 'flutterfire-${firestore.app.name}_${path}_documentSnapshot'; + /// Attaches a listener for [DocumentSnapshot] events. Stream onSnapshot({ bool includeMetadataChanges = false, @@ -379,8 +391,9 @@ class DocumentReference StreamController _createSnapshotStream([ firestore_interop.DocumentListenOptions? options, ]) { + unsubscribeWindowsListener(_documentSnapshotWindowsKey); late JSFunction onSnapshotUnsubscribe; - // ignore: close_sinks, the controler is returned + // ignore: close_sinks, the controller is returned late StreamController controller; final nextWrapper = ((firestore_interop.DocumentSnapshotJsImpl snapshot) { @@ -395,10 +408,12 @@ class DocumentReference jsObject as JSObject, options as JSAny, nextWrapper, errorWrapper) : firestore_interop.onSnapshot( jsObject as JSObject, nextWrapper, errorWrapper); + setWindowsListener(_documentSnapshotWindowsKey, onSnapshotUnsubscribe); } void stopListen() { onSnapshotUnsubscribe.callAsFunction(); + removeWindowsListener(_documentSnapshotWindowsKey); } return controller = StreamController.broadcast( @@ -471,20 +486,26 @@ class Query Query limitToLast(num limit) => Query.fromJsObject(firestore_interop.query( jsObject, firestore_interop.limitToLast(limit.toJS))); - Stream onSnapshot({ - bool includeMetadataChanges = false, - ListenSource source = ListenSource.defaultSource, - }) => + String _querySnapshotWindowsKey(hashCode) => + 'flutterfire-${firestore.app.name}_${hashCode}_querySnapshot'; + + Stream onSnapshot( + {bool includeMetadataChanges = false, + ListenSource source = ListenSource.defaultSource, + required int hashCode}) => _createSnapshotStream( firestore_interop.DocumentListenOptions( includeMetadataChanges: includeMetadataChanges.toJS, source: convertListenSource(source), ), + hashCode, ).stream; StreamController _createSnapshotStream( firestore_interop.DocumentListenOptions options, + int hashCode, ) { + unsubscribeWindowsListener(_querySnapshotWindowsKey(hashCode)); late JSFunction onSnapshotUnsubscribe; // ignore: close_sinks, the controller is returned late StreamController controller; @@ -497,10 +518,13 @@ class Query void startListen() { onSnapshotUnsubscribe = firestore_interop.onSnapshot( jsObject as JSObject, options as JSObject, nextWrapper, errorWrapper); + setWindowsListener( + _querySnapshotWindowsKey(hashCode), onSnapshotUnsubscribe); } void stopListen() { onSnapshotUnsubscribe.callAsFunction(); + removeWindowsListener(_querySnapshotWindowsKey(hashCode)); } return controller = StreamController.broadcast( diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart index 219367ada60b..e7dd0dbbe8d9 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart @@ -188,6 +188,7 @@ class QueryWeb extends QueryPlatform { _buildWebQueryWithParameters().onSnapshot( includeMetadataChanges: includeMetadataChanges, source: source, + hashCode: hashCode, ); return convertWebExceptions( diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart index cca1cfb5290d..234a2566524d 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart @@ -8,9 +8,6 @@ import 'dart:async'; import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; -import 'package:flutter/foundation.dart'; -import 'package:web/web.dart' as web; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:firebase_core_web/firebase_core_web_interop.dart'; import 'package:http_parser/http_parser.dart'; @@ -395,32 +392,9 @@ class Auth extends JsObjectWrapper { // ignore: close_sinks StreamController? _changeController; - String get authStateWindowsKey => 'flutterfire-${app.name}_authStateChanges'; - String get idTokenStateWindowsKey => 'flutterfire-${app.name}_idTokenChanges'; - - // No way to unsubscribe from event listeners on hot reload so we set on the windows object - // and clean up on hot restart if it exists. - // See: https://github.com/firebase/flutterfire/issues/7064 - void _unsubscribeWindowsListener(String key) { - if (kDebugMode) { - final unsubscribe = web.window.getProperty(key.toJS); - if (unsubscribe != null) { - (unsubscribe as JSFunction).callAsFunction(); - } - } - } - - void _setWindowsListener(String key, JSFunction unsubscribe) { - if (kDebugMode) { - web.window.setProperty(key.toJS, unsubscribe); - } - } - - void _removeWindowsListener(String key) { - if (kDebugMode) { - web.window.delete(key.toJS); - } - } + String get _authStateWindowsKey => 'flutterfire-${app.name}_authStateChanges'; + String get _idTokenStateWindowsKey => + 'flutterfire-${app.name}_idTokenChanges'; /// Sends events when the users sign-in state changes. /// @@ -429,7 +403,7 @@ class Auth extends JsObjectWrapper { /// /// If the value is `null`, there is no signed-in user. Stream get onAuthStateChanged { - _unsubscribeWindowsListener(authStateWindowsKey); + unsubscribeWindowsListener(_authStateWindowsKey); if (_changeController == null) { final nextWrapper = (auth_interop.UserJsImpl? user) { @@ -443,14 +417,14 @@ class Auth extends JsObjectWrapper { final unsubscribe = jsObject.onAuthStateChanged(nextWrapper.toJS, errorWrapper.toJS); _onAuthUnsubscribe = unsubscribe; - _setWindowsListener(authStateWindowsKey, unsubscribe); + setWindowsListener(_authStateWindowsKey, unsubscribe); } void stopListen() { _onAuthUnsubscribe!.callAsFunction(); _onAuthUnsubscribe = null; _changeController = null; - _removeWindowsListener(authStateWindowsKey); + removeWindowsListener(_authStateWindowsKey); } _changeController = StreamController.broadcast( @@ -476,7 +450,7 @@ class Auth extends JsObjectWrapper { /// /// If the value is `null`, there is no signed-in user. Stream get onIdTokenChanged { - _unsubscribeWindowsListener(idTokenStateWindowsKey); + unsubscribeWindowsListener(_idTokenStateWindowsKey); if (_idTokenChangedController == null) { final nextWrapper = (auth_interop.UserJsImpl? user) { _idTokenChangedController!.add(User.getInstance(user)); @@ -489,14 +463,14 @@ class Auth extends JsObjectWrapper { final unsubscribe = jsObject.onIdTokenChanged(nextWrapper.toJS, errorWrapper.toJS); _onIdTokenChangedUnsubscribe = unsubscribe; - _setWindowsListener(idTokenStateWindowsKey, unsubscribe); + setWindowsListener(_idTokenStateWindowsKey, unsubscribe); } void stopListen() { _onIdTokenChangedUnsubscribe!.callAsFunction(); _onIdTokenChangedUnsubscribe = null; _idTokenChangedController = null; - _removeWindowsListener(idTokenStateWindowsKey); + removeWindowsListener(_idTokenStateWindowsKey); } _idTokenChangedController = StreamController.broadcast( diff --git a/packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart b/packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart index fec7fd876142..ccc1fefe1b18 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart @@ -10,6 +10,8 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; +import 'package:web/web.dart' as web; +import 'package:flutter/foundation.dart'; import 'func.dart'; @@ -38,3 +40,29 @@ JSPromise handleFutureWithMapper( }); }.toJS); } + +// No way to unsubscribe from event listeners on hot reload so we set on the windows object +// and clean up on hot restart if it exists. +// See: https://github.com/firebase/flutterfire/issues/7064 +void unsubscribeWindowsListener(String key) { + if (kDebugMode) { + final unsubscribe = web.window.getProperty(key.toJS); + if (unsubscribe != null) { + (unsubscribe as JSFunction).callAsFunction(); + } + } +} + +void setWindowsListener(String key, JSFunction unsubscribe) { + if (kDebugMode) { + web.window.setProperty(key.toJS, unsubscribe); + } +} + +void removeWindowsListener(String key) { + if (kDebugMode) { + if (web.window.hasProperty(key.toJS) == true.toJS) { + web.window.delete(key.toJS); + } + } +} diff --git a/packages/firebase_core/firebase_core_web/pubspec.yaml b/packages/firebase_core/firebase_core_web/pubspec.yaml index 034f2966a4fd..374453458a6d 100644 --- a/packages/firebase_core/firebase_core_web/pubspec.yaml +++ b/packages/firebase_core/firebase_core_web/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.17.1 environment: sdk: '>=3.2.0 <4.0.0' - flutter: '>=3.3.0' + flutter: '>=3.16.0' dependencies: firebase_core_platform_interface: ^5.0.0