Initial commit
This commit is contained in:
commit
2f5e10140c
133 changed files with 10429 additions and 0 deletions
20
packages/core/storage/package.json
Normal file
20
packages/core/storage/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@tensamin/core-storage",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./context": "./src/context.tsx",
|
||||
"./indexed-db": "./src/indexed-db.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "bunx prettier --write .",
|
||||
"lint": "eslint src",
|
||||
"build": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"solid-js": "^1.9.11"
|
||||
}
|
||||
}
|
||||
131
packages/core/storage/src/context.tsx
Normal file
131
packages/core/storage/src/context.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import {
|
||||
createContext,
|
||||
createSignal,
|
||||
Show,
|
||||
useContext,
|
||||
type ParentProps,
|
||||
} from "solid-js";
|
||||
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 { createStore } from "solid-js/store";
|
||||
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 = createContext<StorageContextValue>();
|
||||
|
||||
const isIndexedDBSupported = typeof indexedDB !== "undefined";
|
||||
|
||||
export default function StorageProvider(props: ParentProps) {
|
||||
const [storage, setStorage] = createStore<StorageSchema>(defaults);
|
||||
|
||||
const [error, setError] = createSignal<string>("");
|
||||
const [errorDescription, setErrorDescription] = createSignal<string>("");
|
||||
|
||||
async function loadIO<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
): Promise<StorageSchema[K]> {
|
||||
let stored;
|
||||
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(key, stored);
|
||||
return stored;
|
||||
}
|
||||
return defaults[key];
|
||||
}
|
||||
|
||||
async function saveIO<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
value: StorageSchema[K],
|
||||
): Promise<void> {
|
||||
if (JSON.stringify(value) === JSON.stringify(defaults[key])) {
|
||||
await deleteEntry(key);
|
||||
setStorage(key, defaults[key]);
|
||||
} else {
|
||||
await setEntry(key, value);
|
||||
setStorage(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const value: StorageContextValue = {
|
||||
async load<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
): Promise<StorageSchema[K]> {
|
||||
if (
|
||||
storage[key] === undefined ||
|
||||
JSON.stringify(storage[key]) === JSON.stringify(defaults[key])
|
||||
) {
|
||||
const value = await loadIO(key);
|
||||
setStorage(key, value);
|
||||
}
|
||||
|
||||
return storage[key];
|
||||
},
|
||||
|
||||
async save<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
value: StorageSchema[K],
|
||||
): Promise<void> {
|
||||
await saveIO(key, value);
|
||||
setStorage(key, value);
|
||||
},
|
||||
|
||||
async clear() {
|
||||
const keys = Object.keys(defaults) as (keyof StorageSchema)[];
|
||||
await Promise.all(keys.map((key) => deleteEntry(key)));
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error development utility
|
||||
window.save = value.save;
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={error() !== "" && errorDescription() !== ""}
|
||||
fallback={
|
||||
<Show
|
||||
when={isIndexedDBSupported}
|
||||
fallback={
|
||||
<ErrorScreen
|
||||
error="Unsupported Browser"
|
||||
description="Your browser does not support IndexedDB, which is required for this application to function."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StorageContext.Provider value={value}>
|
||||
{props.children}
|
||||
</StorageContext.Provider>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<ErrorScreen error={error()} description={errorDescription()} />
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export function useStorage(): StorageContextValue {
|
||||
const context = useContext(StorageContext);
|
||||
if (!context) {
|
||||
throw new Error("useStorage must be used within a StorageProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
71
packages/core/storage/src/indexed-db.ts
Normal file
71
packages/core/storage/src/indexed-db.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import type { Storage as StorageSchema } from "@tensamin/shared/data";
|
||||
|
||||
const DB_NAME = "tensamin";
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = "storage";
|
||||
|
||||
let dbPromise: Promise<IDBDatabase> | null = null;
|
||||
|
||||
function openDB(): Promise<IDBDatabase> {
|
||||
if (dbPromise) return dbPromise;
|
||||
|
||||
dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
export async function getEntry<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
): Promise<StorageSchema[K] | undefined> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, "readonly");
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
const request = store.get(key as string);
|
||||
|
||||
request.onsuccess = () =>
|
||||
resolve(request.result as StorageSchema[K] | undefined);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function setEntry<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
value: StorageSchema[K],
|
||||
): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, "readwrite");
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
const request = store.put(value, key as string);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteEntry<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, "readwrite");
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
const request = store.delete(key as string);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
13
packages/core/storage/tsconfig.json
Normal file
13
packages/core/storage/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Loading…
Reference in a new issue