Initial commit

This commit is contained in:
Alois 2026-03-10 20:44:25 +01:00
commit 2f5e10140c
133 changed files with 10429 additions and 0 deletions

View 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"
}
}

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

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

View 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"]
}