Problem
LinkRelationshipDialog debounces person searches but neither aborts nor invalidates requests when the dialog closes, when the query falls below two characters, or when a later search begins. A slower older response can overwrite current results and clear the loading state for a newer request.
Evidence (quoted code + context)
Opening resets UI state, but the open effect only executes cleanup when the component unmounts: "useEffect(() => { if (open) { ... setResults([]); ... } }, [open, defaultType]);" (lines 42-55), while the timer cleanup is only "return () => { if (searchTimerRef.current) clearTimeout(searchTimerRef.current); };" in a separate unmount-only effect (lines 57-60). doSearch directly awaits "const result = await api.search(dbId, { q: q.trim(), limit: 10 });" and always commits "setResults(...)" and "setSearching(false)" (lines 62-72). The newer RelationshipModal demonstrates the needed contract with a monotonic request id and checks before it commits results (lines 42-104).
Impact
Typing quickly, closing/reopening, or changing the dialog context can display candidates for a previous query or database. A user can select a stale person and create the wrong relationship, while the spinner may falsely indicate the current request is complete.
Implementation plan
Give the dialog a monotonically increasing request generation (and an AbortController passed through api.search/fetchJson if supported). Increment it and clear the debounce timer on close and on every invalidating input change. Only the latest live request may update results or loading state; handle rejected/aborted searches so the UI does not retain a spinner or unhandled promise.
Acceptance criteria
Results always correspond to the currently visible query and database; deleting to fewer than two characters leaves no results; closing invalidates pending work; and an older response cannot change results or loading after a newer search starts.
Verification
Add a fake-timer React Testing Library test that resolves two deferred api.search calls out of order, plus close/reopen and short-query cases. Run npm test --prefix client -- --run for the dialog tests and npm run build --prefix client.
Dependencies and related work
No PostgreSQL migration work. Align with the existing stale-response protection in RelationshipModal rather than creating a competing search convention.
Scope (Complexity, files, non-goals)
Low; LinkRelationshipDialog, API signal typing only if necessary, and focused tests. Non-goals: changing relationship business rules or search ranking.
Problem
LinkRelationshipDialogdebounces person searches but neither aborts nor invalidates requests when the dialog closes, when the query falls below two characters, or when a later search begins. A slower older response can overwrite current results and clear the loading state for a newer request.Evidence (quoted code + context)
Opening resets UI state, but the
openeffect only executes cleanup when the component unmounts:"useEffect(() => { if (open) { ... setResults([]); ... } }, [open, defaultType]);"(lines 42-55), while the timer cleanup is only"return () => { if (searchTimerRef.current) clearTimeout(searchTimerRef.current); };"in a separate unmount-only effect (lines 57-60).doSearchdirectly awaits"const result = await api.search(dbId, { q: q.trim(), limit: 10 });"and always commits"setResults(...)"and"setSearching(false)"(lines 62-72). The newerRelationshipModaldemonstrates the needed contract with a monotonic request id and checks before it commits results (lines 42-104).Impact
Typing quickly, closing/reopening, or changing the dialog context can display candidates for a previous query or database. A user can select a stale person and create the wrong relationship, while the spinner may falsely indicate the current request is complete.
Implementation plan
Give the dialog a monotonically increasing request generation (and an
AbortControllerpassed throughapi.search/fetchJsonif supported). Increment it and clear the debounce timer on close and on every invalidating input change. Only the latest live request may update results or loading state; handle rejected/aborted searches so the UI does not retain a spinner or unhandled promise.Acceptance criteria
Results always correspond to the currently visible query and database; deleting to fewer than two characters leaves no results; closing invalidates pending work; and an older response cannot change results or loading after a newer search starts.
Verification
Add a fake-timer React Testing Library test that resolves two deferred
api.searchcalls out of order, plus close/reopen and short-query cases. Runnpm test --prefix client -- --runfor the dialog tests andnpm run build --prefix client.Dependencies and related work
No PostgreSQL migration work. Align with the existing stale-response protection in
RelationshipModalrather than creating a competing search convention.Scope (Complexity, files, non-goals)
Low;
LinkRelationshipDialog, API signal typing only if necessary, and focused tests. Non-goals: changing relationship business rules or search ranking.