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 1 [ 2 2 { 3 - "hash": "a90f3a813fd9527e8e59bb1da242e7b67555a8eb", 4 - "short_hash": "a90f3a8", 3 + "hash": "0afa0ffafd9b05f12d7d52c50082a54f237e09d8", 4 + "short_hash": "0afa0ff", 5 5 "author": "Ariel M. Lighty", 6 - "date": "2025-12-26T16:11:04-05:00", 7 - "message": "fix: pass onProgressUpdate and onComplete callbacks to searchAllUsers", 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 8 "files_changed": 3 9 9 }, 10 10 { ··· 24 24 "files_changed": 4 25 25 }, 26 26 { 27 - "hash": "9ca734749fbaa014828f8437afc5e515610afd31", 28 - "short_hash": "9ca7347", 27 + "hash": "581ed00fec3c0c5f472c6ff92e00bf4ed5b27e9a", 28 + "short_hash": "581ed00", 29 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", 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 32 "files_changed": 4 33 33 }, 34 34 { ··· 88 88 "files_changed": 3 89 89 }, 90 90 { 91 - "hash": "32cdee3aeac7ef986df47e0fff786b5f7471e55b", 92 - "short_hash": "32cdee3", 91 + "hash": "ba29fd68872913ba0a587aa7f29f97b3d373a732", 92 + "short_hash": "ba29fd6", 93 93 "author": "Ariel M. Lighty", 94 94 "date": "2025-12-25T13:22:32-05:00", 95 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 96 + "files_changed": 5 97 97 }, 98 98 { 99 - "hash": "ba29fd68872913ba0a587aa7f29f97b3d373a732", 100 - "short_hash": "ba29fd6", 99 + "hash": "32cdee3aeac7ef986df47e0fff786b5f7471e55b", 100 + "short_hash": "32cdee3", 101 101 "author": "Ariel M. Lighty", 102 102 "date": "2025-12-25T13:22:32-05:00", 103 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 104 + "files_changed": 4 105 105 }, 106 106 { 107 107 "hash": "c3e7afad396d130791d801a85cbfc9643bcd6309",
+44
docs/graph-data.json
··· 3530 3530 "created_at": "2025-12-26T20:39:45.657720100-05:00", 3531 3531 "updated_at": "2025-12-26T20:39:45.657720100-05:00", 3532 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}" 3533 3555 } 3534 3556 ], 3535 3557 "edges": [ ··· 6964 6986 "weight": 1.0, 6965 6987 "rationale": "Implementation complete", 6966 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" 6967 7011 } 6968 7012 ] 6969 7013 }
+12 -25
packages/web/src/App.tsx
··· 139 139 searchAllUsers( 140 140 initialResults, 141 141 setStatusMessage, 142 - () => { 142 + (finalResults) => { 143 143 setCurrentStep("results"); 144 144 145 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); 146 + if (finalResults.length > 0) { 147 + saveResults(uploadId, platform, finalResults); 148 + } 154 149 }, 155 150 followLexicon, 156 151 ); ··· 211 206 await searchAllUsers( 212 207 loadedResults, 213 208 (message) => setStatusMessage(message), 214 - async () => { 209 + async (finalResults) => { 215 210 // 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 - } 211 + // finalResults contains the updated results with all matches found 212 + await saveResults(uploadId, platform, finalResults); 221 213 }, 222 214 followLexicon 223 215 ); ··· 233 225 setCurrentStep("home"); 234 226 } 235 227 }, 236 - [setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error, currentDestinationAppId, searchAllUsers, saveResults, searchResults], 228 + [setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error, currentDestinationAppId, searchAllUsers, saveResults], 237 229 ); 238 230 239 231 // Login handler ··· 316 308 await searchAllUsers( 317 309 initialResults, 318 310 setStatusMessage, 319 - () => { 311 + (finalResults) => { 320 312 setCurrentStep('results'); 321 313 322 314 // 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); 315 + if (finalResults.length > 0) { 316 + saveResults(uploadId, platform, finalResults); 317 + } 331 318 332 319 // Clear import ID from URL 333 320 const newUrl = new URL(window.location.href);
+7 -2
packages/web/src/hooks/useSearch.ts
··· 18 18 const searchAllUsers = useCallback(async ( 19 19 resultsToSearch: SearchResult[], 20 20 onProgressUpdate: (message: string) => void, 21 - onComplete: () => void, 21 + onComplete: (finalResults: SearchResult[]) => void, 22 22 followLexicon?: string, 23 23 ) => { 24 24 if (!session || resultsToSearch.length === 0) return; ··· 132 132 onProgressUpdate( 133 133 `Search complete! Found ${totalFound} matches out of ${totalSearched} users searched.`, 134 134 ); 135 - onComplete(); 135 + 136 + // Get current results from state to pass to onComplete 137 + setSearchResults((currentResults) => { 138 + onComplete(currentResults); 139 + return currentResults; 140 + }); 136 141 }, [session]); 137 142 138 143 const toggleMatchSelection = useCallback((resultIndex: number, did: string) => {