Add debounce logic to Stats Cache Invalidator
epic-activity-statistics-dashboard-state-and-realtime-task-005 — Implement a debounce mechanism inside the Stats Cache Invalidator so that rapid successive INSERT events on the activities table (e.g., bulk registrations by a coordinator) are collapsed into a single invalidation signal. Use a Timer-based debounce of 800ms. After the debounce window expires, call ref.invalidate on the stats notifier provider. This prevents unnecessary refetches during bulk registration workflows.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Use `dart:async` Timer with a nullable `_debounceTimer` field. On each event: cancel `_debounceTimer` if non-null, then assign a new `Timer(const Duration(milliseconds: 800), _onDebounceExpired)`. In `_onDebounceExpired`, call `ref.invalidate(statsNotifierProvider)`. Define `static const _kDebounceMs = Duration(milliseconds: 800)` as a class-level constant.
Override `dispose()` to call `_debounceTimer?.cancel()`. Do NOT use Stream.debounceTime from rxdart — keep the dependency surface minimal by using only dart:async. This pattern is identical to search field debouncing and is well-understood by the team.
Testing Requirements
Write unit tests using flutter_test: (1) assert that 10 rapid events within 800ms produce exactly 1 invalidation call; (2) assert that 2 events separated by >800ms produce 2 invalidation calls; (3) assert that Timer is cancelled on dispose and no invalidation fires after disposal. Use fake_async package to control time without real delays. Use a mock ref/callback to count invalidation calls. All tests must pass without real Supabase connection.
Supabase realtime channel subscriptions that are not properly disposed on screen close can accumulate in memory across navigation events, causing duplicate invalidation calls, ghost fetches, and eventual memory leaks on long sessions.
Mitigation & Contingency
Mitigation: Implement StatsCacheInvalidator as a Riverpod provider with an explicit ref.onDispose callback that cancels the realtime channel subscription. Write a widget test that navigates away and back multiple times and asserts that only one subscription is active at any given time.
Contingency: If subscription leaks are found in production, add a global subscription registry that enforces at-most-one subscription per channel key, and schedule a dispose sweep on app background events.
Debouncing rapid inserts may swallow the invalidation signal if the debounce window outlasts the Supabase realtime event delivery window, resulting in the dashboard showing stale totals after a bulk registration completes.
Mitigation & Contingency
Mitigation: Set the debounce window to 800ms (shorter than the typical Supabase realtime delivery latency of 1-2s for batched events) and ensure the leading-edge invalidation fires immediately while trailing duplicates are suppressed. Integration-test with a 20-record bulk insert.
Contingency: If debounce timing proves unreliable, replace debounce with a trailing-edge timer reset on each event and add a guaranteed invalidation 5 seconds after the last event regardless of subsequent events.