diff --git a/apps/web/src/pages/CasinoMachine.tsx b/apps/web/src/pages/CasinoMachine.tsx index d7c7e23b..c34eacc7 100644 --- a/apps/web/src/pages/CasinoMachine.tsx +++ b/apps/web/src/pages/CasinoMachine.tsx @@ -1,26 +1,13 @@ import { useEffect, useState } from "react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { api, keys, renderNode } from "@gl3/client"; -import { CasinoMachineResponseSchema, type CasinoMachine, type CasinoGame } from "@gl3/shared"; +import { renderNode, useCasinoMachine, useCasinoMachineAction, type CasinoMachineVerb } from "@gl3/client"; +import type { CasinoMachine, CasinoGame } from "@gl3/shared"; import { ErrorText, Money, Panel } from "../components/ui.js"; import { PageRenderer } from "../plugins/PageRenderer.js"; import { ReelCabinet } from "./ReelCabinet.js"; import styles from "./pages.module.css"; -const QUERY_KEY = ["casino-machine"]; -export function useCasinoMachine() { - return useQuery({ queryKey: QUERY_KEY, queryFn: async () => CasinoMachineResponseSchema.parse(await api("/api/casino/machine")) }); -} -function useMachineAction() { - const queries = useQueryClient(); - return useMutation({ - mutationFn: async ({ verb, body }: { verb: string; body: Record }) => - CasinoMachineResponseSchema.parse(await api(`/api/casino/machine/${verb}`, { method: "POST", body: JSON.stringify(body) })), - onSuccess: response => { queries.setQueryData(QUERY_KEY, response); }, - onError: () => { void queries.invalidateQueries({ queryKey: QUERY_KEY }); }, - onSettled: () => { void queries.invalidateQueries({ queryKey: keys.me() }); }, - }); -} +export { useCasinoMachine }; +const useMachineAction = useCasinoMachineAction; export function MachineEntry({ game, minBet, cash, jailed }: { game: CasinoGame; minBet: string; cash: string; jailed: boolean }): JSX.Element { const [credits, setCredits] = useState(""); @@ -51,7 +38,7 @@ export function MachineScreen({ machine, jailed }: { machine: CasinoMachine; jai return () => window.clearTimeout(timer); }, [machine.revision, machine.animation, animating]); const busy = action.isPending || animating; - const send = (verb: string, fields: Record = {}) => { + const send = (verb: CasinoMachineVerb, fields: Record = {}) => { setConfirmCashout(false); action.mutate({ verb, body: { id: machine.id, revision: machine.revision, ...fields } }); }; diff --git a/apps/web/src/pages/ReelCabinet.tsx b/apps/web/src/pages/ReelCabinet.tsx index fa33874c..f3bd1899 100644 --- a/apps/web/src/pages/ReelCabinet.tsx +++ b/apps/web/src/pages/ReelCabinet.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react"; import type { CasinoMachine, ReelDisplay } from "@gl3/shared"; -import { renderNode } from "@gl3/client"; +import { renderNode, type CasinoMachineVerb } from "@gl3/client"; import { Money } from "../components/ui.js"; import { PageRenderer } from "../plugins/PageRenderer.js"; import css from "./ReelCabinet.module.css"; @@ -26,7 +26,7 @@ function WinAmount({ amount, animate }: { amount: string; animate: boolean }): J export function ReelCabinet({ machine, display, busy, animating, jailed, reducedMotion, wager, onWager, send, cashout }: { machine: CasinoMachine; display: ReelDisplay; busy: boolean; animating: boolean; jailed: boolean; reducedMotion: boolean; wager: string; onWager: (value: string) => void; - send: (verb: string, fields?: Record) => void; cashout: ReactNode; + send: (verb: CasinoMachineVerb, fields?: Record) => void; cashout: ReactNode; }): JSX.Element { const seenRevision = useRef(machine.revision); const [celebration, setCelebration] = useState(null); diff --git a/package-lock.json b/package-lock.json index af1d921d..eacaef4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9272,9 +9272,9 @@ }, "packages/client": { "name": "@gl3/client", - "version": "0.1.7", + "version": "0.1.8", "dependencies": { - "@gl3/shared": "^1.0.17", + "@gl3/shared": "^1.0.18", "zod": "^3.23.8" }, "devDependencies": { @@ -9570,7 +9570,7 @@ }, "packages/shared": { "name": "@gl3/shared", - "version": "1.0.17", + "version": "1.0.18", "dependencies": { "zod": "^3.23.8" } diff --git a/packages/client/package.json b/packages/client/package.json index 422bee17..3e4d0d26 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@gl3/client", - "version": "0.1.7", + "version": "0.1.8", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -8,7 +8,7 @@ "files": ["dist", "src"], "publishConfig": { "registry": "https://npm.gl3.dev", "access": "public" }, "scripts": { "build": "tsc --build", "prepack": "tsc --build" }, - "dependencies": { "@gl3/shared": "^1.0.17", "zod": "^3.23.8" }, + "dependencies": { "@gl3/shared": "^1.0.18", "zod": "^3.23.8" }, "peerDependencies": { "react": "^18.3.0 || ^19.0.0", "@tanstack/react-query": "^5.0.0" }, "devDependencies": { "@tanstack/react-query": "^5.62.2", "@testing-library/react": "^16.3.2", "@types/react": "^18.3.12", "jsdom": "^29.1.1", "react": "^18.3.1", "react-dom": "^18.3.1" } } diff --git a/packages/client/src/api/keys.ts b/packages/client/src/api/keys.ts index 3406f532..9891a4d0 100644 --- a/packages/client/src/api/keys.ts +++ b/packages/client/src/api/keys.ts @@ -74,6 +74,10 @@ export const keys = { // two always move together (sitting changes both, and a settle changes the // seat counts the lobby lists), and the table poll is a 2.5s query anyway. casinoTable: () => ["casino", "table"] as const, + // The caller's open machine session — `GET /api/casino/machine`. Nested + // under `casino()` for the same reason as the table: the lobby's own + // `invalidates: ["casino"]` and a pull-to-refresh then cover it too. + casinoMachine: () => ["casino", "machine"] as const, // Admin sections (grant-gated). adminSections: () => ["admin", "sections"] as const, diff --git a/packages/client/src/queries/casino.ts b/packages/client/src/queries/casino.ts index 4c07a74d..d6a808d5 100644 --- a/packages/client/src/queries/casino.ts +++ b/packages/client/src/queries/casino.ts @@ -2,11 +2,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CasinoLeaveResponseSchema, CasinoLobbyResponseSchema, + CasinoMachineResponseSchema, CasinoSitResponseSchema, CasinoStepResponseSchema, CasinoTableResponseSchema, type CasinoLeaveResponse, type CasinoLobbyResponse, + type CasinoMachineResponse, type CasinoSitResponse, type CasinoStepResponse, type CasinoTableResponse, @@ -190,3 +192,44 @@ export function useTableAct() { onSettled: () => { void queryClient.invalidateQueries({ queryKey: keys.me() }); }, }); } + +// --------------------------------------------------------------------------- +// Machines +// --------------------------------------------------------------------------- + +/** + * The caller's open machine session, wherever it is — `{ machine: null }` + * when they hold none. A machine outranks both the seat and the lobby: its + * credits are the player's cash on loan, so a page with one open shows it + * before anything else. + */ +export function useCasinoMachine() { + return useQuery({ + queryKey: keys.casinoMachine(), + queryFn: async () => CasinoMachineResponseSchema.parse(await api("/api/casino/machine")), + }); +} + +/** The body every machine verb takes past `open`: the session and the revision it was read at. */ +export type CasinoMachineVerb = "open" | "spin" | "act" | "cashout"; + +/** + * One mutation for all four machine verbs. Every reply is the whole machine, + * written straight into the machine query so the reels the player is looking + * at are the ones the server settled; an error invalidates instead, because a + * `stale_machine` 409 means the cached revision is the one that is wrong. + * Cash moves on `open` (credits in) and `cashout` (credits back), so `me` is + * refreshed whichever verb ran. + */ +export function useCasinoMachineAction() { + const queryClient = useQueryClient(); + return useMutation }>({ + mutationFn: async ({ verb, body }) => + CasinoMachineResponseSchema.parse( + await api(`/api/casino/machine/${verb}`, { method: "POST", body: JSON.stringify(body) }), + ), + onSuccess: (response) => { queryClient.setQueryData(keys.casinoMachine(), response); }, + onError: () => { void queryClient.invalidateQueries({ queryKey: keys.casinoMachine() }); }, + onSettled: () => { void queryClient.invalidateQueries({ queryKey: keys.me() }); }, + }); +} diff --git a/packages/shared/package.json b/packages/shared/package.json index afcd3d3a..095733b3 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@gl3/shared", - "version": "1.0.17", + "version": "1.0.18", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts",