Skip to content

Commit c16453e

Browse files
Merge pull request #720 from Workiva/auto-teardown-adapter-stores
CPLAT-16343 Auto-tear-down ConnectFluxAdapterStores when backing Flux stores dispose
2 parents 3fdf879 + 39ceb42 commit c16453e

File tree

5 files changed

+156
-1
lines changed

5 files changed

+156
-1
lines changed

analysis_options.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ analyzer:
1010
- tools/analyzer_plugin/**
1111
- ddc_precompiled/**
1212
errors:
13+
invalid_export_of_internal_element: warning
1314
unused_import: warning
1415
duplicate_import: warning
1516
missing_required_param: error

lib/over_react_flux.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
export 'src/over_react_redux/over_react_flux.dart';
15+
export 'src/over_react_redux/over_react_flux.dart' hide ConnectFluxAdapterStoreTestingHelper;
1616
export 'src/over_react_redux/redux_multi_provider.dart';
1717
export 'src/over_react_redux/over_react_redux.dart' show ReduxProvider;

lib/src/over_react_redux/over_react_flux.dart

+22
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,37 @@ class ConnectFluxAdapterStore<S extends flux.Store> extends redux.Store<S> {
131131
});
132132

133133
actionsForStore[store] = actions;
134+
135+
// This store is useless once the flux store is disposed, so for convenience,
136+
// we'll tear it down for consumers.
137+
//
138+
// In most cases, though, from a memory management standpoint, tearing this
139+
// store down shouldn't be necessary, since any components subscribed to it
140+
// should have also been unmounted, leaving nothing to retain it.
141+
//
142+
// Use a null-aware to accommodate mock stores in unit tests that return null for `didDispose`.
143+
store.didDispose?.whenComplete(teardown);
134144
}
135145

146+
bool _teardownCalled = false;
147+
136148
@override
137149
Future teardown() async {
150+
_teardownCalled = true;
151+
138152
await _storeListener.cancel();
139153
await super.teardown();
140154
}
141155
}
142156

157+
/// Not to be exported; only used to expose private fields for testing.
158+
@internal
159+
@visibleForTesting
160+
extension ConnectFluxAdapterStoreTestingHelper on ConnectFluxAdapterStore {
161+
@visibleForTesting
162+
bool get teardownCalled => _teardownCalled;
163+
}
164+
143165
/// Adapts a Flux store to the interface of a Redux store.
144166
///
145167
/// When using an Influx architecture, this store allows Redux, Flux, and [connectFlux]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright 2021 Workiva Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:async';
16+
17+
import 'package:over_react/src/over_react_redux/over_react_flux.dart';
18+
import 'package:test/test.dart';
19+
import 'package:w_flux/w_flux.dart' as flux;
20+
21+
main() {
22+
// See connect_flux_integration_test.dart for more test coverage of this class
23+
group('ConnectFluxAdapterStore', () {
24+
group('teardown', () {
25+
test('calls super, closing change subscriptions', () async {
26+
final adapterStore = SimpleStore().asConnectFluxStore(null);
27+
expect(() => adapterStore.onChange.listen((_) {}), returnsNormally,
28+
reason: 'test setup check: the stream should be open');
29+
30+
await adapterStore.teardown();
31+
});
32+
33+
test('stops listening to the flux store', () async {
34+
final store = SpyStore();
35+
final adapterStore = store.asConnectFluxStore(null);
36+
37+
expect(store.spiedSubscriptions, hasLength(1));
38+
// ignore: cancel_subscriptions
39+
final sub = store.spiedSubscriptions.single;
40+
41+
final subCalls = [];
42+
sub.onData(subCalls.add);
43+
44+
store.trigger();
45+
await pumpEventQueue();
46+
expect(subCalls, hasLength(1),
47+
reason: 'test setup check; subscription should have gotten event');
48+
49+
await adapterStore.teardown();
50+
store.trigger();
51+
await pumpEventQueue();
52+
expect(subCalls, hasLength(1), reason: 'subscription should have been cancelled');
53+
});
54+
55+
group('can be called multiple times without throwing', () {
56+
ConnectFluxAdapterStore adapterStore;
57+
58+
setUp(() {
59+
adapterStore = SimpleStore().asConnectFluxStore(null);
60+
});
61+
62+
test('synchronously', () async {
63+
await Future.wait([
64+
adapterStore.teardown(),
65+
adapterStore.teardown(),
66+
]);
67+
});
68+
69+
test('synchronously', () async {
70+
await adapterStore.teardown();
71+
await adapterStore.teardown();
72+
});
73+
});
74+
});
75+
76+
group('calls teardown when the flux store is disposed', () {
77+
flux.Store store;
78+
ConnectFluxAdapterStore adapterStore;
79+
80+
setUp(() {
81+
store = SimpleStore();
82+
adapterStore = store.asConnectFluxStore(null);
83+
});
84+
85+
test('', () async {
86+
expect(adapterStore.teardownCalled, isFalse);
87+
88+
await store.dispose();
89+
await pumpEventQueue();
90+
expect(adapterStore.teardownCalled, isTrue);
91+
});
92+
93+
group('and doesn\'t break if teardown', () {
94+
test('has already been called', () async {
95+
expect(adapterStore.teardownCalled, isFalse);
96+
97+
await adapterStore.teardown();
98+
expect(adapterStore.teardownCalled, isTrue);
99+
100+
await store.dispose();
101+
await pumpEventQueue();
102+
});
103+
104+
test('is called after', () async {
105+
expect(adapterStore.teardownCalled, isFalse);
106+
107+
await store.dispose();
108+
await pumpEventQueue();
109+
expect(adapterStore.teardownCalled, isTrue);
110+
111+
await adapterStore.teardown();
112+
});
113+
});
114+
});
115+
});
116+
}
117+
118+
class SimpleStore extends flux.Store {}
119+
120+
class SpyStore extends flux.Store {
121+
final spiedSubscriptions = <StreamSubscription>[];
122+
123+
@override
124+
listen(onData, {onError, onDone, cancelOnError}) {
125+
final sub =
126+
super.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
127+
spiedSubscriptions.add(sub);
128+
return sub;
129+
}
130+
}

test/over_react_redux_test.dart

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import 'over_react_redux/hooks/use_selector_test.dart' as use_selector_hook_test
2727
import 'over_react_redux/hooks/use_store_test.dart' as use_store_hook_test;
2828
import './over_react_redux/connect_test.dart' as connect_test;
2929
import './over_react_redux/connect_flux_test.dart' as connect_flux_test;
30+
import './over_react_redux/connect_flux_adapter_store_test.dart' as connect_flux_adapter_store_test;
3031
import './over_react_redux/connect_flux_integration_test.dart' as connect_flux_integration_test;
3132
import './over_react_redux/redux_multi_provider_test.dart' as multi_provider_test;
3233
import './over_react_redux/value_mutation_checker_test.dart' as value_mutation_checker_test;
@@ -39,6 +40,7 @@ void main() {
3940
use_store_hook_test.main();
4041
connect_test.main();
4142
connect_flux_test.main();
43+
connect_flux_adapter_store_test.main();
4244
connect_flux_integration_test.main();
4345
multi_provider_test.main();
4446
value_mutation_checker_test.main();

0 commit comments

Comments
 (0)