ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

fix: pass final search results to onComplete callback

Fixes issue where results were displayed but not saved to database until
page refresh. Root cause: onComplete callback accessed stale searchResults
from closure instead of updated state.

Changes:
- useSearch.searchAllUsers: onComplete now receives SearchResult[] param
- useSearch: uses setSearchResults updater to get current state
- App.tsx: updated all 3 searchAllUsers calls to use finalResults
- Removed setTimeout workarounds

Result: Extension and file upload flows now save immediately after search.

byarielm.fyi 212660a9 92cac386

verified
Changed files
+77 -41
docs
packages
web
src
+14 -14
docs/git-history.json
··· 1 [ 2 { 3 - "hash": "a90f3a813fd9527e8e59bb1da242e7b67555a8eb", 4 - "short_hash": "a90f3a8", 5 "author": "Ariel M. Lighty", 6 - "date": "2025-12-26T16:11:04-05:00", 7 - "message": "fix: pass onProgressUpdate and onComplete callbacks to searchAllUsers", 8 "files_changed": 3 9 }, 10 { ··· 24 "files_changed": 4 25 }, 26 { 27 - "hash": "9ca734749fbaa014828f8437afc5e515610afd31", 28 - "short_hash": "9ca7347", 29 "author": "Ariel M. Lighty", 30 - "date": "2025-12-26T13:37:24-05:00", 31 - "message": "update documentation: extension ready for testing after API response fix", 32 "files_changed": 4 33 }, 34 { ··· 88 "files_changed": 3 89 }, 90 { 91 - "hash": "32cdee3aeac7ef986df47e0fff786b5f7471e55b", 92 - "short_hash": "32cdee3", 93 "author": "Ariel M. Lighty", 94 "date": "2025-12-25T13:22:32-05:00", 95 "message": "configure Netlify dev for monorepo with --filter flag\n\nFixed Netlify CLI monorepo detection issue by using --filter flag:\n- Updated root package.json scripts to use 'npx netlify-cli dev --filter @atlast/web'\n- Updated netlify.toml [dev] section to use npm with --prefix for framework command\n- Added monorepo development instructions to CLAUDE.md\n- Documented Windows Git Bash compatibility issue with netlify command\n\nSolution: Use 'npx netlify-cli dev --filter @atlast/web' to bypass monorepo\nproject selection prompt and specify which workspace package to run.\n\nDev server now runs successfully at http://localhost:8888 with all backend\nfunctions loaded.", 96 - "files_changed": 4 97 }, 98 { 99 - "hash": "ba29fd68872913ba0a587aa7f29f97b3d373a732", 100 - "short_hash": "ba29fd6", 101 "author": "Ariel M. Lighty", 102 "date": "2025-12-25T13:22:32-05:00", 103 "message": "configure Netlify dev for monorepo with --filter flag\n\nFixed Netlify CLI monorepo detection issue by using --filter flag:\n- Updated root package.json scripts to use 'npx netlify-cli dev --filter @atlast/web'\n- Updated netlify.toml [dev] section to use npm with --prefix for framework command\n- Added monorepo development instructions to CLAUDE.md\n- Documented Windows Git Bash compatibility issue with netlify command\n\nSolution: Use 'npx netlify-cli dev --filter @atlast/web' to bypass monorepo\nproject selection prompt and specify which workspace package to run.\n\nDev server now runs successfully at http://localhost:8888 with all backend\nfunctions loaded.", 104 - "files_changed": 5 105 }, 106 { 107 "hash": "c3e7afad396d130791d801a85cbfc9643bcd6309",
··· 1 [ 2 { 3 + "hash": "0afa0ffafd9b05f12d7d52c50082a54f237e09d8", 4 + "short_hash": "0afa0ff", 5 "author": "Ariel M. Lighty", 6 + "date": "2025-12-26T20:20:50-05:00", 7 + "message": "fix: sourceUser should be object {username, date} not string\n\nWas setting sourceUser to result.sourceUser.username (string)\nShould be result.sourceUser (SourceUser object)\n\nThis caused:\n- useSearch to call batch.map(r => r.sourceUser.username) on strings\n- .username on string returns undefined\n- batch-search-actors received null values\n- ValidationError: expected string, received null\n\nAlso caused localeCompare error when sorting undefined values.", 8 "files_changed": 3 9 }, 10 { ··· 24 "files_changed": 4 25 }, 26 { 27 + "hash": "581ed00fec3c0c5f472c6ff92e00bf4ed5b27e9a", 28 + "short_hash": "581ed00", 29 "author": "Ariel M. Lighty", 30 + "date": "2025-12-26T13:47:37-05:00", 31 + "message": "fix extension import: use bulkCreate and handle uploadId param\n\nBackend fixes:\n- Use SourceAccountRepository.bulkCreate() instead of non-existent upsertSourceAccount()\n- Change redirectUrl from /results?uploadId= to /?uploadId=\n- More efficient bulk insert instead of loop\n\nFrontend fixes:\n- Add useEffect to load results when uploadId param present\n- Calls loadUploadResults(uploadId) automatically on page load\n- Cleans up URL param after loading\n\nResolves:\n- \"sourceAccountRepo.upsertSourceAccount is not a function\" error\n- \"No routes matched location /results?uploadId=...\" routing error", 32 "files_changed": 4 33 }, 34 { ··· 88 "files_changed": 3 89 }, 90 { 91 + "hash": "ba29fd68872913ba0a587aa7f29f97b3d373a732", 92 + "short_hash": "ba29fd6", 93 "author": "Ariel M. Lighty", 94 "date": "2025-12-25T13:22:32-05:00", 95 "message": "configure Netlify dev for monorepo with --filter flag\n\nFixed Netlify CLI monorepo detection issue by using --filter flag:\n- Updated root package.json scripts to use 'npx netlify-cli dev --filter @atlast/web'\n- Updated netlify.toml [dev] section to use npm with --prefix for framework command\n- Added monorepo development instructions to CLAUDE.md\n- Documented Windows Git Bash compatibility issue with netlify command\n\nSolution: Use 'npx netlify-cli dev --filter @atlast/web' to bypass monorepo\nproject selection prompt and specify which workspace package to run.\n\nDev server now runs successfully at http://localhost:8888 with all backend\nfunctions loaded.", 96 + "files_changed": 5 97 }, 98 { 99 + "hash": "32cdee3aeac7ef986df47e0fff786b5f7471e55b", 100 + "short_hash": "32cdee3", 101 "author": "Ariel M. Lighty", 102 "date": "2025-12-25T13:22:32-05:00", 103 "message": "configure Netlify dev for monorepo with --filter flag\n\nFixed Netlify CLI monorepo detection issue by using --filter flag:\n- Updated root package.json scripts to use 'npx netlify-cli dev --filter @atlast/web'\n- Updated netlify.toml [dev] section to use npm with --prefix for framework command\n- Added monorepo development instructions to CLAUDE.md\n- Documented Windows Git Bash compatibility issue with netlify command\n\nSolution: Use 'npx netlify-cli dev --filter @atlast/web' to bypass monorepo\nproject selection prompt and specify which workspace package to run.\n\nDev server now runs successfully at http://localhost:8888 with all backend\nfunctions loaded.", 104 + "files_changed": 4 105 }, 106 { 107 "hash": "c3e7afad396d130791d801a85cbfc9643bcd6309",
+44
docs/graph-data.json
··· 3530 "created_at": "2025-12-26T20:39:45.657720100-05:00", 3531 "updated_at": "2025-12-26T20:39:45.657720100-05:00", 3532 "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3533 } 3534 ], 3535 "edges": [ ··· 6964 "weight": 1.0, 6965 "rationale": "Implementation complete", 6966 "created_at": "2025-12-26T20:39:48.757903800-05:00" 6967 } 6968 ] 6969 }
··· 3530 "created_at": "2025-12-26T20:39:45.657720100-05:00", 3531 "updated_at": "2025-12-26T20:39:45.657720100-05:00", 3532 "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3533 + }, 3534 + { 3535 + "id": 322, 3536 + "change_id": "2e824556-15c7-4656-b771-1b85cc628edc", 3537 + "node_type": "observation", 3538 + "title": "onComplete callback in handleLoadUpload accesses stale searchResults from closure - state updated by searchAllUsers not visible to callback", 3539 + "description": null, 3540 + "status": "pending", 3541 + "created_at": "2025-12-26T20:51:55.431293100-05:00", 3542 + "updated_at": "2025-12-26T20:51:55.431293100-05:00", 3543 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3544 + }, 3545 + { 3546 + "id": 323, 3547 + "change_id": "88fc65bc-c2da-4df7-b79e-ba80d93e5b77", 3548 + "node_type": "outcome", 3549 + "title": "Fixed stale closure issue - onComplete now receives finalResults from useSearch state", 3550 + "description": null, 3551 + "status": "pending", 3552 + "created_at": "2025-12-26T20:55:36.922743800-05:00", 3553 + "updated_at": "2025-12-26T20:55:36.922743800-05:00", 3554 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 3555 } 3556 ], 3557 "edges": [ ··· 6986 "weight": 1.0, 6987 "rationale": "Implementation complete", 6988 "created_at": "2025-12-26T20:39:48.757903800-05:00" 6989 + }, 6990 + { 6991 + "id": 313, 6992 + "from_node_id": 321, 6993 + "to_node_id": 322, 6994 + "from_change_id": "ac843fbc-1953-4b61-8ef3-4c88c98572f5", 6995 + "to_change_id": "2e824556-15c7-4656-b771-1b85cc628edc", 6996 + "edge_type": "leads_to", 6997 + "weight": 1.0, 6998 + "rationale": "New UX issue found", 6999 + "created_at": "2025-12-26T20:51:58.153139700-05:00" 7000 + }, 7001 + { 7002 + "id": 314, 7003 + "from_node_id": 322, 7004 + "to_node_id": 323, 7005 + "from_change_id": "2e824556-15c7-4656-b771-1b85cc628edc", 7006 + "to_change_id": "88fc65bc-c2da-4df7-b79e-ba80d93e5b77", 7007 + "edge_type": "leads_to", 7008 + "weight": 1.0, 7009 + "rationale": "Implementation complete", 7010 + "created_at": "2025-12-26T20:55:40.014892600-05:00" 7011 } 7012 ] 7013 }
+12 -25
packages/web/src/App.tsx
··· 139 searchAllUsers( 140 initialResults, 141 setStatusMessage, 142 - () => { 143 setCurrentStep("results"); 144 145 // Save results after search completes 146 - setTimeout(() => { 147 - setSearchResults((currentResults) => { 148 - if (currentResults.length > 0) { 149 - saveResults(uploadId, platform, currentResults); 150 - } 151 - return currentResults; 152 - }); 153 - }, 1000); 154 }, 155 followLexicon, 156 ); ··· 211 await searchAllUsers( 212 loadedResults, 213 (message) => setStatusMessage(message), 214 - async () => { 215 // Search complete - save results 216 - // Use current searchResults state which has been updated by searchAllUsers 217 - const currentResults = searchResults.filter(r => !r.isSearching); 218 - if (currentResults.length > 0) { 219 - await saveResults(uploadId, platform, currentResults); 220 - } 221 }, 222 followLexicon 223 ); ··· 233 setCurrentStep("home"); 234 } 235 }, 236 - [setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error, currentDestinationAppId, searchAllUsers, saveResults, searchResults], 237 ); 238 239 // Login handler ··· 316 await searchAllUsers( 317 initialResults, 318 setStatusMessage, 319 - () => { 320 setCurrentStep('results'); 321 322 // Save results after search completes 323 - setTimeout(() => { 324 - setSearchResults((currentResults) => { 325 - if (currentResults.length > 0) { 326 - saveResults(uploadId, platform, currentResults); 327 - } 328 - return currentResults; 329 - }); 330 - }, 1000); 331 332 // Clear import ID from URL 333 const newUrl = new URL(window.location.href);
··· 139 searchAllUsers( 140 initialResults, 141 setStatusMessage, 142 + (finalResults) => { 143 setCurrentStep("results"); 144 145 // Save results after search completes 146 + if (finalResults.length > 0) { 147 + saveResults(uploadId, platform, finalResults); 148 + } 149 }, 150 followLexicon, 151 ); ··· 206 await searchAllUsers( 207 loadedResults, 208 (message) => setStatusMessage(message), 209 + async (finalResults) => { 210 // Search complete - save results 211 + // finalResults contains the updated results with all matches found 212 + await saveResults(uploadId, platform, finalResults); 213 }, 214 followLexicon 215 ); ··· 225 setCurrentStep("home"); 226 } 227 }, 228 + [setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error, currentDestinationAppId, searchAllUsers, saveResults], 229 ); 230 231 // Login handler ··· 308 await searchAllUsers( 309 initialResults, 310 setStatusMessage, 311 + (finalResults) => { 312 setCurrentStep('results'); 313 314 // Save results after search completes 315 + if (finalResults.length > 0) { 316 + saveResults(uploadId, platform, finalResults); 317 + } 318 319 // Clear import ID from URL 320 const newUrl = new URL(window.location.href);
+7 -2
packages/web/src/hooks/useSearch.ts
··· 18 const searchAllUsers = useCallback(async ( 19 resultsToSearch: SearchResult[], 20 onProgressUpdate: (message: string) => void, 21 - onComplete: () => void, 22 followLexicon?: string, 23 ) => { 24 if (!session || resultsToSearch.length === 0) return; ··· 132 onProgressUpdate( 133 `Search complete! Found ${totalFound} matches out of ${totalSearched} users searched.`, 134 ); 135 - onComplete(); 136 }, [session]); 137 138 const toggleMatchSelection = useCallback((resultIndex: number, did: string) => {
··· 18 const searchAllUsers = useCallback(async ( 19 resultsToSearch: SearchResult[], 20 onProgressUpdate: (message: string) => void, 21 + onComplete: (finalResults: SearchResult[]) => void, 22 followLexicon?: string, 23 ) => { 24 if (!session || resultsToSearch.length === 0) return; ··· 132 onProgressUpdate( 133 `Search complete! Found ${totalFound} matches out of ${totalSearched} users searched.`, 134 ); 135 + 136 + // Get current results from state to pass to onComplete 137 + setSearchResults((currentResults) => { 138 + onComplete(currentResults); 139 + return currentResults; 140 + }); 141 }, [session]); 142 143 const toggleMatchSelection = useCallback((resultIndex: number, did: string) => {