import * as React from "react"; import { type Storage as StorageSchema, storageDefaults as defaults, } from "@tensamin/shared/data"; import { getEntry, setEntry, deleteEntry } from "./indexed-db"; import ErrorScreen from "@tensamin/ui/screens/error"; import { log } from "@tensamin/shared/log"; interface StorageContextValue { load(key: K): Promise; save( key: K, value: StorageSchema[K], ): Promise; clear: () => Promise; } const StorageContext = React.createContext( undefined, ); const isIndexedDBSupported = typeof indexedDB !== "undefined"; export default function StorageProvider(props: { children: React.ReactNode }) { const [storage, setStorage] = React.useState(defaults); const storageRef = React.useRef(storage); const [error, setError] = React.useState(""); const [errorDescription, setErrorDescription] = React.useState(""); React.useEffect(() => { storageRef.current = storage; }, [storage]); const loadIO = React.useCallback( async (key: K): Promise => { let stored: StorageSchema[K] | undefined; try { stored = await getEntry(key); } catch (err) { setError("Failed to load data"); setErrorDescription( "An error occurred while loading data from IndexedDB. Please try again.", ); log(0, "Storage", "red", err); } if (stored !== undefined) { setStorage((prev) => ({ ...prev, [key]: stored })); return stored; } return defaults[key]; }, [], ); const saveIO = React.useCallback( async ( key: K, value: StorageSchema[K], ): Promise => { if (JSON.stringify(value) === JSON.stringify(defaults[key])) { await deleteEntry(key); setStorage((prev) => ({ ...prev, [key]: defaults[key] })); } else { await setEntry(key, value); setStorage((prev) => ({ ...prev, [key]: value })); } }, [], ); const value = React.useMemo( () => ({ async load(key: K): Promise { const current = storageRef.current[key]; if ( current === undefined || JSON.stringify(current) === JSON.stringify(defaults[key]) ) { const loadedValue = await loadIO(key); setStorage((prev) => ({ ...prev, [key]: loadedValue })); return loadedValue; } return current; }, async save( key: K, nextValue: StorageSchema[K], ): Promise { await saveIO(key, nextValue); }, async clear() { const keys = Object.keys(defaults) as (keyof StorageSchema)[]; await Promise.all(keys.map((key) => deleteEntry(key))); setStorage(defaults); }, }), [loadIO, saveIO], ); React.useEffect(() => { // @ts-expect-error development utility window.save = value.save; }, [value]); if (error !== "" && errorDescription !== "") { return ; } if (!isIndexedDBSupported) { return ( ); } return {props.children}; } export function useStorage(): StorageContextValue { const context = React.useContext(StorageContext); if (!context) { throw new Error("useStorage must be used within a StorageProvider"); } return context; }