@@ -35,7 +35,7 @@ export default apiInitializer((api) => {
3535 const topicTrackingState = api . container . lookup (
3636 "service:topic-tracking-state"
3737 ) ;
38- let composerSavedFromNested = false ;
38+ let composerSaveInfo = null ;
3939
4040 appEvents . on ( "composer:saved" , ( ) => {
4141 if ( ! siteSettings . nested_replies_enabled ) {
@@ -44,7 +44,11 @@ export default apiInitializer((api) => {
4444
4545 const route = router . currentRouteName ;
4646 if ( route ?. startsWith ( "nested" ) ) {
47- composerSavedFromNested = true ;
47+ const nestedController = api . container . lookup ( "controller:nested" ) ;
48+ composerSaveInfo = {
49+ topicId : nestedController ?. topic ?. id ,
50+ time : Date . now ( ) ,
51+ } ;
4852 }
4953 } ) ;
5054
@@ -100,10 +104,17 @@ export default apiInitializer((api) => {
100104 }
101105
102106 // After composer save on nested route, suppress the redirect to flat view.
103- // Returning null tells routeTo to abort navigation.
104- if ( composerSavedFromNested && / ^ \/ t \/ / . test ( path ) ) {
105- composerSavedFromNested = false ;
106- return null ;
107+ // Returning null tells routeTo to abort navigation. Scoped to the same
108+ // topic and expires after 5 seconds to prevent stale flags from
109+ // suppressing unrelated navigations.
110+ if ( composerSaveInfo && / ^ \/ t \/ / . test ( path ) ) {
111+ const match = TOPIC_URL_RE . exec ( path ) ;
112+ const elapsed = Date . now ( ) - composerSaveInfo . time ;
113+ const savedTopicId = composerSaveInfo . topicId ;
114+ composerSaveInfo = null ;
115+ if ( match && parseInt ( match [ 2 ] , 10 ) === savedTopicId && elapsed < 5000 ) {
116+ return null ;
117+ }
107118 }
108119
109120 // If already in flat view, don't redirect to nested (e.g. timeline navigation).
@@ -152,7 +163,8 @@ export default apiInitializer((api) => {
152163 // state yet), intercept before the topic route's model hook runs.
153164 // When the topic isn't in tracking state, abort and fetch topic info
154165 // to determine the category before deciding which view to load.
155- const checkedTopicIds = new Set ( ) ;
166+ const checkedTopicIds = new Map ( ) ;
167+ const CHECKED_TOPIC_TTL_MS = 60_000 ;
156168
157169 router . on ( "routeWillChange" , ( transition ) => {
158170 if ( ! siteSettings . nested_replies_enabled ) {
@@ -205,15 +217,32 @@ export default apiInitializer((api) => {
205217
206218 // Topic not in tracking state (e.g. direct URL entry). Abort, look
207219 // up the category via a lightweight request, then redirect or resume.
208- if ( checkedTopicIds . has ( topicId ) ) {
220+ const checkedAt = checkedTopicIds . get ( topicId ) ;
221+ if ( checkedAt && Date . now ( ) - checkedAt < CHECKED_TOPIC_TTL_MS ) {
209222 return ;
210223 }
211- checkedTopicIds . add ( topicId ) ;
224+ checkedTopicIds . set ( topicId , Date . now ( ) ) ;
225+
226+ // Evict stale entries to prevent unbounded growth
227+ if ( checkedTopicIds . size > 100 ) {
228+ const now = Date . now ( ) ;
229+ for ( const [ id , time ] of checkedTopicIds ) {
230+ if ( now - time > CHECKED_TOPIC_TTL_MS ) {
231+ checkedTopicIds . delete ( id ) ;
232+ }
233+ }
234+ }
212235
236+ const fromRoute = router . currentRouteName ;
213237 transition . abort ( ) ;
214238
215239 ajax ( `/t/${ topicId } .json` , { data : { track_visit : false } } )
216240 . then ( ( data ) => {
241+ // Bail if user navigated away during the async lookup
242+ if ( router . currentRouteName !== fromRoute ) {
243+ return ;
244+ }
245+
217246 if ( isNestedDefault ( siteSettings , data . category_id ) ) {
218247 const queryParams = { } ;
219248 const nearPost = transition . to ?. params ?. nearPost ;
@@ -226,7 +255,9 @@ export default apiInitializer((api) => {
226255 }
227256 } )
228257 . catch ( ( ) => {
229- // On error, let the normal topic route handle it
258+ if ( router . currentRouteName !== fromRoute ) {
259+ return ;
260+ }
230261 transition . retry ( ) ;
231262 } ) ;
232263 } ) ;
0 commit comments