## What does this PR do? Implements an IndexedDB storage adapter for the onboarding store to replace localStorage. This allows storing much larger data (up to 50% of available disk space) compared to localStorage's limited quota, making it suitable for storing base64-encoded images like logos and banners during onboarding. The implementation includes a fallback to localStorage if IndexedDB is unavailable. Sometimes when you uploaded a large image or banner you get this error. Even if its within the bounds of 10mb (which is what we allow)  ## Visual Demo (For contributors especially) N/A - This is an infrastructure change with no visual component. ## Mandatory Tasks (DO NOT REMOVE) - [x] I have self-reviewed the code. - [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? - Test the onboarding flow and verify that data persists between page refreshes - Test uploading large images during onboarding to ensure they're properly stored - Test in browsers with IndexedDB disabled to verify the localStorage fallback works correctly - Verify that the onboarding state is properly maintained when navigating through the onboarding steps ## Checklist - I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - My code follows the style guidelines of this project - I have commented my code, particularly in hard-to-understand areas - I have checked if my changes generate no new warnings
133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import { createJSONStorage, type StateStorage } from "zustand/middleware";
|
|
|
|
/**
|
|
* IndexedDB storage adapter for onboarding store.
|
|
* Supports much larger storage limits than localStorage (typically 50% of available disk space),
|
|
* making it suitable for storing large base64-encoded images (logos, banners, etc.).
|
|
* Falls back to localStorage if IndexedDB is not available.
|
|
*/
|
|
const DB_NAME = "cal-onboarding-idb";
|
|
const DB_VERSION = 1;
|
|
const STORE_NAME = "keyval";
|
|
|
|
// Cache database connection to avoid opening/closing repeatedly
|
|
let dbPromise: Promise<IDBDatabase> | null = null;
|
|
|
|
function getDB(): Promise<IDBDatabase> {
|
|
if (dbPromise) {
|
|
return dbPromise;
|
|
}
|
|
|
|
dbPromise = new Promise((resolve, reject) => {
|
|
if (typeof window === "undefined" || !window.indexedDB) {
|
|
reject(new Error("IndexedDB is not available"));
|
|
return;
|
|
}
|
|
|
|
const request = window.indexedDB.open(DB_NAME, DB_VERSION);
|
|
|
|
request.onerror = () => {
|
|
dbPromise = null;
|
|
reject(request.error);
|
|
};
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = (event.target as IDBOpenDBRequest).result;
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
db.createObjectStore(STORE_NAME);
|
|
}
|
|
};
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result;
|
|
// Reset promise on close so we can reconnect if needed
|
|
db.onclose = () => {
|
|
dbPromise = null;
|
|
};
|
|
resolve(db);
|
|
};
|
|
});
|
|
|
|
return dbPromise;
|
|
}
|
|
|
|
async function getItem(key: string): Promise<string | null> {
|
|
try {
|
|
const db = await getDB();
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction([STORE_NAME], "readonly");
|
|
const store = transaction.objectStore(STORE_NAME);
|
|
const request = store.get(key);
|
|
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => resolve(request.result ?? null);
|
|
});
|
|
} catch {
|
|
// Fallback to localStorage if IndexedDB fails
|
|
try {
|
|
return window.localStorage.getItem(key);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function setItem(key: string, value: string): Promise<void> {
|
|
try {
|
|
const db = await getDB();
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction([STORE_NAME], "readwrite");
|
|
const store = transaction.objectStore(STORE_NAME);
|
|
const request = store.put(value, key);
|
|
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => resolve();
|
|
});
|
|
} catch {
|
|
// Fallback to localStorage if IndexedDB fails (will throw if quota exceeded)
|
|
try {
|
|
window.localStorage.setItem(key, value);
|
|
} catch {
|
|
// Storage quota exceeded even in fallback - not much we can do
|
|
}
|
|
}
|
|
}
|
|
|
|
async function removeItem(key: string): Promise<void> {
|
|
try {
|
|
const db = await getDB();
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction([STORE_NAME], "readwrite");
|
|
const store = transaction.objectStore(STORE_NAME);
|
|
const request = store.delete(key);
|
|
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => resolve();
|
|
});
|
|
} catch {
|
|
// Fallback to localStorage if IndexedDB fails
|
|
try {
|
|
window.localStorage.removeItem(key);
|
|
} catch {
|
|
// Ignore errors on remove
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* IndexedDB storage adapter that implements StateStorage interface.
|
|
* This provides much larger storage capacity than localStorage, making it suitable for
|
|
* storing large base64-encoded images (logos, banners, etc.).
|
|
*/
|
|
const indexedDBStateStorage: StateStorage = {
|
|
getItem,
|
|
setItem,
|
|
removeItem,
|
|
};
|
|
|
|
/**
|
|
* IndexedDB storage adapter that implements the PersistStorage interface for zustand persist.
|
|
* Uses createJSONStorage to handle JSON serialization/deserialization automatically.
|
|
*/
|
|
export const onboardingIndexedDBStorage = createJSONStorage(() => indexedDBStateStorage);
|