Skip to content

Commit c1a67e5

Browse files
fix(firestore, web): ensure streams are removed on "hot restart" (#12913)
1 parent a224a02 commit c1a67e5

File tree

6 files changed

+82
-41
lines changed

6 files changed

+82
-41
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE HTML>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Flutter web app</title>
6+
<script src="flutter.js"></script>
7+
</head>
8+
<body>
9+
<script>
10+
{{flutter_build_config}}
11+
_flutter.loader.load();
12+
</script>
13+
</body>
14+
</html>

packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ class Firestore extends JsObjectWrapper<firestore_interop.FirestoreJsImpl> {
100100
firestore_interop.enableIndexedDbPersistence(jsObject).toDart;
101101
}
102102

103+
String get _snapshotInSyncWindowsKey =>
104+
'flutterfire-${app.name}_snapshotInSync';
105+
103106
Stream<void> snapshotsInSync() {
107+
unsubscribeWindowsListener(_snapshotInSyncWindowsKey);
104108
late StreamController<void> controller;
105109
late JSFunction onSnapshotsInSyncUnsubscribe;
106110
var nextWrapper = ((JSObject? noValue) {
@@ -110,11 +114,16 @@ class Firestore extends JsObjectWrapper<firestore_interop.FirestoreJsImpl> {
110114
void startListen() {
111115
onSnapshotsInSyncUnsubscribe =
112116
firestore_interop.onSnapshotsInSync(jsObject, nextWrapper);
117+
setWindowsListener(
118+
_snapshotInSyncWindowsKey,
119+
onSnapshotsInSyncUnsubscribe,
120+
);
113121
}
114122

115123
void stopListen() {
116124
onSnapshotsInSyncUnsubscribe.callAsFunction();
117125
controller.close();
126+
removeWindowsListener(_snapshotInSyncWindowsKey);
118127
}
119128

120129
controller = StreamController<void>.broadcast(
@@ -364,6 +373,9 @@ class DocumentReference
364373
(result)! as firestore_interop.DocumentSnapshotJsImpl);
365374
}
366375

376+
String get _documentSnapshotWindowsKey =>
377+
'flutterfire-${firestore.app.name}_${path}_documentSnapshot';
378+
367379
/// Attaches a listener for [DocumentSnapshot] events.
368380
Stream<DocumentSnapshot> onSnapshot({
369381
bool includeMetadataChanges = false,
@@ -379,8 +391,9 @@ class DocumentReference
379391
StreamController<DocumentSnapshot> _createSnapshotStream([
380392
firestore_interop.DocumentListenOptions? options,
381393
]) {
394+
unsubscribeWindowsListener(_documentSnapshotWindowsKey);
382395
late JSFunction onSnapshotUnsubscribe;
383-
// ignore: close_sinks, the controler is returned
396+
// ignore: close_sinks, the controller is returned
384397
late StreamController<DocumentSnapshot> controller;
385398

386399
final nextWrapper = ((firestore_interop.DocumentSnapshotJsImpl snapshot) {
@@ -395,10 +408,12 @@ class DocumentReference
395408
jsObject as JSObject, options as JSAny, nextWrapper, errorWrapper)
396409
: firestore_interop.onSnapshot(
397410
jsObject as JSObject, nextWrapper, errorWrapper);
411+
setWindowsListener(_documentSnapshotWindowsKey, onSnapshotUnsubscribe);
398412
}
399413

400414
void stopListen() {
401415
onSnapshotUnsubscribe.callAsFunction();
416+
removeWindowsListener(_documentSnapshotWindowsKey);
402417
}
403418

404419
return controller = StreamController<DocumentSnapshot>.broadcast(
@@ -471,20 +486,26 @@ class Query<T extends firestore_interop.QueryJsImpl>
471486
Query limitToLast(num limit) => Query.fromJsObject(firestore_interop.query(
472487
jsObject, firestore_interop.limitToLast(limit.toJS)));
473488

474-
Stream<QuerySnapshot> onSnapshot({
475-
bool includeMetadataChanges = false,
476-
ListenSource source = ListenSource.defaultSource,
477-
}) =>
489+
String _querySnapshotWindowsKey(hashCode) =>
490+
'flutterfire-${firestore.app.name}_${hashCode}_querySnapshot';
491+
492+
Stream<QuerySnapshot> onSnapshot(
493+
{bool includeMetadataChanges = false,
494+
ListenSource source = ListenSource.defaultSource,
495+
required int hashCode}) =>
478496
_createSnapshotStream(
479497
firestore_interop.DocumentListenOptions(
480498
includeMetadataChanges: includeMetadataChanges.toJS,
481499
source: convertListenSource(source),
482500
),
501+
hashCode,
483502
).stream;
484503

485504
StreamController<QuerySnapshot> _createSnapshotStream(
486505
firestore_interop.DocumentListenOptions options,
506+
int hashCode,
487507
) {
508+
unsubscribeWindowsListener(_querySnapshotWindowsKey(hashCode));
488509
late JSFunction onSnapshotUnsubscribe;
489510
// ignore: close_sinks, the controller is returned
490511
late StreamController<QuerySnapshot> controller;
@@ -497,10 +518,13 @@ class Query<T extends firestore_interop.QueryJsImpl>
497518
void startListen() {
498519
onSnapshotUnsubscribe = firestore_interop.onSnapshot(
499520
jsObject as JSObject, options as JSObject, nextWrapper, errorWrapper);
521+
setWindowsListener(
522+
_querySnapshotWindowsKey(hashCode), onSnapshotUnsubscribe);
500523
}
501524

502525
void stopListen() {
503526
onSnapshotUnsubscribe.callAsFunction();
527+
removeWindowsListener(_querySnapshotWindowsKey(hashCode));
504528
}
505529

506530
return controller = StreamController<QuerySnapshot>.broadcast(

packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class QueryWeb extends QueryPlatform {
188188
_buildWebQueryWithParameters().onSnapshot(
189189
includeMetadataChanges: includeMetadataChanges,
190190
source: source,
191+
hashCode: hashCode,
191192
);
192193

193194
return convertWebExceptions(

packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88

99
import 'dart:async';
1010
import 'dart:js_interop';
11-
import 'dart:js_interop_unsafe';
12-
import 'package:flutter/foundation.dart';
13-
import 'package:web/web.dart' as web;
1411
import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart';
1512
import 'package:firebase_core_web/firebase_core_web_interop.dart';
1613
import 'package:http_parser/http_parser.dart';
@@ -395,32 +392,9 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
395392
// ignore: close_sinks
396393
StreamController<User?>? _changeController;
397394

398-
String get authStateWindowsKey => 'flutterfire-${app.name}_authStateChanges';
399-
String get idTokenStateWindowsKey => 'flutterfire-${app.name}_idTokenChanges';
400-
401-
// No way to unsubscribe from event listeners on hot reload so we set on the windows object
402-
// and clean up on hot restart if it exists.
403-
// See: https://github.com/firebase/flutterfire/issues/7064
404-
void _unsubscribeWindowsListener(String key) {
405-
if (kDebugMode) {
406-
final unsubscribe = web.window.getProperty(key.toJS);
407-
if (unsubscribe != null) {
408-
(unsubscribe as JSFunction).callAsFunction();
409-
}
410-
}
411-
}
412-
413-
void _setWindowsListener(String key, JSFunction unsubscribe) {
414-
if (kDebugMode) {
415-
web.window.setProperty(key.toJS, unsubscribe);
416-
}
417-
}
418-
419-
void _removeWindowsListener(String key) {
420-
if (kDebugMode) {
421-
web.window.delete(key.toJS);
422-
}
423-
}
395+
String get _authStateWindowsKey => 'flutterfire-${app.name}_authStateChanges';
396+
String get _idTokenStateWindowsKey =>
397+
'flutterfire-${app.name}_idTokenChanges';
424398

425399
/// Sends events when the users sign-in state changes.
426400
///
@@ -429,7 +403,7 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
429403
///
430404
/// If the value is `null`, there is no signed-in user.
431405
Stream<User?> get onAuthStateChanged {
432-
_unsubscribeWindowsListener(authStateWindowsKey);
406+
unsubscribeWindowsListener(_authStateWindowsKey);
433407

434408
if (_changeController == null) {
435409
final nextWrapper = (auth_interop.UserJsImpl? user) {
@@ -443,14 +417,14 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
443417
final unsubscribe =
444418
jsObject.onAuthStateChanged(nextWrapper.toJS, errorWrapper.toJS);
445419
_onAuthUnsubscribe = unsubscribe;
446-
_setWindowsListener(authStateWindowsKey, unsubscribe);
420+
setWindowsListener(_authStateWindowsKey, unsubscribe);
447421
}
448422

449423
void stopListen() {
450424
_onAuthUnsubscribe!.callAsFunction();
451425
_onAuthUnsubscribe = null;
452426
_changeController = null;
453-
_removeWindowsListener(authStateWindowsKey);
427+
removeWindowsListener(_authStateWindowsKey);
454428
}
455429

456430
_changeController = StreamController<User?>.broadcast(
@@ -476,7 +450,7 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
476450
///
477451
/// If the value is `null`, there is no signed-in user.
478452
Stream<User?> get onIdTokenChanged {
479-
_unsubscribeWindowsListener(idTokenStateWindowsKey);
453+
unsubscribeWindowsListener(_idTokenStateWindowsKey);
480454
if (_idTokenChangedController == null) {
481455
final nextWrapper = (auth_interop.UserJsImpl? user) {
482456
_idTokenChangedController!.add(User.getInstance(user));
@@ -489,14 +463,14 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
489463
final unsubscribe =
490464
jsObject.onIdTokenChanged(nextWrapper.toJS, errorWrapper.toJS);
491465
_onIdTokenChangedUnsubscribe = unsubscribe;
492-
_setWindowsListener(idTokenStateWindowsKey, unsubscribe);
466+
setWindowsListener(_idTokenStateWindowsKey, unsubscribe);
493467
}
494468

495469
void stopListen() {
496470
_onIdTokenChangedUnsubscribe!.callAsFunction();
497471
_onIdTokenChangedUnsubscribe = null;
498472
_idTokenChangedController = null;
499-
_removeWindowsListener(idTokenStateWindowsKey);
473+
removeWindowsListener(_idTokenStateWindowsKey);
500474
}
501475

502476
_idTokenChangedController = StreamController<User?>.broadcast(

packages/firebase_core/firebase_core_web/lib/src/interop/utils/utils.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import 'dart:async';
1111
import 'dart:js_interop';
1212
import 'dart:js_interop_unsafe';
13+
import 'package:web/web.dart' as web;
14+
import 'package:flutter/foundation.dart';
1315

1416
import 'func.dart';
1517

@@ -38,3 +40,29 @@ JSPromise handleFutureWithMapper<T, S>(
3840
});
3941
}.toJS);
4042
}
43+
44+
// No way to unsubscribe from event listeners on hot reload so we set on the windows object
45+
// and clean up on hot restart if it exists.
46+
// See: https://github.com/firebase/flutterfire/issues/7064
47+
void unsubscribeWindowsListener(String key) {
48+
if (kDebugMode) {
49+
final unsubscribe = web.window.getProperty(key.toJS);
50+
if (unsubscribe != null) {
51+
(unsubscribe as JSFunction).callAsFunction();
52+
}
53+
}
54+
}
55+
56+
void setWindowsListener(String key, JSFunction unsubscribe) {
57+
if (kDebugMode) {
58+
web.window.setProperty(key.toJS, unsubscribe);
59+
}
60+
}
61+
62+
void removeWindowsListener(String key) {
63+
if (kDebugMode) {
64+
if (web.window.hasProperty(key.toJS) == true.toJS) {
65+
web.window.delete(key.toJS);
66+
}
67+
}
68+
}

packages/firebase_core/firebase_core_web/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ version: 2.17.1
66

77
environment:
88
sdk: '>=3.2.0 <4.0.0'
9-
flutter: '>=3.3.0'
9+
flutter: '>=3.16.0'
1010

1111
dependencies:
1212
firebase_core_platform_interface: ^5.0.0

0 commit comments

Comments
 (0)