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
19 changed files with 515 additions and 2426 deletions

View File

@@ -240,6 +240,10 @@ jobs:
set -u
DEPLOY_BUCKET=${STATIC_DEPLOY_BUCKET:-${{ env.PROJECT }}-${{ env.STACK }}}
NEW_BUCKET="${PROJECT}-${STACK}-console"
if aws s3api head-bucket --bucket "$NEW_BUCKET" 2>/dev/null; then
DEPLOY_BUCKET="$NEW_BUCKET"
fi
# this value will come from the dotenv above
echo "Deploying to $DEPLOY_BUCKET"

View File

@@ -24,15 +24,6 @@
"fleetbase/fleetops-api": "^0.6.35",
"fleetbase/registry-bridge": "^0.1.5",
"fleetbase/storefront-api": "^0.4.13",
"fleetbase/aws-marketplace": "^0.0.8",
"fleetbase/billing-api": "^0.1.20",
"fleetbase/customer-portal-api": "^0.0.10",
"fleetbase/flespi-integration": "^0.1.16",
"fleetbase/internals-api": "^0.0.27",
"fleetbase/samsara-api": "^0.0.3",
"fleetbase/solid-api": "^0.0.7",
"fleetbase/valhalla-api": "^0.0.3",
"fleetbase/vroom-api": "^0.0.3",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",
@@ -61,14 +52,6 @@
{
"type": "composer",
"url": "https://registry.fleetbase.io"
},
{
"type": "vcs",
"url": "https://github.com/fleetbase/aws-marketplace"
},
{
"type": "vcs",
"url": "https://github.com/fleetbase/internals"
}
],
"autoload": {

1495
api/composer.lock generated

File diff suppressed because it is too large Load Diff

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
};
}
}

View File

@@ -1,9 +1,8 @@
API_HOST=https://api.fleetbase.io
API_HOST=
API_NAMESPACE=int/v1
API_SECURE=true
SOCKETCLUSTER_PATH=/socketcluster/
SOCKETCLUSTER_HOST=socket.fleetbase.io
SOCKETCLUSTER_HOST=
SOCKETCLUSTER_SECURE=true
SOCKETCLUSTER_PORT=8000
OSRM_HOST=https://router.project-osrm.org
DISABLE_RUNTIME_CONFIG=true
SOCKETCLUSTER_PORT=38000
OSRM_HOST=https://router.project-osrm.org

View File

@@ -1,9 +0,0 @@
API_HOST=https://api.qa.fleetbase.io
API_NAMESPACE=int/v1
API_SECURE=true
SOCKETCLUSTER_PATH=/socketcluster/
SOCKETCLUSTER_HOST=socket.qa.fleetbase.io
SOCKETCLUSTER_SECURE=true
SOCKETCLUSTER_PORT=8000
OSRM_HOST=https://router.project-osrm.org
DISABLE_RUNTIME_CONFIG=true

View File

@@ -33,24 +33,15 @@
},
"dependencies": {
"@ember/legacy-built-in-components": "^0.4.2",
"@fleetbase/aws-marketplace": "^0.0.8",
"@fleetbase/billing-engine": "^0.1.20",
"@fleetbase/customer-portal-engine": "^0.0.10",
"@fleetbase/dev-engine": "^0.2.12",
"@fleetbase/ember-core": "^0.3.10",
"@fleetbase/ember-ui": "^0.3.18",
"@fleetbase/fleetops-data": "^0.1.25",
"@fleetbase/fleetops-engine": "^0.6.35",
"@fleetbase/flespi-engine": "^0.1.16",
"@fleetbase/iam-engine": "^0.1.6",
"@fleetbase/internals-engine": "^0.0.27",
"@fleetbase/leaflet-routing-machine": "^3.2.17",
"@fleetbase/registry-bridge-engine": "^0.1.5",
"@fleetbase/samsara-engine": "^0.0.3",
"@fleetbase/solid-engine": "^0.0.7",
"@fleetbase/storefront-engine": "^0.4.13",
"@fleetbase/valhalla-engine": "^0.0.3",
"@fleetbase/vroom-engine": "^0.0.3",
"@formatjs/intl-datetimeformat": "^6.18.2",
"@formatjs/intl-numberformat": "^8.15.6",
"@formatjs/intl-pluralrules": "^5.4.6",

1233
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff