fix(test): cancel one-shot Timers on CacheFiller / MutationReplayer dispose

Smoke test failed: "A Timer is still pending even after the widget
tree was disposed." Both workers fired their initial-delay Timer
via `Timer(duration, _sweep)` and stored only the periodic ticker
in the cancellable field — the one-shot Timer leaked past dispose
and tripped the test framework's invariant check.

Track both as _initialTimer + _intervalTimer; cancel both in
dispose(). Behavior is unchanged in production (ref.onDispose only
fires on process death normally); this is purely a test-harness
fix.
This commit is contained in:
2026-05-14 18:38:38 -04:00
parent b991fde3fe
commit d27dd69bfc
2 changed files with 12 additions and 8 deletions
+6 -4
View File
@@ -37,7 +37,8 @@ class CacheFiller {
CacheFiller(this._ref); CacheFiller(this._ref);
final Ref _ref; final Ref _ref;
Timer? _timer; Timer? _initialTimer;
Timer? _intervalTimer;
bool _running = false; bool _running = false;
bool _disposed = false; bool _disposed = false;
@@ -61,8 +62,8 @@ class CacheFiller {
static const _maxIdsPerSweep = 200; static const _maxIdsPerSweep = 200;
void start() { void start() {
Timer(_initialDelay, _sweep); _initialTimer = Timer(_initialDelay, _sweep);
_timer = Timer.periodic(_interval, (_) => _sweep()); _intervalTimer = Timer.periodic(_interval, (_) => _sweep());
} }
Future<void> _sweep() async { Future<void> _sweep() async {
@@ -217,7 +218,8 @@ class CacheFiller {
void dispose() { void dispose() {
_disposed = true; _disposed = true;
_timer?.cancel(); _initialTimer?.cancel();
_intervalTimer?.cancel();
} }
} }
+6 -4
View File
@@ -91,7 +91,8 @@ class MutationReplayer {
MutationReplayer(this._ref); MutationReplayer(this._ref);
final Ref _ref; final Ref _ref;
Timer? _timer; Timer? _initialTimer;
Timer? _intervalTimer;
bool _running = false; bool _running = false;
bool _disposed = false; bool _disposed = false;
@@ -109,7 +110,7 @@ class MutationReplayer {
// Drain shortly after launch in case there are queued mutations // Drain shortly after launch in case there are queued mutations
// from a prior session that died offline. 3s lets the auth + // from a prior session that died offline. 3s lets the auth +
// server-url + dio providers finish their async warmup. // server-url + dio providers finish their async warmup.
Timer(const Duration(seconds: 3), drain); _initialTimer = Timer(const Duration(seconds: 3), drain);
// Drain on every connectivity emission that resolves to online. // Drain on every connectivity emission that resolves to online.
// ref.listen fires on changes after subscription; we don't get // ref.listen fires on changes after subscription; we don't get
@@ -120,7 +121,7 @@ class MutationReplayer {
if (isOnline) unawaited(drain()); if (isOnline) unawaited(drain());
}); });
_timer = Timer.periodic(_interval, (_) => drain()); _intervalTimer = Timer.periodic(_interval, (_) => drain());
} }
/// Walk the queue oldest-first, replaying each mutation. Stops on /// Walk the queue oldest-first, replaying each mutation. Stops on
@@ -214,7 +215,8 @@ class MutationReplayer {
void dispose() { void dispose() {
_disposed = true; _disposed = true;
_timer?.cancel(); _initialTimer?.cancel();
_intervalTimer?.cancel();
} }
} }