Implement debounced Supabase ILIKE search path
epic-contact-list-management-business-logic-task-005 — Build the remote search path in ContactSearchService for datasets above the size threshold. Implement debounced Supabase ILIKE queries against the contacts table targeting name and notes columns. Apply the same query normalization as the local path. Integrate with ContactRepository's search method, passing the normalized query string and returning a list of matching contact models.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Implement debounce using a `Timer` (dart:async) held as a nullable field in the service. On each new query call, cancel any existing timer and schedule a new one. When the timer fires, perform the Supabase call. Use a `Completer>` pattern or expose a `Stream
>` so the caller can await the debounced result cleanly.
A `StreamController.broadcast()` approach is recommended: each call to search() emits into the stream after the debounce fires, allowing Riverpod to `watch` the stream directly. The ContactRepository.searchContacts() method should use `.or()` with ILIKE rather than two separate `.ilike()` calls to avoid AND semantics. Ensure the debounce timer is cancelled in a dispose() method to prevent timer leaks when the service is garbage collected. For testability, inject the debounce duration as an optional constructor parameter with a default of `const Duration(milliseconds: 300)`.
Testing Requirements
Unit tests (flutter_test) with a mocked ContactRepository to verify: normalisation applied before passing to repo, empty query short-circuits without calling repo, repo errors propagate correctly. Integration test (test with a real or emulated Supabase instance) to verify: ILIKE query returns expected matches, OR logic across name+notes works, RLS blocks cross-org results. Debounce behaviour test: use fake async / Timer mocking to confirm only one repo call fires after three rapid invocations within the window.
For organizations with large contact lists (NHF has 1,400 local chapters and potentially thousands of contacts), local in-memory filtering may be too slow and Supabase ILIKE queries without supporting indexes may exceed acceptable response times or accumulate excessive read costs, degrading search usability for power users.
Mitigation & Contingency
Mitigation: Define and document the list-size threshold in ContactSearchService before implementation. Confirm that indexes on name and notes columns exist in the Supabase schema before enabling server-side search. Profile ContactSearchService against realistic data volumes in the staging environment using the largest expected org.
Contingency: If response times are unacceptable in staging, introduce result-count pagination in ContactListService and add a user-visible 'showing top N results — refine your search' indicator, deferring full pagination to a follow-up task.
In NHF's multi-chapter context, when a user switches organization, Riverpod providers may emit a brief window of stale contact data scoped to the previous organization before the invalidation cycle completes, transiently exposing contacts from the wrong chapter.
Mitigation & Contingency
Mitigation: Model organization context as a Riverpod provider dependency so that any context change immediately marks contact providers as stale. Render a loading skeleton instead of the stale list during the re-fetch transition. Cover this scenario in integration tests with explicit org-switch sequences.
Contingency: If race conditions are observed during QA, add an explicit organization_id equality check in ContactListService that compares each fetched record's scope to the active session org, discarding any mismatched batch before returning results to the provider.