@@ -5,6 +5,9 @@ import { apiGet, ApiError } from '../api/client';
55// Auto-refresh interval (30 seconds)
66const AUTO_REFRESH_INTERVAL_MS = 30000 ;
77
8+ // Grace period to suppress deleted threads from reappearing via API polling (2 minutes)
9+ const DELETE_GRACE_PERIOD_MS = 120000 ;
10+
811export function useThreads ( ) {
912 const [ threads , setThreads ] = useState < Thread [ ] > ( [ ] ) ;
1013 const [ loading , setLoading ] = useState ( true ) ;
@@ -13,55 +16,73 @@ export function useThreads() {
1316 const [ hasMore , setHasMore ] = useState ( false ) ;
1417 const cursorRef = useRef < string | null > ( null ) ;
1518 const autoRefreshRef = useRef < number | null > ( null ) ;
19+ // Track recently deleted thread IDs to prevent them reappearing from API during eventual consistency lag
20+ const deletedIdsRef = useRef < Map < string , number > > ( new Map ( ) ) ;
1621
17- const fetchThreads = useCallback ( async ( append = false ) => {
18- if ( append ) {
19- setLoadingMore ( true ) ;
20- } else {
21- setLoading ( true ) ;
22+ const filterDeleted = useCallback ( ( threadList : Thread [ ] ) : Thread [ ] => {
23+ const now = Date . now ( ) ;
24+ const deleted = deletedIdsRef . current ;
25+ // Prune expired entries
26+ for ( const [ id , expiry ] of deleted ) {
27+ if ( now >= expiry ) deleted . delete ( id ) ;
2228 }
23- setError ( null ) ;
24-
25- try {
26- const cursor = append ? cursorRef . current : null ;
27- const data = await apiGet < ThreadsResult > (
28- `/api/threads?limit=50${ cursor ? `&cursor=${ cursor } ` : '' } ` ,
29- ) ;
29+ if ( deleted . size === 0 ) return threadList ;
30+ return threadList . filter ( ( t ) => ! deleted . has ( t . id ) ) ;
31+ } , [ ] ) ;
3032
33+ const fetchThreads = useCallback (
34+ async ( append = false ) => {
3135 if ( append ) {
32- setThreads ( ( prev ) => [ ... prev , ... data . threads ] ) ;
36+ setLoadingMore ( true ) ;
3337 } else {
34- // Stabilize reference: skip setState if thread list hasn't meaningfully changed,
35- // preventing downstream re-renders (e.g., useFilters label re-fetch) on every poll
36- setThreads ( ( prev ) => {
37- if ( prev . length !== data . threads . length ) return data . threads ;
38- const changed = prev . some ( ( t , i ) => {
39- const next = data . threads [ i ] ;
40- return (
41- ! next ||
42- t . id !== next . id ||
43- t . title !== next . title ||
44- t . lastUpdated !== next . lastUpdated
45- ) ;
46- } ) ;
47- return changed ? data . threads : prev ;
48- } ) ;
38+ setLoading ( true ) ;
4939 }
50- cursorRef . current = data . nextCursor ;
51- setHasMore ( data . hasMore ) ;
52- } catch ( err ) {
53- if ( err instanceof ApiError ) {
54- console . error ( `[useThreads] API error ${ err . status } : ${ err . message } ` ) ;
55- setError ( err . message ) ;
56- } else {
57- console . error ( '[useThreads] Unexpected error:' , err ) ;
58- setError ( err instanceof Error ? err . message : 'Unknown error' ) ;
40+ setError ( null ) ;
41+
42+ try {
43+ const cursor = append ? cursorRef . current : null ;
44+ const data = await apiGet < ThreadsResult > (
45+ `/api/threads?limit=50${ cursor ? `&cursor=${ cursor } ` : '' } ` ,
46+ ) ;
47+
48+ const filtered = filterDeleted ( data . threads ) ;
49+
50+ if ( append ) {
51+ setThreads ( ( prev ) => [ ...prev , ...filterDeleted ( data . threads ) ] ) ;
52+ } else {
53+ // Stabilize reference: skip setState if thread list hasn't meaningfully changed,
54+ // preventing downstream re-renders (e.g., useFilters label re-fetch) on every poll
55+ setThreads ( ( prev ) => {
56+ if ( prev . length !== filtered . length ) return filtered ;
57+ const changed = prev . some ( ( t , i ) => {
58+ const next = filtered [ i ] ;
59+ return (
60+ ! next ||
61+ t . id !== next . id ||
62+ t . title !== next . title ||
63+ t . lastUpdated !== next . lastUpdated
64+ ) ;
65+ } ) ;
66+ return changed ? filtered : prev ;
67+ } ) ;
68+ }
69+ cursorRef . current = data . nextCursor ;
70+ setHasMore ( data . hasMore ) ;
71+ } catch ( err ) {
72+ if ( err instanceof ApiError ) {
73+ console . error ( `[useThreads] API error ${ err . status } : ${ err . message } ` ) ;
74+ setError ( err . message ) ;
75+ } else {
76+ console . error ( '[useThreads] Unexpected error:' , err ) ;
77+ setError ( err instanceof Error ? err . message : 'Unknown error' ) ;
78+ }
79+ } finally {
80+ setLoading ( false ) ;
81+ setLoadingMore ( false ) ;
5982 }
60- } finally {
61- setLoading ( false ) ;
62- setLoadingMore ( false ) ;
63- }
64- } , [ ] ) ;
83+ } ,
84+ [ filterDeleted ] ,
85+ ) ;
6586
6687 const loadMore = useCallback ( ( ) => {
6788 if ( hasMore && ! loadingMore ) {
@@ -70,6 +91,7 @@ export function useThreads() {
7091 } , [ hasMore , loadingMore , fetchThreads ] ) ;
7192
7293 const removeThread = useCallback ( ( threadId : string ) => {
94+ deletedIdsRef . current . set ( threadId , Date . now ( ) + DELETE_GRACE_PERIOD_MS ) ;
7395 setThreads ( ( prev ) => prev . filter ( ( t ) => t . id !== threadId ) ) ;
7496 } , [ ] ) ;
7597
0 commit comments