diff --git a/package.json b/package.json index 98157f9..cd7e366 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,7 @@ "object-hash": "^3.0.0" }, "onlyBuiltDependencies": [ - "@parcel/watcher", - "@prisma/client", - "@prisma/engines", - "esbuild", - "prisma" + "esbuild" ] }, "devDependencies": { diff --git a/pkg/client/package.json b/pkg/client/package.json index 227f358..35a2a13 100644 --- a/pkg/client/package.json +++ b/pkg/client/package.json @@ -9,6 +9,7 @@ "dependencies": { "@elysiajs/eden": "^1.3.2", "@solid-primitives/scheduled": "^1.5.2", + "@solid-primitives/storage": "^4.3.3", "@solidjs/router": "^0.15.3", "js-cookie": "^3.0.5", "kefir": "^3.8.8", diff --git a/pkg/client/src/app.tsx b/pkg/client/src/app.tsx index fdd834a..2bdc667 100644 --- a/pkg/client/src/app.tsx +++ b/pkg/client/src/app.tsx @@ -1,17 +1,14 @@ +import { makePersisted } from "@solid-primitives/storage"; import { Route, Router } from "@solidjs/router"; -import { createResource, lazy, Suspense } from "solid-js"; +import pkg from "^/package.json"; +import { createSignal, lazy, Suspense } from "solid-js"; import { render } from "solid-js/web"; import "virtual:uno.css"; -import pkg from "^/package.json"; import "./style.css"; -import api from "./api"; -import { mePromise } from "./profile"; +import { name, setName } from "./profile"; const Profile = () => { let dialogRef!: HTMLDialogElement; - const [profile] = createResource(() => - mePromise.then(() => api.profile.get()) - ); return ( <> @@ -24,10 +21,10 @@ const Profile = () => {
Name:{" "} { dialogRef.close(); - void api.setName.post({ name: e.target.value }); + setName(e.target.value); }} class="bg-emerald-200 border-1.5 rounded-full px-4" /> diff --git a/pkg/client/src/components/Game.tsx b/pkg/client/src/components/Game.tsx index 3216ebc..84c0eaf 100644 --- a/pkg/client/src/components/Game.tsx +++ b/pkg/client/src/components/Game.tsx @@ -4,7 +4,7 @@ import type { SimplePlayerView, SimpleResult, } from "@games/shared/games/simple"; -import { me, profile } from "~/profile"; +import { me } from "~/profile"; import Hand from "./Hand"; import Pile from "./Pile"; import { TableContext } from "./Table"; @@ -39,7 +39,7 @@ export default () => { {view().playerTurn == me() ? "your" - : profile(view().playerTurn)()?.name + "'s"} + : table.playerNames[view().playerTurn] + "'s"} {" "} turn
diff --git a/pkg/client/src/components/Player.tsx b/pkg/client/src/components/Player.tsx index 4989e8d..01f76df 100644 --- a/pkg/client/src/components/Player.tsx +++ b/pkg/client/src/components/Player.tsx @@ -1,5 +1,5 @@ import { createSignal, useContext } from "solid-js"; -import { playerColor, profile } from "~/profile"; +import { playerColor } from "~/profile"; import { TableContext } from "./Table"; import { Stylable } from "./toolbox"; import { createObservable, createObservableWithInit } from "~/fn"; @@ -31,7 +31,7 @@ export default (props: { playerKey: string } & Stylable) => { class={`${props.class} w-20 h-20 rounded-full flex justify-center items-center`} >

- {profile(props.playerKey)()?.name} + {table?.playerNames[props.playerKey]}

); diff --git a/pkg/client/src/components/Table.tsx b/pkg/client/src/components/Table.tsx index 160c16f..7c8e4a7 100644 --- a/pkg/client/src/components/Table.tsx +++ b/pkg/client/src/components/Table.tsx @@ -10,16 +10,24 @@ import { Show, } from "solid-js"; import api, { fromWebsocket } from "~/api"; -import { createObservable, createObservableWithInit, cx } from "~/fn"; -import { me, mePromise, profile } from "~/profile"; +import { + createObservable, + createObservableStore, + createObservableWithInit, + cx, +} from "~/fn"; +import { me, mePromise } from "~/profile"; import Game from "./Game"; import Player from "./Player"; import games from "@games/shared/games/index"; +import { createStore, Store } from "solid-js/store"; +import { name } from "~/profile"; export const TableContext = createContext<{ view: Accessor; sendWs: (msg: TWsIn) => void; wsEvents: Stream; + playerNames: Store<{ [key: string]: string }>; }>(); export default (props: { tableKey: string }) => { @@ -37,7 +45,9 @@ export default (props: { tableKey: string }) => { ); onCleanup(() => wsPromise.then((ws) => ws.close())); - const presenceEvents = wsEvents.filter((evt) => evt.playersPresent != null); + const presenceEvents = wsEvents.filter( + (evt) => evt.playersPresent !== undefined + ); const gameEvents = wsEvents.filter((evt) => evt.view !== undefined); const resultEvents = wsEvents.filter((evt) => evt.results !== undefined); @@ -45,6 +55,13 @@ export default (props: { tableKey: string }) => { presenceEvents.map((evt) => evt.playersPresent!), [] ); + const playerNames = createObservableStore( + wsEvents + .filter((evt) => evt.playerNames != null) + .map(({ playerNames }) => playerNames!) + .toProperty(), + {} + ); const [ready, setReady] = createSignal(false); mePromise.then( @@ -57,8 +74,9 @@ export default (props: { tableKey: string }) => { ); createEffect(() => sendWs({ ready: ready() })); + createEffect(() => sendWs({ name: name() })); const view = createObservable(gameEvents.map((evt) => evt.view)); - const results = createObservable( + const results = createObservable( merge([ gameEvents .filter((evt) => "view" in evt && evt.view == null) @@ -73,6 +91,7 @@ export default (props: { tableKey: string }) => { sendWs, wsEvents, view, + playerNames, }} >
@@ -138,7 +157,7 @@ export default (props: { tableKey: string }) => { - {profile(results())()?.name} won! + {playerNames[results()!]} won! diff --git a/pkg/client/src/fn.ts b/pkg/client/src/fn.ts index 968e092..069f7bb 100644 --- a/pkg/client/src/fn.ts +++ b/pkg/client/src/fn.ts @@ -1,5 +1,6 @@ import { Observable } from "kefir"; import { Accessor, createSignal } from "solid-js"; +import { createStore } from "solid-js/store"; declare global { interface Array { @@ -37,3 +38,12 @@ export const createObservableWithInit = ( }; export const cx = (...classes: string[]) => classes.join(" "); + +export const createObservableStore = ( + obs: Observable, + init: T +) => { + const [store, setStore] = createStore(init); + obs.onValue((val) => setStore(val)); + return store; +}; diff --git a/pkg/client/src/profile.ts b/pkg/client/src/profile.ts index 1af5b36..b6b2cf1 100644 --- a/pkg/client/src/profile.ts +++ b/pkg/client/src/profile.ts @@ -1,26 +1,16 @@ -import { createResource, Resource } from "solid-js"; +import { createEffect, createResource, createSignal, Resource } from "solid-js"; import { ApiType } from "./fn"; import api from "./api"; import hash from "object-hash"; +import { makePersisted } from "@solid-primitives/storage"; export const mePromise = api.whoami.post().then((r) => r.data); export const [me] = createResource(() => mePromise); - -const playerProfiles: { - [humanKey: string]: Resource>; -} = {}; - -export const profile = (humanKey: string) => { - if (!(humanKey in playerProfiles)) { - playerProfiles[humanKey] = createResource(() => - api.profile - .get({ query: { otherHumanKey: humanKey } }) - .then((r) => r.data) - )[0]; - } - - return playerProfiles[humanKey]; -}; +createEffect(() => console.log(me())); export const playerColor = (humanKey: string) => "#" + hash(humanKey).substring(0, 6); + +export const [name, setName] = makePersisted(createSignal("__name__"), { + name: "name", +}); diff --git a/pkg/server/db/migrations/20250805231347_init/migration.sql b/pkg/server/db/migrations/20250805231347_init/migration.sql deleted file mode 100644 index ef9aabb..0000000 --- a/pkg/server/db/migrations/20250805231347_init/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ --- CreateTable -CREATE TABLE "Game" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - "name" TEXT NOT NULL, - "rules" TEXT -); - --- CreateTable -CREATE TABLE "Instance" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "gameId" INTEGER NOT NULL, - "gameState" JSONB NOT NULL, - CONSTRAINT "Instance_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "Game_name_key" ON "Game"("name"); diff --git a/pkg/server/db/migrations/20250809034803_strings_are_just_better/migration.sql b/pkg/server/db/migrations/20250809034803_strings_are_just_better/migration.sql deleted file mode 100644 index a33e2b2..0000000 --- a/pkg/server/db/migrations/20250809034803_strings_are_just_better/migration.sql +++ /dev/null @@ -1,34 +0,0 @@ -/* - Warnings: - - - The primary key for the `Game` table will be changed. If it partially fails, the table could be left without primary key constraint. - - You are about to drop the column `id` on the `Game` table. All the data in the column will be lost. - - You are about to drop the column `gameId` on the `Instance` table. All the data in the column will be lost. - - Added the required column `key` to the `Game` table without a default value. This is not possible if the table is not empty. - - Added the required column `gameKey` to the `Instance` table without a default value. This is not possible if the table is not empty. - -*/ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Game" ( - "key" TEXT NOT NULL PRIMARY KEY, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - "name" TEXT NOT NULL, - "rules" TEXT -); -INSERT INTO "new_Game" ("createdAt", "name", "rules", "updatedAt") SELECT "createdAt", "name", "rules", "updatedAt" FROM "Game"; -DROP TABLE "Game"; -ALTER TABLE "new_Game" RENAME TO "Game"; -CREATE TABLE "new_Instance" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "gameKey" TEXT NOT NULL, - "gameState" JSONB NOT NULL, - CONSTRAINT "Instance_gameKey_fkey" FOREIGN KEY ("gameKey") REFERENCES "Game" ("key") ON DELETE RESTRICT ON UPDATE CASCADE -); -INSERT INTO "new_Instance" ("gameState", "id") SELECT "gameState", "id" FROM "Instance"; -DROP TABLE "Instance"; -ALTER TABLE "new_Instance" RENAME TO "Instance"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/pkg/server/db/migrations/20250809191236_strings/migration.sql b/pkg/server/db/migrations/20250809191236_strings/migration.sql deleted file mode 100644 index 0701d21..0000000 --- a/pkg/server/db/migrations/20250809191236_strings/migration.sql +++ /dev/null @@ -1,20 +0,0 @@ -/* - Warnings: - - - The primary key for the `Instance` table will be changed. If it partially fails, the table could be left without primary key constraint. - -*/ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Instance" ( - "id" TEXT NOT NULL PRIMARY KEY, - "gameKey" TEXT NOT NULL, - "gameState" JSONB NOT NULL, - CONSTRAINT "Instance_gameKey_fkey" FOREIGN KEY ("gameKey") REFERENCES "Game" ("key") ON DELETE RESTRICT ON UPDATE CASCADE -); -INSERT INTO "new_Instance" ("gameKey", "gameState", "id") SELECT "gameKey", "gameState", "id" FROM "Instance"; -DROP TABLE "Instance"; -ALTER TABLE "new_Instance" RENAME TO "Instance"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/pkg/server/db/migrations/20250809192322_humans/migration.sql b/pkg/server/db/migrations/20250809192322_humans/migration.sql deleted file mode 100644 index d7d52cc..0000000 --- a/pkg/server/db/migrations/20250809192322_humans/migration.sql +++ /dev/null @@ -1,28 +0,0 @@ -/* - Warnings: - - - Added the required column `createdByKey` to the `Instance` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateTable -CREATE TABLE "Human" ( - "key" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL -); - --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Instance" ( - "id" TEXT NOT NULL PRIMARY KEY, - "createdByKey" TEXT NOT NULL, - "gameKey" TEXT NOT NULL, - "gameState" JSONB NOT NULL, - CONSTRAINT "Instance_createdByKey_fkey" FOREIGN KEY ("createdByKey") REFERENCES "Human" ("key") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "Instance_gameKey_fkey" FOREIGN KEY ("gameKey") REFERENCES "Game" ("key") ON DELETE RESTRICT ON UPDATE CASCADE -); -INSERT INTO "new_Instance" ("gameKey", "gameState", "id") SELECT "gameKey", "gameState", "id" FROM "Instance"; -DROP TABLE "Instance"; -ALTER TABLE "new_Instance" RENAME TO "Instance"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/pkg/server/db/migrations/20250813041445_names_optional/migration.sql b/pkg/server/db/migrations/20250813041445_names_optional/migration.sql deleted file mode 100644 index 2df6ca0..0000000 --- a/pkg/server/db/migrations/20250813041445_names_optional/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Human" ( - "key" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL DEFAULT '' -); -INSERT INTO "new_Human" ("key", "name") SELECT "key", "name" FROM "Human"; -DROP TABLE "Human"; -ALTER TABLE "new_Human" RENAME TO "Human"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/pkg/server/db/migrations/20250818213152_tokens/migration.sql b/pkg/server/db/migrations/20250818213152_tokens/migration.sql deleted file mode 100644 index 117242a..0000000 --- a/pkg/server/db/migrations/20250818213152_tokens/migration.sql +++ /dev/null @@ -1,45 +0,0 @@ -/* - Warnings: - - - The required column `token` was added to the `Human` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. - -*/ --- CreateTable -CREATE TABLE "_HumanToInstance" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL, - CONSTRAINT "_HumanToInstance_A_fkey" FOREIGN KEY ("A") REFERENCES "Human" ("key") ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT "_HumanToInstance_B_fkey" FOREIGN KEY ("B") REFERENCES "Instance" ("id") ON DELETE CASCADE ON UPDATE CASCADE -); - --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Human" ( - "key" TEXT NOT NULL PRIMARY KEY, - "token" TEXT NOT NULL, - "name" TEXT NOT NULL DEFAULT '__name__', - "lastActive" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); -INSERT INTO "new_Human" ("key", "name") SELECT "key", "name" FROM "Human"; -DROP TABLE "Human"; -ALTER TABLE "new_Human" RENAME TO "Human"; -CREATE UNIQUE INDEX "Human_token_key" ON "Human"("token"); -CREATE TABLE "new_Instance" ( - "id" TEXT NOT NULL PRIMARY KEY, - "createdByKey" TEXT NOT NULL, - "gameKey" TEXT NOT NULL, - "gameState" JSONB NOT NULL, - CONSTRAINT "Instance_gameKey_fkey" FOREIGN KEY ("gameKey") REFERENCES "Game" ("key") ON DELETE RESTRICT ON UPDATE CASCADE -); -INSERT INTO "new_Instance" ("createdByKey", "gameKey", "gameState", "id") SELECT "createdByKey", "gameKey", "gameState", "id" FROM "Instance"; -DROP TABLE "Instance"; -ALTER TABLE "new_Instance" RENAME TO "Instance"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; - --- CreateIndex -CREATE UNIQUE INDEX "_HumanToInstance_AB_unique" ON "_HumanToInstance"("A", "B"); - --- CreateIndex -CREATE INDEX "_HumanToInstance_B_index" ON "_HumanToInstance"("B"); diff --git a/pkg/server/db/migrations/20250819011115_gamestate_nullable/migration.sql b/pkg/server/db/migrations/20250819011115_gamestate_nullable/migration.sql deleted file mode 100644 index 99b6453..0000000 --- a/pkg/server/db/migrations/20250819011115_gamestate_nullable/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Instance" ( - "id" TEXT NOT NULL PRIMARY KEY, - "createdByKey" TEXT NOT NULL, - "gameKey" TEXT NOT NULL, - "gameState" JSONB, - CONSTRAINT "Instance_gameKey_fkey" FOREIGN KEY ("gameKey") REFERENCES "Game" ("key") ON DELETE RESTRICT ON UPDATE CASCADE -); -INSERT INTO "new_Instance" ("createdByKey", "gameKey", "gameState", "id") SELECT "createdByKey", "gameKey", "gameState", "id" FROM "Instance"; -DROP TABLE "Instance"; -ALTER TABLE "new_Instance" RENAME TO "Instance"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/pkg/server/db/migrations/migration_lock.toml b/pkg/server/db/migrations/migration_lock.toml deleted file mode 100644 index 2a5a444..0000000 --- a/pkg/server/db/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "sqlite" diff --git a/pkg/server/db/schema.prisma b/pkg/server/db/schema.prisma deleted file mode 100644 index 14d9f80..0000000 --- a/pkg/server/db/schema.prisma +++ /dev/null @@ -1,37 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -model Game { - key String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String - rules String? - instances Instance[] -} - -model Human { - key String @id @default(cuid()) - token String @unique @default(cuid()) - name String @default("__name__") - lastActive DateTime @default(now()) - - playingInstances Instance[] -} - -model Instance { - id String @id @default(cuid()) - createdByKey String - gameKey String - - players Human[] - - game Game @relation(fields: [gameKey], references: [key]) - gameState Json? -} diff --git a/pkg/server/package.json b/pkg/server/package.json index f760194..cbacffb 100644 --- a/pkg/server/package.json +++ b/pkg/server/package.json @@ -1,15 +1,8 @@ { "name": "@games/server", "scripts": { - "dev": "concurrently 'pnpm run devserver' 'pnpm run dbstudio'", - "devserver": "NODE_ENV=development PORT=5001 bun run --hot src/index.ts", - "dbstudio": "pnpm dlx prisma studio --browser none", - "dbdeploy": "pnpm dlx prisma migrate deploy", - "dbtypes": "pnpm dlx prisma generate", - "dbsync": "pnpm dlx prisma migrate dev", - "dbwipe": "pnpm dlx prisma migrate reset", - "prod": "NODE_ENV=production bun run src/index.ts", - "start": "concurrently 'pnpm run prod' 'pnpm run dbstudio'" + "dev": "NODE_ENV=development PORT=5001 bun run --hot src/index.ts", + "start": "NODE_ENV=production bun run src/index.ts" }, "dependencies": { "@elysiajs/cors": "^1.3.3", diff --git a/pkg/server/prisma.config.ts b/pkg/server/prisma.config.ts deleted file mode 100644 index 25b7c97..0000000 --- a/pkg/server/prisma.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import path from "node:path"; -import { defineConfig } from "prisma/config"; - -export default defineConfig({ - schema: path.join("db", "schema.prisma"), -}); diff --git a/pkg/server/src/api.ts b/pkg/server/src/api.ts index 2ec4da6..dd9823e 100644 --- a/pkg/server/src/api.ts +++ b/pkg/server/src/api.ts @@ -4,9 +4,9 @@ import dayjs from "dayjs"; import { Elysia, t } from "elysia"; import { combine } from "kefir"; import Bus from "kefir-bus"; -import db from "./db"; import { liveTable, WsIn, WsOut } from "./table"; import { err } from "./logging"; +import { generateTokenAndKey, resolveToken, tokenExists } from "./human"; export const WS = Bus< { @@ -19,63 +19,23 @@ export const WS = Bus< const api = new Elysia({ prefix: "/api" }) .post("/whoami", async ({ cookie: { token } }) => { - let human: Human | null; - if ( - token.value == null || - (human = await db.human.findUnique({ - where: { - token: token.value, - }, - })) == null - ) { - human = await db.human.create({ - data: {}, - }); + console.log("WHOAMI"); + let key: string | undefined; + if (token.value == null || (key = resolveToken(token.value)) == null) { + const [newToken, newKey] = generateTokenAndKey(); token.set({ - value: human.token, + value: newToken, expires: dayjs().add(1, "year").toDate(), httpOnly: true, }); + return newKey; } - - return human.key; + return key; }) .derive(async ({ cookie: { token }, status }) => { - const humanKey = await db.human - .findUnique({ - where: { token: token.value }, - }) - .then((human) => human?.key); + const humanKey = token.value && resolveToken(token.value); return humanKey != null ? { humanKey } : status(401); }) - .post( - "/setName", - ({ body: { name }, humanKey }) => - db.human.update({ - where: { - key: humanKey, - }, - data: { - name, - }, - }), - { - body: t.Object({ - name: t.String(), - }), - } - ) - .get("/profile", ({ humanKey, query: { otherHumanKey } }) => - db.human - .findFirst({ where: { key: otherHumanKey ?? humanKey } }) - .then((human) => { - if (human == null) { - return null; - } - const { token, ...safeProfile } = human; - return safeProfile; - }) - ) .ws("/ws/:tableKey", { body: WsIn, response: WsOut, diff --git a/pkg/server/src/db.ts b/pkg/server/src/db.ts deleted file mode 100644 index 3cfc482..0000000 --- a/pkg/server/src/db.ts +++ /dev/null @@ -1,5 +0,0 @@ -"use server"; - -import { PrismaClient } from "@prisma/client"; - -export default new PrismaClient(); diff --git a/pkg/server/src/human.ts b/pkg/server/src/human.ts new file mode 100644 index 0000000..b1608ff --- /dev/null +++ b/pkg/server/src/human.ts @@ -0,0 +1,17 @@ +const tokenToHumanKey: { [token: string]: string } = {}; +const playerKeys: Set = new Set(); + +export const generateTokenAndKey = () => { + let token: string, key: string; + do { + [token, key] = [crypto.randomUUID(), crypto.randomUUID()]; + tokenToHumanKey[token] = key; + playerKeys.add(key); + } while (!(token in tokenToHumanKey || playerKeys.has(key))); + return [token, key]; +}; + +export const resolveToken = (token: string) => + tokenToHumanKey[token] as string | undefined; +export const tokenExists = (token: string) => token in tokenToHumanKey; +export const keyExists = (key: string) => playerKeys.has(key); diff --git a/pkg/server/src/table.ts b/pkg/server/src/table.ts index 1a07e36..19319ea 100644 --- a/pkg/server/src/table.ts +++ b/pkg/server/src/table.ts @@ -15,6 +15,7 @@ import { log } from "./logging"; export const WsOut = t.Object({ playersPresent: t.Optional(t.Array(t.String())), + playerNames: t.Optional(t.Record(t.String(), t.String())), playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))), gameConfig: t.Optional(t.Any()), view: t.Optional(t.Any()), @@ -22,6 +23,7 @@ export const WsOut = t.Object({ }); export type TWsOut = typeof WsOut.static; export const WsIn = t.Union([ + t.Object({ name: t.String() }), t.Object({ ready: t.Boolean() }), t.Object({ action: t.Any() }), t.Object({ quit: t.Literal(true) }), @@ -55,6 +57,7 @@ type TablePayload< >; gameConfig: Property; results: Property; + playerNames: Property<{ [key: string]: string }, any>; }; player: { [key: string]: { @@ -133,11 +136,12 @@ export const liveTable = < }); }); - const { ready, action, quit } = partition( - ["ready", "action", "quit"], + const { name, ready, action, quit } = partition( + ["name", "ready", "action", "quit"], messages ) as unknown as { // yuck + name: Observable; ready: Observable; action: Observable; quit: Observable; @@ -145,17 +149,23 @@ export const liveTable = < const gameEnds = quit.map((_) => null); + const gameIsActivePool = pool(); + const gameIsActive = merge([ + constant(false), + gameIsActivePool, + ]).toProperty(); + const playersReady = multiScan( null as { [key: string]: boolean; } | null, [ - playersPresent, // TODO: filter to only outside active games + playersPresent.filterBy(invert(gameIsActive)), (prev, players: ValueWithin) => Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])), ], [ - ready, // TODO: filter to only outside active games + ready.filterBy(invert(gameIsActive)), (prev, evt: ValueWithin) => prev?.[evt.humanKey] != null ? { @@ -172,6 +182,13 @@ export const liveTable = < ] ).toProperty(); + const playerNames = name + .scan( + (prev, n) => ({ ...prev, [n.humanKey]: n.name }), + {} as { [key: string]: string } + ) + .toProperty(); + const gameStarts = playersReady .filter( (pr) => @@ -194,12 +211,6 @@ export const liveTable = < const resultsPool = pool(); const results = merge([constant(null), resultsPool]).toProperty(); - const gameIsActivePool = pool(); - const gameIsActive = merge([ - constant(false), - gameIsActivePool, - ]).toProperty(); - const gameState = multiScan( null as GameState | null, [ @@ -270,6 +281,7 @@ export const liveTable = < playersReady, gameConfig, results, + playerNames, }, player: playerStreams, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2aace19..5babdb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@solid-primitives/scheduled': specifier: ^1.5.2 version: 1.5.2(solid-js@1.9.9) + '@solid-primitives/storage': + specifier: ^4.3.3 + version: 4.3.3(solid-js@1.9.9) '@solidjs/router': specifier: ^0.15.3 version: 0.15.3(solid-js@1.9.9) @@ -592,6 +595,23 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/storage@4.3.3': + resolution: {integrity: sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw==} + peerDependencies: + '@tauri-apps/plugin-store': '*' + solid-js: ^1.6.12 + solid-start: '*' + peerDependenciesMeta: + '@tauri-apps/plugin-store': + optional: true + solid-start: + optional: true + + '@solid-primitives/utils@6.3.2': + resolution: {integrity: sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==} + peerDependencies: + solid-js: ^1.6.12 + '@solidjs/router@0.15.3': resolution: {integrity: sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==} peerDependencies: @@ -2811,6 +2831,15 @@ snapshots: dependencies: solid-js: 1.9.9 + '@solid-primitives/storage@4.3.3(solid-js@1.9.9)': + dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) + solid-js: 1.9.9 + + '@solid-primitives/utils@6.3.2(solid-js@1.9.9)': + dependencies: + solid-js: 1.9.9 + '@solidjs/router@0.15.3(solid-js@1.9.9)': dependencies: solid-js: 1.9.9