neser 0.1.1

NESER - NES Emulator in Rust - is a NES emulator written in Rust. It aims to be a high-quality, hardware-accurate emulator that is also easy to use and extend. It supports a wide range of NES games and features, including various mappers, audio processing, and input handling. NESER is designed to be modular and extensible, allowing developers to easily add new features or support for additional hardware. It can be run using one of two frontends: a native desktop application using SDL2, or a web application using WebAssembly. The desktop application provides a high-performance, feature-rich experience with support for various input devices and display options, while the web application allows users to play NES games directly in their browsers without needing to install any software in a BYOR manner (Bring Your Own Roms).
Documentation
export function buildSaveStateKey({ name, size, hash }) {
    return `rom:${name}:${size}:${hash}`;
}

export async function createRomSaveKey({ name, size, bytes }) {
    const hash = await computeRomHash(bytes);
    return buildSaveStateKey({ name, size, hash });
}

export async function computeRomHash(bytes) {
    const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
    if (globalThis.crypto?.subtle?.digest) {
        const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
        return bufferToHex(digest);
    }
    const { createHash } = await import("node:crypto");
    const hash = createHash("sha256").update(Buffer.from(data)).digest("hex");
    return hash;
}

export async function openSaveStateDb(name = "neser") {
    if (!globalThis.indexedDB) {
        throw new Error("IndexedDB not available");
    }
    return new Promise((resolve, reject) => {
        const request = globalThis.indexedDB.open(name, 1);
        request.onerror = () => reject(request.error);
        request.onupgradeneeded = () => {
            const db = request.result;
            if (!db.objectStoreNames.contains("savestates")) {
                db.createObjectStore("savestates");
            }
        };
        request.onsuccess = () => resolve(request.result);
    });
}

export async function saveState(db, key, bytes) {
    const payload = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
    return new Promise((resolve, reject) => {
        const tx = db.transaction("savestates", "readwrite");
        const store = tx.objectStore("savestates");
        const request = store.put(payload, key);
        request.onerror = () => reject(request.error);
        tx.oncomplete = () => resolve();
        tx.onerror = () => reject(tx.error || request.error);
        tx.onabort = () => reject(tx.error || request.error);
    });
}

export async function loadState(db, key) {
    return new Promise((resolve, reject) => {
        const tx = db.transaction("savestates", "readonly");
        const store = tx.objectStore("savestates");
        const request = store.get(key);
        request.onerror = () => reject(request.error);
        request.onsuccess = () => {
            const result = request.result;
            if (result) {
                resolve(result instanceof Uint8Array ? result : new Uint8Array(result));
            } else {
                resolve(null);
            }
        };
    });
}

export async function hasState(db, key) {
    return new Promise((resolve, reject) => {
        const tx = db.transaction("savestates", "readonly");
        const store = tx.objectStore("savestates");
        const request = store.getKey(key);
        request.onerror = () => reject(request.error);
        request.onsuccess = () => resolve(request.result !== undefined);
    });
}

function bufferToHex(buffer) {
    const bytes = new Uint8Array(buffer);
    return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
}