136 lines
No EOL
3.8 KiB
TypeScript
136 lines
No EOL
3.8 KiB
TypeScript
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<K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]>;
|
|
save<K extends keyof StorageSchema>(
|
|
key: K,
|
|
value: StorageSchema[K],
|
|
): Promise<void>;
|
|
clear: () => Promise<void>;
|
|
}
|
|
|
|
const StorageContext = React.createContext<StorageContextValue | undefined>(
|
|
undefined,
|
|
);
|
|
|
|
const isIndexedDBSupported = typeof indexedDB !== "undefined";
|
|
|
|
export default function StorageProvider(props: { children: React.ReactNode }) {
|
|
const [storage, setStorage] = React.useState<StorageSchema>(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 <K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]> => {
|
|
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 <K extends keyof StorageSchema>(
|
|
key: K,
|
|
value: StorageSchema[K],
|
|
): Promise<void> => {
|
|
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<StorageContextValue>(
|
|
() => ({
|
|
async load<K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]> {
|
|
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<K extends keyof StorageSchema>(
|
|
key: K,
|
|
nextValue: StorageSchema[K],
|
|
): Promise<void> {
|
|
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 <ErrorScreen error={error} description={errorDescription} />;
|
|
}
|
|
|
|
if (!isIndexedDBSupported) {
|
|
return (
|
|
<ErrorScreen
|
|
error="Unsupported Browser"
|
|
description="Your browser does not support IndexedDB, which is required for this application to function."
|
|
/>
|
|
);
|
|
}
|
|
|
|
return <StorageContext.Provider value={value}>{props.children}</StorageContext.Provider>;
|
|
}
|
|
|
|
export function useStorage(): StorageContextValue {
|
|
const context = React.useContext(StorageContext);
|
|
if (!context) {
|
|
throw new Error("useStorage must be used within a StorageProvider");
|
|
}
|
|
return context;
|
|
} |