no db
This commit is contained in:
@@ -12,11 +12,7 @@
|
||||
"object-hash": "^3.0.0"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@prisma/client",
|
||||
"@prisma/engines",
|
||||
"esbuild",
|
||||
"prisma"
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = () => {
|
||||
<div class="fixed tr bg-emerald-100 m-2 p-4 rounded-xl border-2 shadow-md shadow-black">
|
||||
Name:{" "}
|
||||
<input
|
||||
value={profile()?.data?.name ?? ""}
|
||||
value={name()}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
/>
|
||||
|
||||
@@ -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 () => {
|
||||
<span class="font-bold">
|
||||
{view().playerTurn == me()
|
||||
? "your"
|
||||
: profile(view().playerTurn)()?.name + "'s"}
|
||||
: table.playerNames[view().playerTurn] + "'s"}
|
||||
</span>{" "}
|
||||
turn
|
||||
</div>
|
||||
|
||||
@@ -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`}
|
||||
>
|
||||
<p style={{ "font-size": "1em" }}>
|
||||
{profile(props.playerKey)()?.name}
|
||||
{table?.playerNames[props.playerKey]}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<any>;
|
||||
sendWs: (msg: TWsIn) => void;
|
||||
wsEvents: Stream<TWsOut, any>;
|
||||
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<string>(
|
||||
merge([
|
||||
gameEvents
|
||||
.filter((evt) => "view" in evt && evt.view == null)
|
||||
@@ -73,6 +91,7 @@ export default (props: { tableKey: string }) => {
|
||||
sendWs,
|
||||
wsEvents,
|
||||
view,
|
||||
playerNames,
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-around p-t-14">
|
||||
@@ -138,7 +157,7 @@ export default (props: { tableKey: string }) => {
|
||||
</Show>
|
||||
<Show when={results() != null}>
|
||||
<span class="bg-[var(--light)] text-[var(--dark)] rounded-[24px] border-2 border-[var(--dark)] absolute center p-4 shadow-lg text-[4em] text-center">
|
||||
{profile(results())()?.name} won!
|
||||
{playerNames[results()!]} won!
|
||||
</span>
|
||||
</Show>
|
||||
</TableContext.Provider>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Observable } from "kefir";
|
||||
import { Accessor, createSignal } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
@@ -37,3 +38,12 @@ export const createObservableWithInit = <T>(
|
||||
};
|
||||
|
||||
export const cx = (...classes: string[]) => classes.join(" ");
|
||||
|
||||
export const createObservableStore = <T extends object = {}>(
|
||||
obs: Observable<T, any>,
|
||||
init: T
|
||||
) => {
|
||||
const [store, setStore] = createStore<T>(init);
|
||||
obs.onValue((val) => setStore(val));
|
||||
return store;
|
||||
};
|
||||
|
||||
@@ -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<ApiType<typeof api.profile.get>>;
|
||||
} = {};
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: path.join("db", "schema.prisma"),
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export default new PrismaClient();
|
||||
17
pkg/server/src/human.ts
Normal file
17
pkg/server/src/human.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const tokenToHumanKey: { [token: string]: string } = {};
|
||||
const playerKeys: Set<string> = 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);
|
||||
@@ -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<GameConfig | null, any>;
|
||||
results: Property<GameResult, any>;
|
||||
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<Attributed & { name: string }, any>;
|
||||
ready: Observable<Attributed & { ready: boolean }, any>;
|
||||
action: Observable<Attributed & { action: GameAction }, any>;
|
||||
quit: Observable<Attributed, any>;
|
||||
@@ -145,17 +149,23 @@ export const liveTable = <
|
||||
|
||||
const gameEnds = quit.map((_) => null);
|
||||
|
||||
const gameIsActivePool = pool<boolean, any>();
|
||||
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<typeof playersPresent>) =>
|
||||
Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])),
|
||||
],
|
||||
[
|
||||
ready, // TODO: filter to only outside active games
|
||||
ready.filterBy(invert(gameIsActive)),
|
||||
(prev, evt: ValueWithin<typeof ready>) =>
|
||||
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<GameResult | null, any>();
|
||||
const results = merge([constant(null), resultsPool]).toProperty();
|
||||
|
||||
const gameIsActivePool = pool<boolean, any>();
|
||||
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,
|
||||
},
|
||||
|
||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user