Skip to content

Commit

Permalink
fix(firestore, web): ensure streams are removed on "hot restart" (#12913
Browse files Browse the repository at this point in the history
)
  • Loading branch information
russellwheatley committed Jun 6, 2024
1 parent a224a02 commit c1a67e5
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Flutter web app</title>
<script src="flutter.js"></script>
</head>
<body>
<script>
{{flutter_build_config}}
_flutter.loader.load();
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ class Firestore extends JsObjectWrapper<firestore_interop.FirestoreJsImpl> {
firestore_interop.enableIndexedDbPersistence(jsObject).toDart;
}

String get _snapshotInSyncWindowsKey =>
'flutterfire-${app.name}_snapshotInSync';

Stream<void> snapshotsInSync() {
unsubscribeWindowsListener(_snapshotInSyncWindowsKey);
late StreamController<void> controller;
late JSFunction onSnapshotsInSyncUnsubscribe;
var nextWrapper = ((JSObject? noValue) {
Expand All @@ -110,11 +114,16 @@ class Firestore extends JsObjectWrapper<firestore_interop.FirestoreJsImpl> {
void startListen() {
onSnapshotsInSyncUnsubscribe =
firestore_interop.onSnapshotsInSync(jsObject, nextWrapper);
setWindowsListener(
_snapshotInSyncWindowsKey,
onSnapshotsInSyncUnsubscribe,
);
}

void stopListen() {
onSnapshotsInSyncUnsubscribe.callAsFunction();
controller.close();
removeWindowsListener(_snapshotInSyncWindowsKey);
}

controller = StreamController<void>.broadcast(
Expand Down Expand Up @@ -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<DocumentSnapshot> onSnapshot({
bool includeMetadataChanges = false,
Expand All @@ -379,8 +391,9 @@ class DocumentReference
StreamController<DocumentSnapshot> _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<DocumentSnapshot> controller;

final nextWrapper = ((firestore_interop.DocumentSnapshotJsImpl snapshot) {
Expand All @@ -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<DocumentSnapshot>.broadcast(
Expand Down Expand Up @@ -471,20 +486,26 @@ class Query<T extends firestore_interop.QueryJsImpl>
Query limitToLast(num limit) => Query.fromJsObject(firestore_interop.query(
jsObject, firestore_interop.limitToLast(limit.toJS)));

Stream<QuerySnapshot> onSnapshot({
bool includeMetadataChanges = false,
ListenSource source = ListenSource.defaultSource,
}) =>
String _querySnapshotWindowsKey(hashCode) =>
'flutterfire-${firestore.app.name}_${hashCode}_querySnapshot';

Stream<QuerySnapshot> onSnapshot(
{bool includeMetadataChanges = false,
ListenSource source = ListenSource.defaultSource,
required int hashCode}) =>
_createSnapshotStream(
firestore_interop.DocumentListenOptions(
includeMetadataChanges: includeMetadataChanges.toJS,
source: convertListenSource(source),
),
hashCode,
).stream;

StreamController<QuerySnapshot> _createSnapshotStream(
firestore_interop.DocumentListenOptions options,
int hashCode,
) {
unsubscribeWindowsListener(_querySnapshotWindowsKey(hashCode));
late JSFunction onSnapshotUnsubscribe;
// ignore: close_sinks, the controller is returned
late StreamController<QuerySnapshot> controller;
Expand All @@ -497,10 +518,13 @@ class Query<T extends firestore_interop.QueryJsImpl>
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<QuerySnapshot>.broadcast(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ class QueryWeb extends QueryPlatform {
_buildWebQueryWithParameters().onSnapshot(
includeMetadataChanges: includeMetadataChanges,
source: source,
hashCode: hashCode,
);

return convertWebExceptions(
Expand Down
44 changes: 9 additions & 35 deletions packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -395,32 +392,9 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
// ignore: close_sinks
StreamController<User?>? _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.
///
Expand All @@ -429,7 +403,7 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
///
/// If the value is `null`, there is no signed-in user.
Stream<User?> get onAuthStateChanged {
_unsubscribeWindowsListener(authStateWindowsKey);
unsubscribeWindowsListener(_authStateWindowsKey);

if (_changeController == null) {
final nextWrapper = (auth_interop.UserJsImpl? user) {
Expand All @@ -443,14 +417,14 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
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<User?>.broadcast(
Expand All @@ -476,7 +450,7 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
///
/// If the value is `null`, there is no signed-in user.
Stream<User?> get onIdTokenChanged {
_unsubscribeWindowsListener(idTokenStateWindowsKey);
unsubscribeWindowsListener(_idTokenStateWindowsKey);
if (_idTokenChangedController == null) {
final nextWrapper = (auth_interop.UserJsImpl? user) {
_idTokenChangedController!.add(User.getInstance(user));
Expand All @@ -489,14 +463,14 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
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<User?>.broadcast(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -38,3 +40,29 @@ JSPromise handleFutureWithMapper<T, S>(
});
}.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);
}
}
}
2 changes: 1 addition & 1 deletion packages/firebase_core/firebase_core_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c1a67e5

Please sign in to comment.