high priority low complexity backend pending backend specialist Tier 3

Acceptance Criteria

A `dart:async` Timer-based debounce of exactly 800ms is implemented inside the Stats Cache Invalidator class
When 5 rapid INSERT events arrive within 800ms, only a single invalidation signal is emitted after the debounce window expires
When INSERT events arrive more than 800ms apart, each one triggers its own invalidation signal
The previous Timer is cancelled and restarted on each incoming event within the debounce window
The debounce Timer is properly cancelled and disposed when the invalidator is disposed to prevent memory leaks
No invalidation occurs during the debounce window — only after the 800ms silence period
The debounce constant (800ms) is defined as a named constant, not a magic number
Coordinator bulk registration of 10+ activities in quick succession results in exactly one stats refetch
The debounce mechanism does not drop the final invalidation event — it always fires after the last event

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase Realtime (WebSocket)
data models
activity
annual_summary
performance requirements
Debounce window must be exactly 800ms — no drift under normal load
Timer cancellation must be O(1) with no memory allocation on cancel
Bulk registration of 50 activities must not cause more than 1 refetch within 800ms window
security requirements
Invalidator must not expose raw Supabase realtime payloads to UI layer
Timer disposal must be guaranteed on widget/provider unmount to prevent dangling callbacks

Execution Context

Execution Tier
Tier 3

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.

Component
Stats Cache Invalidator
infrastructure medium
Epic Risks (2)
medium impact medium prob technical

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.

medium impact low prob scope

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.