critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

Given a user types a query, when the debounce period elapses, then the stream emits a SearchLoading state immediately before the search begins
Given a debounced query completes successfully, when results are returned, then the stream emits a SearchSuccess state containing the merged and deduplicated contact list
Given a search query fails (network error or Supabase exception), when the error is caught, then the stream emits a SearchError state with a user-friendly message and does NOT crash
Given a new debounced query arrives while a prior search is still in-flight, when the new query is processed, then the previous in-flight operation is cancelled and its result is discarded (no stale results emitted)
Given the service is disposed, when dispose() is called, then the internal StreamController is closed and no further events are emitted
Given a query that returns zero results, when emitted, then a SearchSuccess state with an empty list is emitted (not an error state)
Given the stream is subscribed to multiple times, when each subscriber receives events, then all subscribers receive the same events (broadcast stream)
The stream must never emit null — all states must be typed SearchResult variants

Technical Requirements

frameworks
Flutter
Dart
BLoC
Riverpod
apis
Supabase Realtime / REST
data models
Contact
SearchResult
SearchLoading
SearchSuccess
SearchError
performance requirements
First loading state must be emitted within 5ms of debounce firing — no blocking synchronous work before emitting loading
In-flight cancellation must use Dart CancelToken pattern or switchMap-equivalent to avoid memory leaks from abandoned futures
Stream must not buffer more than the most recent emission to avoid memory growth during rapid typing
security requirements
SearchError messages must not expose raw Supabase error details or stack traces to the UI layer — wrap in generic user-facing messages
Ensure cancelled in-flight requests do not write results to the stream after cancellation (race condition prevention)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Model the stream using a `StreamController.broadcast()`. Wire SearchDebounceUtility's output stream via `switchMap` (available in rxdart, or implement manually with a `_currentSubscription` pattern: cancel previous subscription before starting new one). The switchMap pattern is the idiomatic cancellation approach: store the active StreamSubscription in a nullable field, cancel it before subscribing to the new repository call. Emit SearchLoading synchronously via `_controller.add(SearchLoading())` before awaiting the repository.

Wrap the async repository call in try/catch — catch PostgrestException, SocketException, and generic Exception separately if different error messages are needed. Expose via a getter `Stream get results => _controller.stream`. If using Riverpod, expose as a StreamProvider. If using BLoC, this service feeds into a SearchBloc via a StreamSubscription in the bloc's constructor.

Keep the service framework-agnostic (pure Dart) so it can be used with either BLoC or Riverpod.

Testing Requirements

Unit tests using flutter_test and fake_async for timing control. Test: (1) loading state emitted immediately on debounce fire, (2) success state with correct contacts on completion, (3) error state on repository exception — verify message is user-friendly, (4) stale result cancellation — emit two rapid queries, verify only the second result appears, (5) empty query emits no loading/search events (relies on SearchDebounceUtility filtering), (6) dispose closes stream cleanly. Use StreamQueue from async package to assert stream emission order. Mock SupabaseSearchRepository and OfflineSearchRepository with Mockito or manual fakes.

Component
Contact Search Service
service low
Epic Risks (2)
medium impact medium prob technical

Cancelling in-flight Supabase HTTP requests via RxDart switchMap may not actually abort the server-side query if the Supabase Dart client does not support request cancellation tokens, leading to wasted API calls and potential race conditions where a slow earlier response arrives after a faster later one.

Mitigation & Contingency

Mitigation: Audit the supabase-flutter client's cancellation support before implementation. Use RxDart switchMap to discard stale emissions even if HTTP cancellation is unavailable, ensuring only the latest result reaches the UI.

Contingency: If race conditions surface in testing, add a query sequence counter to tag each emission and discard any response whose sequence number is lower than the most recently emitted one.

medium impact low prob technical

Connectivity detection used to route between online and offline repositories may have latency or give false positives on flaky connections, causing the service to attempt Supabase queries that time out instead of falling back to the cache promptly.

Mitigation & Contingency

Mitigation: Use a try/catch with a short timeout on Supabase calls. On network error, immediately fall back to the offline repository and emit a cached result with an offline indicator rather than surfacing an error state.

Contingency: If the timeout-based fallback proves insufficient, implement a connection health check stream that pre-validates connectivity before each query batch.