Skip to content

Commit 057112b

Browse files
authored
Merge pull request #196 from EngineScript/copilot/fix-error-message-on-preferences
fix: improve localStorage error handling, request deduplication, and preference loading in external-services.js
2 parents 2818615 + 1b75039 commit 057112b

1 file changed

Lines changed: 85 additions & 51 deletions

File tree

config/var/www/admin/control-panel/external-services/external-services.js

Lines changed: 85 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)