Compare commits

...

3 Commits

Author SHA1 Message Date
Ron
dccd1b6883 Merge pull request #500 from fleetbase/fix/onboarding-context-quota-error
fix: Add localStorage quota error handling to onboarding-context service
2026-02-12 15:49:48 +08:00
Ronald A. Richardson
5e69a7d443 v0.7.29 2026-02-12 15:48:02 +08:00
Ronald A Richardson
3dc5b9b015 fix: Add localStorage quota error handling to onboarding-context service
- Add _safeSet() and _safeGet() wrappers around appCache operations
- Implement in-memory fallback when localStorage quota is exceeded
- Add user notification when storage issues occur
- Track quota exceeded status to avoid duplicate notifications
- Add getStorageStatus() method for debugging
- Ensure onboarding can complete even with full localStorage
- Data persists in memory for current session when storage is full
2026-02-05 03:54:10 -05:00
4 changed files with 130 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { isArray } from '@ember/array';
import { debug } from '@ember/debug';
import { storageFor } from 'ember-local-storage';
import { add, isPast } from 'date-fns';
import { task } from 'ember-concurrency';

View File

@@ -6,20 +6,110 @@ const KEYS_INDEX = `${CONTEXT_PREFIX}__keys__`;
export default class OnboardingContextService extends Service {
@service appCache;
@service notifications;
@tracked data = {};
@tracked quotaExceeded = false;
@tracked usingMemoryFallback = false;
// In-memory fallback storage for when localStorage is full
_memoryCache = new Map();
/**
* Safe wrapper for appCache.set with quota error handling
*
* @param {string} key - The key to set
* @param {*} value - The value to store
* @returns {Object} Result object with success status and storage type
*/
_safeSet(key, value) {
try {
this.appCache.set(key, value);
return { success: true, storage: 'localStorage' };
} catch (error) {
if (this._isQuotaError(error)) {
console.warn(`[OnboardingContext] localStorage quota exceeded, using memory fallback for key: ${key}`);
// Store in memory as fallback
this._memoryCache.set(key, value);
// Mark that we're using fallback and notify user (only once)
if (!this.quotaExceeded) {
this.quotaExceeded = true;
this.usingMemoryFallback = true;
this._notifyUser();
}
return { success: true, storage: 'memory', warning: 'Using memory fallback' };
}
// Re-throw non-quota errors
throw error;
}
}
/**
* Safe wrapper for appCache.get with memory fallback
*
* @param {string} key - The key to retrieve
* @returns {*} The stored value or undefined
*/
_safeGet(key) {
try {
const value = this.appCache.get(key);
if (value !== undefined) {
return value;
}
} catch (error) {
console.warn(`[OnboardingContext] Error reading from appCache: ${error.message}`);
}
// Fallback to memory cache
return this._memoryCache.get(key);
}
/**
* Check if error is a quota exceeded error
*
* @param {Error} error - The error to check
* @returns {boolean} True if it's a quota error
*/
_isQuotaError(error) {
return (
error instanceof DOMException &&
(error.code === 22 ||
error.code === 1014 ||
error.name === 'QuotaExceededError' ||
error.name === 'NS_ERROR_DOM_QUOTA_REACHED')
);
}
/**
* Notify user about storage issues (only called once)
*/
_notifyUser() {
if (this.notifications) {
this.notifications.warning(
'Your browser storage is full. Your onboarding progress will be saved temporarily but may be lost if you close this tab. Please complete the onboarding process in this session.',
{
timeout: 10000,
clearDuration: 300
}
);
}
}
/**
* Get a value from in-memory state first, then fallback to cache
*/
get(key) {
return this.data[key] ?? this.appCache.get(`${CONTEXT_PREFIX}${key}`);
return this.data[key] ?? this._safeGet(`${CONTEXT_PREFIX}${key}`);
}
/**
* Get a value directly from cache
*/
getFromCache(key) {
return this.appCache.get(`${CONTEXT_PREFIX}${key}`);
return this._safeGet(`${CONTEXT_PREFIX}${key}`);
}
/**
@@ -28,11 +118,11 @@ export default class OnboardingContextService extends Service {
* @returns {Object}
*/
restore() {
const keys = this.appCache.get(KEYS_INDEX) ?? [];
const keys = this._safeGet(KEYS_INDEX) ?? [];
const persisted = {};
for (const key of keys) {
const value = this.appCache.get(`${CONTEXT_PREFIX}${key}`);
const value = this._safeGet(`${CONTEXT_PREFIX}${key}`);
if (value !== undefined) {
persisted[key] = value;
}
@@ -63,14 +153,14 @@ export default class OnboardingContextService extends Service {
this.data = { ...this.data, ...filteredData };
if (options.persist === true) {
const keys = new Set(this.appCache.get(KEYS_INDEX) ?? []);
const keys = new Set(this._safeGet(KEYS_INDEX) ?? []);
for (const key of Object.keys(filteredData)) {
keys.add(key);
this.appCache.set(`${CONTEXT_PREFIX}${key}`, this.data[key]);
this._safeSet(`${CONTEXT_PREFIX}${key}`, this.data[key]);
}
this.appCache.set(KEYS_INDEX, [...keys]);
this._safeSet(KEYS_INDEX, [...keys]);
}
}
@@ -88,11 +178,11 @@ export default class OnboardingContextService extends Service {
this.data = { ...this.data, [key]: value };
if (options.persist === true) {
const keys = new Set(this.appCache.get(KEYS_INDEX) ?? []);
const keys = new Set(this._safeGet(KEYS_INDEX) ?? []);
keys.add(key);
this.appCache.set(`${CONTEXT_PREFIX}${key}`, value);
this.appCache.set(KEYS_INDEX, [...keys]);
this._safeSet(`${CONTEXT_PREFIX}${key}`, value);
this._safeSet(KEYS_INDEX, [...keys]);
}
}
@@ -110,24 +200,44 @@ export default class OnboardingContextService extends Service {
const { [key]: _removed, ...rest } = this.data; // eslint-disable-line no-unused-vars
this.data = rest;
const keys = new Set(this.appCache.get(KEYS_INDEX) ?? []);
const keys = new Set(this._safeGet(KEYS_INDEX) ?? []);
keys.delete(key);
this.appCache.set(`${CONTEXT_PREFIX}${key}`, undefined);
this.appCache.set(KEYS_INDEX, [...keys]);
this._safeSet(`${CONTEXT_PREFIX}${key}`, undefined);
this._safeSet(KEYS_INDEX, [...keys]);
// Also remove from memory cache
this._memoryCache.delete(`${CONTEXT_PREFIX}${key}`);
}
/**
* Fully reset onboarding context (memory + persistence)
*/
reset() {
const keys = this.appCache.get(KEYS_INDEX) ?? [];
const keys = this._safeGet(KEYS_INDEX) ?? [];
for (const key of keys) {
this.appCache.set(`${CONTEXT_PREFIX}${key}`, undefined);
this._safeSet(`${CONTEXT_PREFIX}${key}`, undefined);
this._memoryCache.delete(`${CONTEXT_PREFIX}${key}`);
}
this.appCache.set(KEYS_INDEX, []);
this._safeSet(KEYS_INDEX, []);
this._memoryCache.clear();
this.data = {};
this.quotaExceeded = false;
this.usingMemoryFallback = false;
}
}
/**
* Get storage status for debugging
*
* @returns {Object} Storage status information
*/
getStorageStatus() {
return {
quotaExceeded: this.quotaExceeded,
usingMemoryFallback: this.usingMemoryFallback,
memoryItemCount: this._memoryCache.size
};
}
}