@@ -756,10 +756,28 @@ export class ExternalServicesManager {
756756 this . applyPreferenceChanges ( currentPreferences , safeChanges ) ;
757757
758758 // Save preferences to local storage (avoid tamper-prone cookie storage)
759+ const storageTestKey = '__servicePreferences_storage_test__' ;
759760 try {
760- window . localStorage . setItem ( 'servicePreferences' , JSON . stringify ( currentPreferences ) ) ;
761+ globalThis . localStorage . setItem ( storageTestKey , '1' ) ;
762+ globalThis . localStorage . removeItem ( storageTestKey ) ;
763+ } catch ( availabilityError ) {
764+ console . error ( 'localStorage availability check failed:' , availabilityError ) ;
765+ throw new Error ( 'Unable to save preferences: browser storage is unavailable or disabled.' ) ;
766+ }
767+
768+ try {
769+ globalThis . localStorage . setItem ( 'servicePreferences' , JSON . stringify ( currentPreferences ) ) ;
761770 } catch ( storageError ) {
762- throw new Error ( 'Unable to save preferences: browser storage is full or disabled.' ) ;
771+ const isQuotaExceeded = storageError && (
772+ storageError . name === 'QuotaExceededError' ||
773+ storageError . name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
774+ storageError . code === 22 ||
775+ storageError . code === 1014
776+ ) ;
777+ if ( isQuotaExceeded ) {
778+ throw new Error ( 'Unable to save preferences: browser storage is full.' ) ;
779+ }
780+ throw new Error ( 'Unable to save preferences: browser storage is unavailable or disabled.' ) ;
763781 }
764782 this . applyPreferenceChanges ( services , safeChanges ) ;
765783
@@ -1091,35 +1109,48 @@ export class ExternalServicesManager {
10911109 * @returns {Promise<Object> } Promise resolving to service data (from cache or parsed JSON response)
10921110 */
10931111 async fetchServiceData ( fetchFn , serviceKey ) {
1112+ // Initialize in-flight request map lazily if not already set
1113+ if ( ! this . inFlightRequests ) {
1114+ this . inFlightRequests = { } ;
1115+ }
1116+
10941117 // Check cache first - no need to queue if cached
10951118 let data = this . getCachedService ( serviceKey ) ;
1096-
1119+
10971120 if ( ! data ) {
1098- // Not in cache, queue the fetch request with concurrency limiting
1099- data = await this . queueRequest ( async ( ) => {
1100- const controller = new AbortController ( ) ;
1101- const timeoutId = setTimeout ( ( ) => controller . abort ( ) , this . requestTimeoutMs ) ;
1102-
1103- try {
1104- const response = await fetchFn ( controller . signal ) ;
1105- clearTimeout ( timeoutId ) ;
1106-
1107- if ( ! response . ok ) {
1108- throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
1121+ // Reuse existing in-flight request for this serviceKey to avoid duplicate fetches
1122+ if ( ! this . inFlightRequests [ serviceKey ] ) {
1123+ this . inFlightRequests [ serviceKey ] = this . queueRequest ( async ( ) => {
1124+ const controller = new AbortController ( ) ;
1125+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , this . requestTimeoutMs ) ;
1126+
1127+ try {
1128+ const response = await fetchFn ( controller . signal ) ;
1129+ clearTimeout ( timeoutId ) ;
1130+
1131+ if ( ! response . ok ) {
1132+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
1133+ }
1134+
1135+ const responseData = await response . json ( ) ;
1136+
1137+ // Cache the response
1138+ this . setCachedService ( serviceKey , responseData ) ;
1139+ return responseData ;
1140+ } catch ( error ) {
1141+ clearTimeout ( timeoutId ) ;
1142+ throw error ;
11091143 }
1110-
1111- const responseData = await response . json ( ) ;
1112-
1113- // Cache the response
1114- this . setCachedService ( serviceKey , responseData ) ;
1115- return responseData ;
1116- } catch ( error ) {
1117- clearTimeout ( timeoutId ) ;
1118- throw error ;
1119- }
1120- } ) ;
1144+ } ) ;
1145+ }
1146+
1147+ try {
1148+ data = await this . inFlightRequests [ serviceKey ] ;
1149+ } finally {
1150+ delete this . inFlightRequests [ serviceKey ] ;
1151+ }
11211152 }
1122-
1153+
11231154 return data ;
11241155 }
11251156
@@ -1311,36 +1342,39 @@ export class ExternalServicesManager {
13111342 * @returns {Object|null } Parsed preferences object or null if not found/invalid
13121343 */
13131344 loadServicePreferences ( ) {
1345+ // Try to load from local storage
1346+ let storedPrefs = null ;
13141347 try {
1315- // Try to load from local storage
1316- let storedPrefs = null ;
1317- try {
1318- storedPrefs = window . localStorage . getItem ( 'servicePreferences' ) ;
1319- } catch ( storageError ) {
1320- console . error ( 'Failed to access localStorage for service preferences:' , storageError ) ;
1321- return null ;
1322- }
1348+ storedPrefs = globalThis . localStorage . getItem ( 'servicePreferences' ) ;
1349+ } catch ( storageError ) {
1350+ console . error ( 'Failed to access localStorage for service preferences:' , storageError ) ;
1351+ return null ;
1352+ }
13231353
1324- if ( storedPrefs ) {
1325- try {
1326- const parsed = JSON . parse ( storedPrefs ) ;
1327- // Validate it's an object with expected structure
1328- if ( typeof parsed === 'object' && parsed !== null && ! Array . isArray ( parsed ) ) {
1329- return parsed ;
1330- }
1331- } catch ( parseError ) {
1332- console . error ( 'Failed to parse stored preferences:' , parseError ) ;
1333- // Clear invalid entry
1334- window . localStorage . removeItem ( 'servicePreferences' ) ;
1335- }
1336- }
1337-
1354+ if ( ! storedPrefs ) {
13381355 // Return null if no valid preferences found - will use defaults
13391356 return null ;
1340- } catch ( error ) {
1341- console . error ( 'Failed to load service preferences:' , error ) ;
1342- return null ;
13431357 }
1358+
1359+ try {
1360+ const parsed = JSON . parse ( storedPrefs ) ;
1361+ // Validate it's an object with expected structure
1362+ if ( typeof parsed === 'object' && parsed !== null && ! Array . isArray ( parsed ) ) {
1363+ return parsed ;
1364+ }
1365+ } catch ( parseError ) {
1366+ console . error ( 'Failed to parse stored preferences:' , parseError ) ;
1367+ console . warn ( 'Corrupted service preferences detected; resetting stored preferences to defaults.' ) ;
1368+ // Clear invalid entry
1369+ try {
1370+ globalThis . localStorage . removeItem ( 'servicePreferences' ) ;
1371+ } catch ( removeError ) {
1372+ console . error ( 'Failed to clear invalid stored preferences:' , removeError ) ;
1373+ }
1374+ }
1375+
1376+ // Return null if no valid preferences found - will use defaults
1377+ return null ;
13441378 }
13451379
13461380 /**
0 commit comments