Refactor to have IndexedDB use id instead of name

This commit is contained in:
Jared Schoeny
2025-10-22 23:11:58 -10:00
parent 6d8c3bdbf4
commit 5c1b380203
8 changed files with 133 additions and 129 deletions

View File

@@ -83,7 +83,7 @@ export default async function HackDetail({ params }: HackDetailProps) {
title={hack.title}
version={patchVersion || "Pre-release"}
author={author}
baseRom={baseRom?.name || ""}
baseRomId={baseRom?.id || ""}
platform={baseRom?.platform}
patchUrl={signedPatchUrl}
/>

View File

@@ -11,7 +11,7 @@ interface HackActionsProps {
title: string;
version: string;
author: string;
baseRom: string;
baseRomId: string;
platform?: "GBA" | "GBC" | "GB" | "NDS";
patchUrl: string;
}
@@ -20,7 +20,7 @@ const HackActions: React.FC<HackActionsProps> = ({
title,
version,
author,
baseRom,
baseRomId,
platform,
patchUrl,
}) => {
@@ -30,10 +30,10 @@ const HackActions: React.FC<HackActionsProps> = ({
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
if (isLinked(baseRom) && (hasPermission(baseRom) || hasCached(baseRom))) {
if (isLinked(baseRomId) && (hasPermission(baseRomId) || hasCached(baseRomId))) {
setStatus("ready");
}
}, [baseRom, isLinked, hasPermission, hasCached]);
}, [baseRomId, isLinked, hasPermission, hasCached]);
function onSelectFile(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0] ?? null;
@@ -47,12 +47,12 @@ const HackActions: React.FC<HackActionsProps> = ({
setError(null);
let baseFile = file;
if (!baseFile) {
if (!isLinked(baseRom) && !hasCached(baseRom)) return;
if (!hasCached(baseRom)) {
const perm = await ensurePermission(baseRom, true);
if (!isLinked(baseRomId) && !hasCached(baseRomId)) return;
if (!hasCached(baseRomId)) {
const perm = await ensurePermission(baseRomId, true);
if (perm !== "granted") return;
}
const linkedFile = await getFileBlob(baseRom);
const linkedFile = await getFileBlob(baseRomId);
if (!linkedFile) return;
baseFile = linkedFile;
}
@@ -100,9 +100,9 @@ const HackActions: React.FC<HackActionsProps> = ({
author={author}
onPatch={onPatch}
status={status}
isLinked={isLinked(baseRom)}
ready={hasPermission(baseRom) || hasCached(baseRom)}
onClickLink={() => (isLinked(baseRom) ? ensurePermission(baseRom, true) : linkRom(baseRom))}
isLinked={isLinked(baseRomId)}
ready={hasPermission(baseRomId) || hasCached(baseRomId)}
onClickLink={() => (isLinked(baseRomId) ? ensurePermission(baseRomId, true) : linkRom(baseRomId))}
supported={supported}
onUploadChange={onSelectFile}
/>

View File

@@ -39,9 +39,9 @@ export default function HackPatchForm(props: HackPatchFormProps) {
const baseRomName = baseRomEntry?.name;
const { isLinked, hasPermission, hasCached, importUploadedBlob, ensurePermission, getFileBlob, supported } = useBaseRoms();
const baseRomReady = !!baseRomName && (hasPermission(baseRomName) || hasCached(baseRomName));
const baseRomNeedsPermission = !!baseRomName && isLinked(baseRomName) && !baseRomReady;
const baseRomMissing = !!baseRomName && !isLinked(baseRomName) && !hasCached(baseRomName);
const baseRomReady = !!baseRomId && (hasPermission(baseRomId) || hasCached(baseRomId));
const baseRomNeedsPermission = !!baseRomId && isLinked(baseRomId) && !baseRomReady;
const baseRomMissing = !!baseRomId && !isLinked(baseRomId) && !hasCached(baseRomId);
const isVersionTaken = version.trim() && existingVersions.includes(version.trim());
const canSubmit = React.useMemo(() => {
@@ -78,8 +78,8 @@ export default function HackPatchForm(props: HackPatchFormProps) {
}, [patchMode]);
async function onGrantPermission() {
if (!baseRomName) return;
await ensurePermission(baseRomName, true);
if (!baseRomId) return;
await ensurePermission(baseRomId, true);
}
async function onUploadBaseRom(e: React.ChangeEvent<HTMLInputElement>) {
@@ -92,7 +92,7 @@ export default function HackPatchForm(props: HackPatchFormProps) {
setGenError("That ROM doesn't match any supported base ROM.");
return;
}
if (matched !== baseRomName) {
if (matched !== baseRomId) {
setGenError(`This ROM matches "${matched}", but this hack requires "${baseRomName}".`);
return;
}
@@ -106,11 +106,11 @@ export default function HackPatchForm(props: HackPatchFormProps) {
setGenStatus("generating");
setGenError("");
const mod = e.target.files?.[0] || null;
if (!mod || !baseRomName) {
if (!mod || !baseRomId) {
setGenStatus("idle");
return;
}
let baseFile = await getFileBlob(baseRomName);
let baseFile = await getFileBlob(baseRomId);
if (!baseFile) {
setGenStatus("idle");
setGenError("Base ROM not available.");

View File

@@ -97,9 +97,9 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
const baseRomPlatform = baseRomEntry?.platform;
const { isLinked, hasPermission, hasCached, importUploadedBlob, ensurePermission, getFileBlob, supported } = useBaseRoms();
const baseRomReady = baseRomName && (hasPermission(baseRomName) || hasCached(baseRomName));
const baseRomNeedsPermission = baseRomName && isLinked(baseRomName) && !baseRomReady;
const baseRomMissing = baseRomName && !isLinked(baseRomName) && !hasCached(baseRomName);
const baseRomReady = baseRom && (hasPermission(baseRom) || hasCached(baseRom));
const baseRomNeedsPermission = baseRom && isLinked(baseRom) && !baseRomReady;
const baseRomMissing = baseRom && !isLinked(baseRom) && !hasCached(baseRom);
const coverPreviews = React.useMemo(() => {
return newCoverFiles.map((f) => URL.createObjectURL(f));
@@ -251,8 +251,8 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
};
async function onGrantPermission() {
if (!baseRomName) return;
await ensurePermission(baseRomName, true);
if (!baseRom) return;
await ensurePermission(baseRom, true);
}
async function onUploadBaseRom(e: React.ChangeEvent<HTMLInputElement>) {
@@ -260,13 +260,14 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
setGenError("");
const f = e.target.files?.[0];
if (!f) return;
const matchedName = await importUploadedBlob(f);
if (!matchedName) {
const matchedId = await importUploadedBlob(f);
if (!matchedId) {
setGenError("That ROM doesn't match any supported base ROM.");
return;
}
if (matchedName !== baseRomName) {
setGenError(`This ROM matches "${matchedName}", but the form requires "${baseRomName}".`);
if (matchedId !== baseRom) {
const matchedName = baseRoms.find(r => r.id === matchedId)?.name;
setGenError(`This ROM matches "${matchedName ?? matchedId}", but the form requires "${baseRomName}".`);
return;
}
} catch {
@@ -279,11 +280,11 @@ export default function HackSubmitForm({ dummy = false }: HackSubmitFormProps) {
setGenStatus("generating");
setGenError("");
const mod = e.target.files?.[0] || null;
if (!mod || !baseRomName) {
if (!mod || !baseRom) {
setGenStatus("idle");
return;
}
let baseFile = await getFileBlob(baseRomName);
let baseFile = await getFileBlob(baseRom);
if (!baseFile) {
setGenStatus("idle");
setGenError("Base ROM not available.");

View File

@@ -25,9 +25,9 @@ type CardHack = {
export default function HackCard({ hack, clickable = true, className = "" }: { hack: CardHack; clickable?: boolean; className?: string }) {
const { isLinked, hasPermission, hasCached } = useBaseRoms();
const match = baseRoms.find((r) => r.id === hack.baseRomId);
const baseName = match?.name ?? undefined;
const linked = baseName ? isLinked(baseName) : false;
const ready = baseName ? hasPermission(baseName) || hasCached(baseName) : false;
const baseId = match?.id ?? undefined;
const linked = baseId ? isLinked(baseId) : false;
const ready = baseId ? hasPermission(baseId) || hasCached(baseId) : false;
const images = (hack.covers && hack.covers.length > 0 ? hack.covers : []).filter(Boolean);
const isCarousel = images.length > 1;
const pathname = usePathname();
@@ -156,7 +156,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
return text.length > 120 ? text.slice(0, 120).trimEnd() + "…" : text;
})()}
</p>
<div className="mt-3 text-xs text-foreground/60">Base: {baseName ?? "Unknown"}</div>
<div className="mt-3 text-xs text-foreground/60">Base: {baseId ?? "Unknown"}</div>
</div>
</div>
);

View File

@@ -17,14 +17,15 @@ export default function RomsInteractive() {
async function onUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
const name = await importUploadedBlob(file);
const id = await importUploadedBlob(file);
const name = baseRoms.find(r => r.id === id)?.name;
if (name) setUploadMsg(`Recognized and cached: ${name}`);
else setUploadMsg("Unrecognized ROM. Not cached.");
e.target.value = "";
}
const linkedOrCached = baseRoms.filter(({ name }) => Boolean(cached[name]) || Boolean(linked[name]));
const notLinked = baseRoms.filter(({ name }) => !cached[name] && !linked[name]);
const linkedOrCached = baseRoms.filter(({ id }) => Boolean(cached[id]) || Boolean(linked[id]));
const notLinked = baseRoms.filter(({ id }) => !cached[id] && !linked[id]);
return (
<>
@@ -66,7 +67,8 @@ export default function RomsInteractive() {
setUploadMsg(`Unrecognized file. Not cached. Accepted types: ${ALLOWED_TYPES.join(", ")}`);
return;
}
const name = await importUploadedBlob(file);
const id = await importUploadedBlob(file);
const name = baseRoms.find(r => r.id === id)?.name;
if (name) setUploadMsg(`Recognized and cached: ${name}`);
else setUploadMsg("Unrecognized ROM. Not cached.");
}}
@@ -98,23 +100,24 @@ export default function RomsInteractive() {
<div className="mt-6">
<div className="mb-3 text-xs font-medium uppercase tracking-wide text-foreground/70">Linked or cached</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{linkedOrCached.map(({ name, platform, region }) => {
const isLinked = Boolean(linked[name]);
const status = statuses[name] ?? (isLinked ? "prompt" : "denied");
const isCached = Boolean(cached[name]);
{linkedOrCached.map(({ id, platform, region }) => {
const name = baseRoms.find(r => r.id === id)?.name;
const isLinked = Boolean(linked[id]);
const status = statuses[id] ?? (isLinked ? "prompt" : "denied");
const isCached = Boolean(cached[id]);
return (
<BaseRomCard
key={name}
name={name}
key={id}
name={name ?? id}
platform={platform}
region={region}
isLinked={isLinked}
status={status}
isCached={isCached}
onRemoveCache={() => removeFromCache(name)}
onUnlink={() => unlinkRom(name)}
onEnsurePermission={() => ensurePermission(name, true)}
onImportCache={() => importToCache(name)}
onRemoveCache={() => removeFromCache(id)}
onUnlink={() => unlinkRom(id)}
onEnsurePermission={() => ensurePermission(id, true)}
onImportCache={() => importToCache(id)}
/>
);
})}
@@ -125,9 +128,9 @@ export default function RomsInteractive() {
<div className="mt-8">
<div className="mb-3 text-xs font-medium uppercase tracking-wide text-foreground/70">Not linked</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{notLinked.map(({ name, platform, region }) => (
{notLinked.map(({ id, name, platform, region }) => (
<BaseRomCard
key={name}
key={id}
name={name}
platform={platform}
region={region}

View File

@@ -14,17 +14,17 @@ type ContextValue = {
countGranted: number;
countReady: number;
totalCachedBytes: number;
isLinked: (name: string) => boolean;
hasPermission: (name: string) => boolean;
hasCached: (name: string) => boolean;
getHandle: (name: string) => any | null;
linkRom: (name: string) => Promise<void>;
unlinkRom: (name: string) => Promise<void>;
ensurePermission: (name: string, request?: boolean) => Promise<"granted" | "prompt" | "denied" | "error">;
getFileBlob: (name: string) => Promise<File | null>;
importToCache: (name: string) => Promise<void>;
removeFromCache: (name: string) => Promise<void>;
importUploadedBlob: (file: File) => Promise<string | null>; // returns matched name
isLinked: (id: string) => boolean;
hasPermission: (id: string) => boolean;
hasCached: (id: string) => boolean;
getHandle: (id: string) => any | null;
linkRom: (id: string) => Promise<void>;
unlinkRom: (id: string) => Promise<void>;
ensurePermission: (id: string, request?: boolean) => Promise<"granted" | "prompt" | "denied" | "error">;
getFileBlob: (id: string) => Promise<File | null>;
importToCache: (id: string) => Promise<void>;
removeFromCache: (id: string) => Promise<void>;
importUploadedBlob: (file: File) => Promise<string | null>; // returns matched id
};
const BaseRomContext = React.createContext<ContextValue | null>(null);
@@ -44,7 +44,7 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
const st: Record<string, "granted" | "prompt" | "denied" | "error"> = {};
const cacheState: Record<string, boolean> = {};
for (const r of rows) {
map[r.name] = r.handle;
map[r.id] = r.handle;
try {
const perm = r.handle?.queryPermission?.({ mode: "read" });
let state: any = "prompt";
@@ -52,16 +52,16 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
const result = await perm;
state = result;
}
st[r.name] = state === "granted" ? "granted" : state === "denied" ? "denied" : "prompt";
st[r.id] = state === "granted" ? "granted" : state === "denied" ? "denied" : "prompt";
} catch {
st[r.name] = "error";
st[r.id] = "error";
}
}
// Check existing blobs for all known bases (and linked ones)
const blobRows = await getAllBlobEntries();
let total = 0;
for (const row of blobRows) {
cacheState[row.name] = true;
cacheState[row.id] = true;
total += row.blob?.size ?? 0;
}
setLinked(map);
@@ -74,23 +74,23 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
})();
}, []);
function isLinked(name: string) {
return Boolean(linked[name]);
function isLinked(id: string) {
return Boolean(linked[id]);
}
function getHandle(name: string) {
return linked[name] ?? null;
function getHandle(id: string) {
return linked[id] ?? null;
}
function hasPermission(name: string) {
return statuses[name] === "granted";
function hasPermission(id: string) {
return statuses[id] === "granted";
}
function hasCached(name: string) {
return Boolean(cached[name]);
function hasCached(id: string) {
return Boolean(cached[id]);
}
async function linkRom(name: string) {
async function linkRom(id: string) {
if (!supported) {
// Fallback: use an <input type="file"> to allow selecting a ROM and cache it if recognized
try {
@@ -107,8 +107,8 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
const hash = await sha1Hex(file);
const match = baseRoms.find((r) => r.sha1 && r.sha1.toLowerCase() === hash.toLowerCase());
if (match) {
await setRomBlob(match.name, file);
setCached((prev) => ({ ...prev, [match.name]: true }));
await setRomBlob(match.id, file);
setCached((prev) => ({ ...prev, [match.id]: true }));
setTotalCachedBytes((n) => n + file.size);
}
} catch {}
@@ -141,30 +141,30 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
await handle.requestPermission({ mode: "read" });
}
}
await setRomHandle(name, handle);
setLinked((prev) => ({ ...prev, [name]: handle }));
await setRomHandle(id, handle);
setLinked((prev) => ({ ...prev, [id]: handle }));
try {
const q = await handle.queryPermission?.({ mode: "read" });
setStatuses((prev) => ({ ...prev, [name]: q === "granted" ? "granted" : q === "denied" ? "denied" : "prompt" }));
setStatuses((prev) => ({ ...prev, [id]: q === "granted" ? "granted" : q === "denied" ? "denied" : "prompt" }));
} catch {
setStatuses((prev) => ({ ...prev, [name]: "error" }));
setStatuses((prev) => ({ ...prev, [id]: "error" }));
}
} catch (e) {
// canceled or failed
}
}
async function unlinkRom(name: string) {
async function unlinkRom(id: string) {
try {
await deleteRomHandle(name);
await deleteRomHandle(id);
setLinked((prev) => {
const next = { ...prev };
delete next[name];
delete next[id];
return next;
});
setStatuses((prev) => {
const next = { ...prev };
delete next[name];
delete next[id];
return next;
});
// Note: keep cached copy unless explicitly removed
@@ -173,8 +173,8 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
}
}
async function ensurePermission(name: string, request = false) {
const handle = linked[name];
async function ensurePermission(id: string, request = false) {
const handle = linked[id];
if (!handle) return "error";
try {
let state = await handle.queryPermission?.({ mode: "read" });
@@ -182,19 +182,19 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
state = await handle.requestPermission({ mode: "read" });
}
const mapped = state === "granted" ? "granted" : state === "denied" ? "denied" : "prompt";
setStatuses((prev) => ({ ...prev, [name]: mapped }));
setStatuses((prev) => ({ ...prev, [id]: mapped }));
return mapped;
} catch {
setStatuses((prev) => ({ ...prev, [name]: "error" }));
setStatuses((prev) => ({ ...prev, [id]: "error" }));
return "error";
}
}
async function getFileBlob(name: string): Promise<File | null> {
async function getFileBlob(id: string): Promise<File | null> {
// Prefer cached
const cachedBlob = await getRomBlob(name);
if (cachedBlob) return new File([cachedBlob], name);
const handle = linked[name];
const cachedBlob = await getRomBlob(id);
if (cachedBlob) return new File([cachedBlob], id);
const handle = linked[id];
if (!handle) return null;
try {
const file = await handle.getFile();
@@ -205,25 +205,25 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
}
}
async function importToCache(name: string) {
const handle = linked[name];
async function importToCache(id: string) {
const handle = linked[id];
if (!handle) return;
try {
const file = await handle.getFile();
await setRomBlob(name, file);
setCached((prev) => ({ ...prev, [name]: true }));
await setRomBlob(id, file);
setCached((prev) => ({ ...prev, [id]: true }));
setTotalCachedBytes((n) => n + file.size);
} catch {
// noop
}
}
async function removeFromCache(name: string) {
async function removeFromCache(id: string) {
try {
await deleteRomBlob(name);
await deleteRomBlob(id);
setCached((prev) => {
const next = { ...prev };
delete next[name];
delete next[id];
return next;
});
// We cannot easily know the blob size now; recalc total
@@ -238,16 +238,16 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
}
}
// Accept a user-uploaded base ROM, hash it, and if it matches a known base ROM, cache it under that name
// Accept a user-uploaded base ROM, hash it, and if it matches a known base ROM, cache it under that id
async function importUploadedBlob(file: File): Promise<string | null> {
try {
const hash = await sha1Hex(file);
const match = baseRoms.find((r) => r.sha1 && r.sha1.toLowerCase() === hash.toLowerCase());
if (!match) return null;
await setRomBlob(match.name, file);
setCached((prev) => ({ ...prev, [match.name]: true }));
await setRomBlob(match.id, file);
setCached((prev) => ({ ...prev, [match.id]: true }));
setTotalCachedBytes((n) => n + file.size);
return match.name;
return match.id;
} catch {
return null;
}
@@ -262,9 +262,9 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
const quota = estimate?.quota ?? Infinity;
const usage = estimate?.usage ?? totalCachedBytes;
const headroom = quota - usage;
for (const name of names) {
if (cached[name]) continue;
const handle = linked[name];
for (const id of names) {
if (cached[id]) continue;
const handle = linked[id];
if (!handle) continue;
try {
const file = await handle.getFile();
@@ -272,8 +272,8 @@ export function BaseRomProvider({ children }: { children: React.ReactNode }) {
const smallEnough = size <= 128 * 1024 * 1024; // 128MB default
const hasRoom = headroom > size * 1.2 && headroom > 64 * 1024 * 1024; // some buffer
if (smallEnough && hasRoom) {
await setRomBlob(name, file);
setCached((prev) => ({ ...prev, [name]: true }));
await setRomBlob(id, file);
setCached((prev) => ({ ...prev, [id]: true }));
setTotalCachedBytes((n) => n + size);
}
} catch {}

View File

@@ -2,10 +2,10 @@
import type { Platform } from "@/data/baseRoms";
import { PLATFORMS } from "@/data/baseRoms";
const DB_NAME = "romhaven";
const DB_NAME = "hackdex";
const DB_VERSION = 2;
const STORE = "roms";
const BLOB_STORE = "rom_blobs";
const STORE = "base_roms";
const BLOB_STORE = "base_rom_blobs";
function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
@@ -13,10 +13,10 @@ function openDB(): Promise<IDBDatabase> {
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains(STORE)) {
db.createObjectStore(STORE, { keyPath: "name" });
db.createObjectStore(STORE, { keyPath: "id" });
}
if (!db.objectStoreNames.contains(BLOB_STORE)) {
db.createObjectStore(BLOB_STORE, { keyPath: "name" });
db.createObjectStore(BLOB_STORE, { keyPath: "id" });
}
};
req.onsuccess = () => resolve(req.result);
@@ -24,29 +24,29 @@ function openDB(): Promise<IDBDatabase> {
});
}
export async function setRomHandle(name: string, handle: any): Promise<void> {
export async function setRomHandle(id: string, handle: any): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, "readwrite");
const store = tx.objectStore(STORE);
store.put({ name, handle, updatedAt: Date.now() });
store.put({ id, handle, updatedAt: Date.now() });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
export async function getRomHandle(name: string): Promise<any | null> {
export async function getRomHandle(id: string): Promise<any | null> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, "readonly");
const store = tx.objectStore(STORE);
const req = store.get(name);
const req = store.get(id);
req.onsuccess = () => resolve(req.result?.handle ?? null);
req.onerror = () => reject(req.error);
});
}
export async function getAllRomEntries(): Promise<Array<{ name: string; handle: any }>> {
export async function getAllRomEntries(): Promise<Array<{ id: string; handle: any }>> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, "readonly");
@@ -54,57 +54,57 @@ export async function getAllRomEntries(): Promise<Array<{ name: string; handle:
const req = store.getAll();
req.onsuccess = () => {
const rows = (req.result as any[]) || [];
resolve(rows.map((r) => ({ name: r.name, handle: r.handle })));
resolve(rows.map((r) => ({ id: r.id, handle: r.handle })));
};
req.onerror = () => reject(req.error);
});
}
export async function deleteRomHandle(name: string): Promise<void> {
export async function deleteRomHandle(id: string): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, "readwrite");
const store = tx.objectStore(STORE);
store.delete(name);
store.delete(id);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
export async function setRomBlob(name: string, blob: Blob): Promise<void> {
export async function setRomBlob(id: string, blob: Blob): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(BLOB_STORE, "readwrite");
const store = tx.objectStore(BLOB_STORE);
store.put({ name, blob, updatedAt: Date.now() });
store.put({ id, blob, updatedAt: Date.now() });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
export async function getRomBlob(name: string): Promise<Blob | null> {
export async function getRomBlob(id: string): Promise<Blob | null> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(BLOB_STORE, "readonly");
const store = tx.objectStore(BLOB_STORE);
const req = store.get(name);
const req = store.get(id);
req.onsuccess = () => resolve(req.result?.blob ?? null);
req.onerror = () => reject(req.error);
});
}
export async function deleteRomBlob(name: string): Promise<void> {
export async function deleteRomBlob(id: string): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(BLOB_STORE, "readwrite");
const store = tx.objectStore(BLOB_STORE);
store.delete(name);
store.delete(id);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
export async function getAllBlobEntries(): Promise<Array<{ name: string; blob: Blob; updatedAt?: number }>> {
export async function getAllBlobEntries(): Promise<Array<{ id: string; blob: Blob; updatedAt?: number }>> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(BLOB_STORE, "readonly");