"use client"; import { useEffect, useRef, useState } from "react"; import PinballCanvas from "./PinballCanvas"; import { takePendingTable } from "@/lib/pinball/tableSelectionStore"; import { getLibraryEntry, saveToLibrary } from "@/lib/pinball/tableLibrary"; import { romGameNameFromFileName } from "@/lib/pinball/rom"; import type { RomSelection } from "@/lib/pinball/usePinballInstance"; import type { TableInfo } from "@/lib/pinball/types"; interface ResolvedTable { tableData?: ArrayBuffer; rom?: RomSelection; tableInfo: TableInfo; } async function toRomSelection(romFile: File): Promise { return { gameName: romGameNameFromFileName(romFile.name), romData: await romFile.arrayBuffer() }; } export default function PlayView() { const [resolved, setResolved] = useState(null); // takePendingTable() pops the store on read, so it must run at most once // per real mount — React's dev-mode double-invoke of effects would // otherwise have the second call see an already-emptied store (the first // call already popped the selection) and silently fall back to the demo // table. No unmount-cancellation guard here: this ref makes the // double-invoke's second pass a full no-op, so the async work below only // ever runs once for a given component instance — a cleanup-driven cancel // flag would incorrectly cancel that one real run's own result, since the // phantom unmount's cleanup fires for whichever pass actually started it. const hasResolvedRef = useRef(false); useEffect(() => { if (hasResolvedRef.current) return; hasResolvedRef.current = true; (async () => { const selection = takePendingTable(); if (selection?.kind === "upload") { const { vpxFile, romFile, saveToLibrary: shouldSave } = selection; const [tableData, rom] = await Promise.all([ vpxFile.arrayBuffer(), romFile ? toRomSelection(romFile) : Promise.resolve(undefined), ]); if (shouldSave) { saveToLibrary({ id: crypto.randomUUID(), name: vpxFile.name, sizeBytes: vpxFile.size, addedAt: Date.now(), vpxBlob: vpxFile, romFileName: romFile?.name, romBlob: romFile, }).catch((err) => console.error("Failed to save table to library:", err)); } setResolved({ tableData, rom, tableInfo: { name: vpxFile.name, sizeBytes: vpxFile.size, isDemo: false, romFileName: romFile?.name }, }); return; } if (selection?.kind === "library") { const entry = await getLibraryEntry(selection.id); if (entry) { const [tableData, rom] = await Promise.all([ entry.vpxBlob.arrayBuffer(), entry.romBlob ? entry.romBlob.arrayBuffer().then((romData) => ({ gameName: romGameNameFromFileName(entry.romFileName ?? ""), romData, })) : Promise.resolve(undefined), ]); setResolved({ tableData, rom, tableInfo: { name: entry.name, sizeBytes: entry.sizeBytes, isDemo: false, romFileName: entry.romFileName, }, }); return; } // Fall through to the demo table if the library entry was removed // (e.g. deleted in another tab) between selecting it and arriving here. } // No pending upload/library selection (fresh visit, or a hard refresh // of /play) — fall back to the bundled demo table rather than // dead-ending the route. setResolved({ tableInfo: { name: "Demo table", sizeBytes: 0, isDemo: true }, }); })(); }, []); if (!resolved) { return (
Preparing table…
); } return ( ); }