@@ -12,8 +12,9 @@ import {
1212 MESSAGE_TYPES ,
1313 NOTIFICATION_TYPES ,
1414 NOTIFICATION_TYPE_ICONS ,
15+ CONCURRENCY ,
1516} from "../lib/constants.js" ;
16- import { formatReason } from "../lib/format-utils.js" ;
17+ import { formatReason , classifyError } from "../lib/format-utils.js" ;
1718import { buildNotificationUrl } from "../lib/url-builder.js" ;
1819import { LRUCache , DEFAULT_LRU_CACHE_SIZE } from "../lib/lru-cache.js" ;
1920
@@ -279,6 +280,30 @@ function filterToCurrentlyStored(detailedNotifications, currentStoredNotificatio
279280 return detailedNotifications . filter ( ( n ) => currentStoredIds . has ( n . id ) ) ;
280281}
281282
283+ /**
284+ * Merge notifications with current storage and save, guarded by fetch version.
285+ * Aborts if a newer fetch has started (before or during the async storage read).
286+ * @param {number } fetchVersion - Version of the fetch that produced these notifications
287+ * @param {Array } notifications - Notifications to save
288+ * @param {string } label - Log label for debugging
289+ * @returns {Promise<boolean> } true if saved, false if superseded
290+ */
291+ async function mergeAndSaveIfCurrent ( fetchVersion , notifications , label ) {
292+ if ( fetchVersion < notificationFetchVersion ) {
293+ console . log ( `Fetch #${ fetchVersion } superseded before ${ label } , skipping` ) ;
294+ return false ;
295+ }
296+ const currentStored = await storage . getNotifications ( ) ;
297+ // Re-check after async read: user mark-as-read may have bumped the version
298+ if ( fetchVersion < notificationFetchVersion ) {
299+ console . log ( `Fetch #${ fetchVersion } superseded during ${ label } storage read, skipping` ) ;
300+ return false ;
301+ }
302+ const safe = filterToCurrentlyStored ( notifications , currentStored ) ;
303+ await storage . setNotifications ( safe ) ;
304+ return true ;
305+ }
306+
282307/**
283308 * Check for new notifications
284309 *
@@ -414,23 +439,22 @@ async function checkNotifications() {
414439 }
415440 }
416441
417- // Split into priority (first 10 visible) and background loading
418- const VISIBLE_COUNT = 10 ; // Approximately one screen of notifications
419- const priorityNotifications = notificationsNeedingDetails . slice ( 0 , VISIBLE_COUNT ) ;
420- const backgroundNotifications = notificationsNeedingDetails . slice ( VISIBLE_COUNT ) ;
442+ // Split into priority (visible) and background loading
443+ const priorityNotifications = notificationsNeedingDetails . slice ( 0 , CONCURRENCY . VISIBLE_COUNT ) ;
444+ const backgroundNotifications = notificationsNeedingDetails . slice ( CONCURRENCY . VISIBLE_COUNT ) ;
421445
422446 console . log (
423447 `Loading details: ${ priorityNotifications . length } priority, ${ backgroundNotifications . length } background` ,
424448 ) ;
425449
426- // Priority loading: First 10 notifications ( visible on screen)
450+ // Priority loading: First visible notifications
427451 let priorityResults = [ ] ; // Define in outer scope for background logging
428452 if ( priorityNotifications . length > 0 ) {
429453 priorityResults = await fetchWithConcurrencyLimit (
430454 priorityNotifications . map ( ( { notification : n , index } ) =>
431455 createDetailFetchTask ( n , index , detailedNotifications ) ,
432456 ) ,
433- 5 , // Concurrency limit: 5 requests at a time
457+ CONCURRENCY . PRIORITY ,
434458 ) ;
435459
436460 // Log priority loading results
@@ -440,21 +464,16 @@ async function checkNotifications() {
440464 `Fetch #${ currentFetchVersion } priority: ${ prioritySuccess } loaded, ${ priorityFailed } failed` ,
441465 ) ;
442466
443- // Check if superseded before updating
444- if ( currentFetchVersion >= notificationFetchVersion ) {
445- // Merge with current storage to avoid overwriting user deletions
446- const currentStored = await storage . getNotifications ( ) ;
447- // Re-check version after async read: a user mark-as-read may have bumped
448- // notificationFetchVersion between the read and write, making our snapshot stale.
449- if ( currentFetchVersion >= notificationFetchVersion ) {
450- const safeDetailed = filterToCurrentlyStored ( detailedNotifications , currentStored ) ;
451-
452- // Save priority notifications immediately
453- await storage . setNotifications ( safeDetailed ) ;
454- console . log (
455- `Fetch #${ currentFetchVersion } saved ${ priorityNotifications . length } priority notifications` ,
456- ) ;
457- }
467+ // Merge with current storage and save (guarded by version check)
468+ const saved = await mergeAndSaveIfCurrent (
469+ currentFetchVersion ,
470+ detailedNotifications ,
471+ "priority save" ,
472+ ) ;
473+ if ( saved ) {
474+ console . log (
475+ `Fetch #${ currentFetchVersion } saved ${ priorityNotifications . length } priority notifications` ,
476+ ) ;
458477 }
459478 }
460479
@@ -465,7 +484,7 @@ async function checkNotifications() {
465484 backgroundNotifications . map ( ( { notification : n , index } ) =>
466485 createDetailFetchTask ( n , index , detailedNotifications ) ,
467486 ) ,
468- 3 , // Lower concurrency for background: 3 requests at a time
487+ CONCURRENCY . BACKGROUND ,
469488 )
470489 . then ( async ( backgroundResults ) => {
471490 // Check if a newer fetch has completed while we were fetching details
@@ -490,27 +509,15 @@ async function checkNotifications() {
490509 `Author cache: ${ cacheStats . size } /${ cacheStats . maxSize } (${ cacheStats . utilization } )` ,
491510 ) ;
492511
493- // Double-check before final save
494- if ( currentFetchVersion >= notificationFetchVersion ) {
495- // Merge with current storage to avoid overwriting user deletions
496- const currentStoredNotifications = await storage . getNotifications ( ) ;
497- // Re-check version after async read: a user mark-as-read may have bumped
498- // notificationFetchVersion between the read and write, making our snapshot stale.
499- if ( currentFetchVersion >= notificationFetchVersion ) {
500- const safeDetailed = filterToCurrentlyStored (
501- detailedNotifications ,
502- currentStoredNotifications ,
503- ) ;
504-
505- // Update storage with all completed details
506- await storage . setNotifications ( safeDetailed ) ;
507- console . log (
508- `Fetch #${ currentFetchVersion } updated storage with detailed notifications` ,
509- ) ;
510- }
511- } else {
512+ // Merge with current storage and save (guarded by version check)
513+ const saved = await mergeAndSaveIfCurrent (
514+ currentFetchVersion ,
515+ detailedNotifications ,
516+ "background save" ,
517+ ) ;
518+ if ( saved ) {
512519 console . log (
513- `Fetch #${ currentFetchVersion } superseded before final save, skipping storage update ` ,
520+ `Fetch #${ currentFetchVersion } updated storage with detailed notifications ` ,
514521 ) ;
515522 }
516523 } )
@@ -529,27 +536,21 @@ async function checkNotifications() {
529536 console . error ( `Failed to check notifications (fetch #${ currentFetchVersion } ):` , error ) ;
530537
531538 // Handle different error types with appropriate UI feedback
532- if ( error . message && error . message . includes ( "Rate limited" ) ) {
533- // Rate limited - show timer badge with reset info
539+ const errorType = classifyError ( error ) ;
540+ if ( errorType === "rate- limited" ) {
534541 const rateLimitInfo = github . getRateLimitInfo ( ) ;
535542 await action . setBadgeText ( { text : "⏱" } ) ;
536543 await action . setBadgeBackgroundColor ( { color : BADGE_COLORS . RATE_LIMITED } ) ;
537544 await action . setTitle ( {
538545 title : `Rate limited. Resets ${ rateLimitInfo . resetIn || "soon" } ` ,
539546 } ) ;
540- } else if ( error . message && error . message . includes ( "timeout" ) ) {
541- // Network timeout
547+ } else if ( errorType === "timeout" ) {
542548 await action . setBadgeText ( { text : "⏱" } ) ;
543549 await action . setBadgeBackgroundColor ( { color : BADGE_COLORS . TIMEOUT } ) ;
544550 await action . setTitle ( { title : "Request timeout - will retry" } ) ;
545- } else if (
546- error . message &&
547- ( error . message . includes ( "NetworkError" ) || error . message . includes ( "Failed to fetch" ) )
548- ) {
549- // Network error - keep last known state, update title only
551+ } else if ( errorType === "offline" ) {
550552 await action . setTitle ( { title : "Offline - showing cached data" } ) ;
551553 } else {
552- // Other errors
553554 console . error ( "Unexpected error:" , error ) ;
554555 await action . setTitle ( { title : `Error: ${ error . message } ` } ) ;
555556 }
0 commit comments