refactor: use new_search to create CancellationToken for search#192
refactor: use new_search to create CancellationToken for search#192hsqStephenZhang wants to merge 6 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors search cancellation/versioning so the backend owns the global search version counter (instead of the frontend supplying versions), and updates the IPC payload to represent cancelled searches explicitly.
Changes:
- Add
CancellationToken::new_search()(auto-increments global search version) and exposeCancellationToken::version(). - Update the Tauri
searchcommand to usenew_search()and serialize cancelled searches asresults: null/Option<Vec<_>>. - Update frontend IPC types and hook logic to treat
results: nullas a cancelled search.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| search-cancel/src/lib.rs | Adds backend-owned search token creation + version accessor. |
| cardinal/src-tauri/src/commands.rs | Switches search command to backend-owned cancellation/versioning and nullable results. |
| cardinal/src/types/ipc.ts | Updates IPC payload typing to allow results: null for cancellation. |
| cardinal/src/hooks/useFileSearch.ts | Stops sending version, and ignores cancelled/stale responses. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// create a token for search, different with `CancellationToken::new` | ||
| /// it uses global version's current version number | ||
| /// user don't need to specify a version number |
| pub fn new_search() -> Self { | ||
| let version = self::ACTIVE_SEARCH_VERSION.fetch_add(1, Ordering::SeqCst) + 1; | ||
| Self { | ||
| version, | ||
| active_version: &ACTIVE_SEARCH_VERSION, | ||
| } | ||
| } |
| if (rawResults.results === null || searchVersionRef.current !== requestVersion) { | ||
| return; | ||
| } |
| #[derive(Serialize, Default)] | ||
| pub struct SearchResponse { | ||
| pub results: Vec<SlabIndex>, | ||
| pub results: Option<Vec<SlabIndex>>, | ||
| pub highlights: Vec<String>, | ||
| } |
| export type SearchResponsePayload = { | ||
| results: number[]; | ||
| // null means search is cancelled | ||
| results: number[] | null; | ||
| highlights?: string[]; |
| if (rawResults.results === null || searchVersionRef.current !== requestVersion) { | ||
| return; | ||
| } | ||
|
|
|
seems that i don't have the permission to let copilot re-review the code. |
|
@hsqStephenZhang You need a copilot pro subscription :-P |
There was a problem hiding this comment.
Pull request overview
Refactors search cancellation/versioning so the backend owns the global search version and the frontend can detect cancelled searches via an explicit status code in the search response.
Changes:
- Added
CancellationToken::new_search()(auto-increments global search version) and aversion()accessor. - Updated Tauri
searchcommand to usenew_search()and return astatus_codeindicating OK/ERROR/CANCELLED. - Updated frontend IPC types,
useFileSearchhook, and tests to use the newstatus_codefield and remove the requestversionparameter.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| search-cancel/src/lib.rs | Adds new_search() + version() and expands cancellation tests. |
| cardinal/src-tauri/src/commands.rs | Switches search cancellation to backend-managed versioning; adds status_code to response. |
| cardinal/src/types/ipc.ts | Introduces SearchStatusCode and adds status_code to SearchResponsePayload. |
| cardinal/src/hooks/useFileSearch.ts | Stops sending version; ignores cancelled results using status_code. |
| cardinal/src/hooks/tests/useFileSearch.test.ts | Updates mocks to include status_code and adds a cancelled-search test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export type SearchResponsePayload = { | ||
| results: number[]; | ||
| highlights?: string[]; | ||
| status_code: SearchStatusCode; |
| #[derive(Serialize, Default)] | ||
| pub struct SearchResponse { | ||
| pub results: Vec<SlabIndex>, | ||
| pub highlights: Vec<String>, | ||
| pub status_code: u8, | ||
| } |
| if ( | ||
| rawResults.status_code === SearchStatusCode.CANCELLED || | ||
| searchVersionRef.current !== requestVersion | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| const searchResults = rawResults.results as SlabIndex[]; | ||
| const highlightTerms = Array.isArray(rawResults.highlights) | ||
| ? rawResults.highlights.filter((term): term is string => typeof term === 'string') | ||
| : []; | ||
|
|
| }) { | ||
| error!("Failed to send search request: {e:?}"); | ||
| return Ok(SearchResponse::default()); | ||
| return Ok(SearchResponse::new_error()); | ||
| } | ||
|
|
||
| match result_rx.recv() { | ||
| Ok(res) => res, | ||
| Err(e) => { | ||
| error!("Failed to receive search result: {e:?}"); | ||
| return Ok(SearchResponse::default()); | ||
| return Ok(SearchResponse::new_error()); | ||
| } |
| it('ignores results when backend returns null (cancelled)', async () => { | ||
| const initialResults = [1, 2, 3] as SlabIndex[]; | ||
|
|
||
| mockedInvoke.mockImplementation((command: string) => { | ||
| if (command === 'get_app_status') { | ||
| return Promise.resolve('Ready'); | ||
| } | ||
| if (command === 'search') { | ||
| // First call (initial search) returns results | ||
| return Promise.resolve({ | ||
| results: initialResults, | ||
| highlights: [], | ||
| status_code: SearchStatusCode.OK, | ||
| }); | ||
| } | ||
| return Promise.resolve(null); | ||
| }); | ||
|
|
||
| const { result } = renderHook(() => useFileSearch()); | ||
|
|
||
| // Wait for initial search to complete | ||
| await waitFor(() => expect(result.current.state.initialFetchCompleted).toBe(true)); | ||
| expect(result.current.state.results).toBe(initialResults); | ||
|
|
||
| // Mock search to return null for the next call | ||
| mockedInvoke.mockImplementation((command: string) => { | ||
| if (command === 'search') { | ||
| return Promise.resolve({ | ||
| results: [], | ||
| highlights: [], | ||
| status_code: SearchStatusCode.CANCELLED, | ||
| }); | ||
| } | ||
| return Promise.resolve('Ready'); | ||
| }); |
|
|
||
| #[test] | ||
| fn search_token_cancelled_after_new_search_version2() { | ||
| let _guard = lock_versions(); | ||
| reset_versions(); | ||
|
|
||
| let search_v1 = CancellationToken::new_search(); | ||
| assert!(search_v1.is_cancelled().is_some()); | ||
|
|
||
| let search_v2 = CancellationToken::new_search(); | ||
| assert!(search_v2.is_cancelled().is_some()); | ||
| assert!(search_v1.is_cancelled().is_none()); // search_v1 is now cancelled | ||
| } |
There was a problem hiding this comment.
Pull request overview
Refactors Cardinal’s search cancellation to be backend-owned by introducing CancellationToken::new_search() (auto-incrementing a global search version), and updates the IPC contract so the frontend can distinguish “cancelled” from “successful” search results.
Changes:
- Add
CancellationToken::new_search()and update Rust tests to use it instead of passing frontend-managed versions. - Extend search IPC response with
statusCode(OK vs CANCELLED) and update the React hook + tests to consume it. - Update Tauri
searchcommand to create cancellation tokens internally and propagate cancellation viastatusCode.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| search-cancel/tests/more.rs | Updates cancellation tests to use CancellationToken::new_search(). |
| search-cancel/src/lib.rs | Introduces new_search() and exposes version(); adds/updates token behavior tests. |
| search-cache/tests/query_matrix_big.rs | Updates cancellation test to use new_search(). |
| search-cache/tests/edge_cases_comprehensive.rs | Updates cancellation-related tests to use new_search(). |
| search-cache/tests/content_filter_comprehensive.rs | Updates cancellation test setup to use new_search(). |
| search-cache/tests/boundary_limits_tests.rs | Updates cancellation tests to use new_search(). |
| search-cache/src/tests/traversal.rs | Updates cancellation test to use new_search(). |
| search-cache/src/tests/date_volume.rs | Switches a “no cancellation” token to new_search() (now risky under parallel tests). |
| search-cache/src/cache.rs | Updates unit tests that simulate cancellation to use new_search(). |
| namepool/tests/fuzz_large.rs | Updates cancellation simulation to use new_search(). |
| namepool/src/lib.rs | Updates unit tests to use new_search() for cancellation. |
| doc/inner/ui-dataflow.md | Updates UI/backend dataflow documentation for backend-owned cancellation and statusCode. |
| doc/inner/search-cancellation.md | Updates cancellation docs to reference new_search(). |
| cardinal/src/types/ipc.ts | Adds SearchStatusCode enum and makes statusCode required on search response payload. |
| cardinal/src/hooks/useFileSearch.ts | Removes version parameter from search invoke; ignores cancelled responses via statusCode. |
| cardinal/src/hooks/tests/useFileSearch.test.ts | Updates existing test payload shape and adds a CANCELLED-handling test. |
| cardinal/src-tauri/src/commands.rs | Removes version arg from search command, uses new_search(), returns statusCode, and errors on channel failures. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| pub fn new(version: u64) -> Self { | ||
| ACTIVE_SEARCH_VERSION.store(version, Ordering::SeqCst); | ||
| /// Creates a token for a search, unlike `CancellationToken::new`. |
| let _ = cache.search("dm:thisyear").unwrap(); | ||
| // ensure no cancellation | ||
| let token = CancellationToken::new(9999); // never cancelled | ||
| let token = CancellationToken::new_search(); // never cancelled |
| if ( | ||
| rawResults.statusCode === SearchStatusCode.CANCELLED || | ||
| searchVersionRef.current !== requestVersion | ||
| ) { | ||
| return; | ||
| } | ||
|
|
| // The results should still be the initial results, not updated or cleared | ||
| // We wait a bit to ensure it had time to process if it were going to | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
| expect(result.current.state.results).toBe(initialResults); | ||
| expect(result.current.state.currentQuery).toBe(''); // Query doesn't update on cancelled search |
5d5d911 to
cf9c7b5
Compare
Fix
fix #191
Goal
let
search-cachedecide if the version is outdated since there is only a global counter.Solution
search-cancelto provide a new api thatfetch_and_add(counter, 1)Test
via observation that there is no anomaly