no db
This commit is contained in:
@@ -12,11 +12,7 @@
|
|||||||
"object-hash": "^3.0.0"
|
"object-hash": "^3.0.0"
|
||||||
},
|
},
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"esbuild"
|
||||||
"@prisma/client",
|
|
||||||
"@prisma/engines",
|
|
||||||
"esbuild",
|
|
||||||
"prisma"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/eden": "^1.3.2",
|
"@elysiajs/eden": "^1.3.2",
|
||||||
"@solid-primitives/scheduled": "^1.5.2",
|
"@solid-primitives/scheduled": "^1.5.2",
|
||||||
|
"@solid-primitives/storage": "^4.3.3",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"kefir": "^3.8.8",
|
"kefir": "^3.8.8",
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
import { Route, Router } from "@solidjs/router";
|
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 { render } from "solid-js/web";
|
||||||
import "virtual:uno.css";
|
import "virtual:uno.css";
|
||||||
import pkg from "^/package.json";
|
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import api from "./api";
|
import { name, setName } from "./profile";
|
||||||
import { mePromise } from "./profile";
|
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
let dialogRef!: HTMLDialogElement;
|
let dialogRef!: HTMLDialogElement;
|
||||||
const [profile] = createResource(() =>
|
|
||||||
mePromise.then(() => api.profile.get())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
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">
|
<div class="fixed tr bg-emerald-100 m-2 p-4 rounded-xl border-2 shadow-md shadow-black">
|
||||||
Name:{" "}
|
Name:{" "}
|
||||||
<input
|
<input
|
||||||
value={profile()?.data?.name ?? ""}
|
value={name()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
dialogRef.close();
|
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"
|
class="bg-emerald-200 border-1.5 rounded-full px-4"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
SimplePlayerView,
|
SimplePlayerView,
|
||||||
SimpleResult,
|
SimpleResult,
|
||||||
} from "@games/shared/games/simple";
|
} from "@games/shared/games/simple";
|
||||||
import { me, profile } from "~/profile";
|
import { me } from "~/profile";
|
||||||
import Hand from "./Hand";
|
import Hand from "./Hand";
|
||||||
import Pile from "./Pile";
|
import Pile from "./Pile";
|
||||||
import { TableContext } from "./Table";
|
import { TableContext } from "./Table";
|
||||||
@@ -39,7 +39,7 @@ export default () => {
|
|||||||
<span class="font-bold">
|
<span class="font-bold">
|
||||||
{view().playerTurn == me()
|
{view().playerTurn == me()
|
||||||
? "your"
|
? "your"
|
||||||
: profile(view().playerTurn)()?.name + "'s"}
|
: table.playerNames[view().playerTurn] + "'s"}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
turn
|
turn
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSignal, useContext } from "solid-js";
|
import { createSignal, useContext } from "solid-js";
|
||||||
import { playerColor, profile } from "~/profile";
|
import { playerColor } from "~/profile";
|
||||||
import { TableContext } from "./Table";
|
import { TableContext } from "./Table";
|
||||||
import { Stylable } from "./toolbox";
|
import { Stylable } from "./toolbox";
|
||||||
import { createObservable, createObservableWithInit } from "~/fn";
|
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`}
|
class={`${props.class} w-20 h-20 rounded-full flex justify-center items-center`}
|
||||||
>
|
>
|
||||||
<p style={{ "font-size": "1em" }}>
|
<p style={{ "font-size": "1em" }}>
|
||||||
{profile(props.playerKey)()?.name}
|
{table?.playerNames[props.playerKey]}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,16 +10,24 @@ import {
|
|||||||
Show,
|
Show,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import api, { fromWebsocket } from "~/api";
|
import api, { fromWebsocket } from "~/api";
|
||||||
import { createObservable, createObservableWithInit, cx } from "~/fn";
|
import {
|
||||||
import { me, mePromise, profile } from "~/profile";
|
createObservable,
|
||||||
|
createObservableStore,
|
||||||
|
createObservableWithInit,
|
||||||
|
cx,
|
||||||
|
} from "~/fn";
|
||||||
|
import { me, mePromise } from "~/profile";
|
||||||
import Game from "./Game";
|
import Game from "./Game";
|
||||||
import Player from "./Player";
|
import Player from "./Player";
|
||||||
import games from "@games/shared/games/index";
|
import games from "@games/shared/games/index";
|
||||||
|
import { createStore, Store } from "solid-js/store";
|
||||||
|
import { name } from "~/profile";
|
||||||
|
|
||||||
export const TableContext = createContext<{
|
export const TableContext = createContext<{
|
||||||
view: Accessor<any>;
|
view: Accessor<any>;
|
||||||
sendWs: (msg: TWsIn) => void;
|
sendWs: (msg: TWsIn) => void;
|
||||||
wsEvents: Stream<TWsOut, any>;
|
wsEvents: Stream<TWsOut, any>;
|
||||||
|
playerNames: Store<{ [key: string]: string }>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
export default (props: { tableKey: string }) => {
|
export default (props: { tableKey: string }) => {
|
||||||
@@ -37,7 +45,9 @@ export default (props: { tableKey: string }) => {
|
|||||||
);
|
);
|
||||||
onCleanup(() => wsPromise.then((ws) => ws.close()));
|
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 gameEvents = wsEvents.filter((evt) => evt.view !== undefined);
|
||||||
const resultEvents = wsEvents.filter((evt) => evt.results !== undefined);
|
const resultEvents = wsEvents.filter((evt) => evt.results !== undefined);
|
||||||
|
|
||||||
@@ -45,6 +55,13 @@ export default (props: { tableKey: string }) => {
|
|||||||
presenceEvents.map((evt) => evt.playersPresent!),
|
presenceEvents.map((evt) => evt.playersPresent!),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
const playerNames = createObservableStore(
|
||||||
|
wsEvents
|
||||||
|
.filter((evt) => evt.playerNames != null)
|
||||||
|
.map(({ playerNames }) => playerNames!)
|
||||||
|
.toProperty(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
const [ready, setReady] = createSignal(false);
|
const [ready, setReady] = createSignal(false);
|
||||||
mePromise.then(
|
mePromise.then(
|
||||||
@@ -57,8 +74,9 @@ export default (props: { tableKey: string }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
createEffect(() => sendWs({ ready: ready() }));
|
createEffect(() => sendWs({ ready: ready() }));
|
||||||
|
createEffect(() => sendWs({ name: name() }));
|
||||||
const view = createObservable(gameEvents.map((evt) => evt.view));
|
const view = createObservable(gameEvents.map((evt) => evt.view));
|
||||||
const results = createObservable(
|
const results = createObservable<string>(
|
||||||
merge([
|
merge([
|
||||||
gameEvents
|
gameEvents
|
||||||
.filter((evt) => "view" in evt && evt.view == null)
|
.filter((evt) => "view" in evt && evt.view == null)
|
||||||
@@ -73,6 +91,7 @@ export default (props: { tableKey: string }) => {
|
|||||||
sendWs,
|
sendWs,
|
||||||
wsEvents,
|
wsEvents,
|
||||||
view,
|
view,
|
||||||
|
playerNames,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex justify-around p-t-14">
|
<div class="flex justify-around p-t-14">
|
||||||
@@ -138,7 +157,7 @@ export default (props: { tableKey: string }) => {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={results() != null}>
|
<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">
|
<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>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</TableContext.Provider>
|
</TableContext.Provider>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Observable } from "kefir";
|
import { Observable } from "kefir";
|
||||||
import { Accessor, createSignal } from "solid-js";
|
import { Accessor, createSignal } from "solid-js";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Array<T> {
|
interface Array<T> {
|
||||||
@@ -37,3 +38,12 @@ export const createObservableWithInit = <T>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const cx = (...classes: string[]) => classes.join(" ");
|
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 { ApiType } from "./fn";
|
||||||
import api from "./api";
|
import api from "./api";
|
||||||
import hash from "object-hash";
|
import hash from "object-hash";
|
||||||
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
|
|
||||||
export const mePromise = api.whoami.post().then((r) => r.data);
|
export const mePromise = api.whoami.post().then((r) => r.data);
|
||||||
export const [me] = createResource(() => mePromise);
|
export const [me] = createResource(() => mePromise);
|
||||||
|
createEffect(() => console.log(me()));
|
||||||
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];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const playerColor = (humanKey: string) =>
|
export const playerColor = (humanKey: string) =>
|
||||||
"#" + hash(humanKey).substring(0, 6);
|
"#" + 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",
|
"name": "@games/server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently 'pnpm run devserver' 'pnpm run dbstudio'",
|
"dev": "NODE_ENV=development PORT=5001 bun run --hot src/index.ts",
|
||||||
"devserver": "NODE_ENV=development PORT=5001 bun run --hot src/index.ts",
|
"start": "NODE_ENV=production bun run 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'"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cors": "^1.3.3",
|
"@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 { Elysia, t } from "elysia";
|
||||||
import { combine } from "kefir";
|
import { combine } from "kefir";
|
||||||
import Bus from "kefir-bus";
|
import Bus from "kefir-bus";
|
||||||
import db from "./db";
|
|
||||||
import { liveTable, WsIn, WsOut } from "./table";
|
import { liveTable, WsIn, WsOut } from "./table";
|
||||||
import { err } from "./logging";
|
import { err } from "./logging";
|
||||||
|
import { generateTokenAndKey, resolveToken, tokenExists } from "./human";
|
||||||
|
|
||||||
export const WS = Bus<
|
export const WS = Bus<
|
||||||
{
|
{
|
||||||
@@ -19,63 +19,23 @@ export const WS = Bus<
|
|||||||
|
|
||||||
const api = new Elysia({ prefix: "/api" })
|
const api = new Elysia({ prefix: "/api" })
|
||||||
.post("/whoami", async ({ cookie: { token } }) => {
|
.post("/whoami", async ({ cookie: { token } }) => {
|
||||||
let human: Human | null;
|
console.log("WHOAMI");
|
||||||
if (
|
let key: string | undefined;
|
||||||
token.value == null ||
|
if (token.value == null || (key = resolveToken(token.value)) == null) {
|
||||||
(human = await db.human.findUnique({
|
const [newToken, newKey] = generateTokenAndKey();
|
||||||
where: {
|
|
||||||
token: token.value,
|
|
||||||
},
|
|
||||||
})) == null
|
|
||||||
) {
|
|
||||||
human = await db.human.create({
|
|
||||||
data: {},
|
|
||||||
});
|
|
||||||
token.set({
|
token.set({
|
||||||
value: human.token,
|
value: newToken,
|
||||||
expires: dayjs().add(1, "year").toDate(),
|
expires: dayjs().add(1, "year").toDate(),
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
});
|
});
|
||||||
|
return newKey;
|
||||||
}
|
}
|
||||||
|
return key;
|
||||||
return human.key;
|
|
||||||
})
|
})
|
||||||
.derive(async ({ cookie: { token }, status }) => {
|
.derive(async ({ cookie: { token }, status }) => {
|
||||||
const humanKey = await db.human
|
const humanKey = token.value && resolveToken(token.value);
|
||||||
.findUnique({
|
|
||||||
where: { token: token.value },
|
|
||||||
})
|
|
||||||
.then((human) => human?.key);
|
|
||||||
return humanKey != null ? { humanKey } : status(401);
|
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", {
|
.ws("/ws/:tableKey", {
|
||||||
body: WsIn,
|
body: WsIn,
|
||||||
response: WsOut,
|
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({
|
export const WsOut = t.Object({
|
||||||
playersPresent: t.Optional(t.Array(t.String())),
|
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()))),
|
playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))),
|
||||||
gameConfig: t.Optional(t.Any()),
|
gameConfig: t.Optional(t.Any()),
|
||||||
view: 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 type TWsOut = typeof WsOut.static;
|
||||||
export const WsIn = t.Union([
|
export const WsIn = t.Union([
|
||||||
|
t.Object({ name: t.String() }),
|
||||||
t.Object({ ready: t.Boolean() }),
|
t.Object({ ready: t.Boolean() }),
|
||||||
t.Object({ action: t.Any() }),
|
t.Object({ action: t.Any() }),
|
||||||
t.Object({ quit: t.Literal(true) }),
|
t.Object({ quit: t.Literal(true) }),
|
||||||
@@ -55,6 +57,7 @@ type TablePayload<
|
|||||||
>;
|
>;
|
||||||
gameConfig: Property<GameConfig | null, any>;
|
gameConfig: Property<GameConfig | null, any>;
|
||||||
results: Property<GameResult, any>;
|
results: Property<GameResult, any>;
|
||||||
|
playerNames: Property<{ [key: string]: string }, any>;
|
||||||
};
|
};
|
||||||
player: {
|
player: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@@ -133,11 +136,12 @@ export const liveTable = <
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const { ready, action, quit } = partition(
|
const { name, ready, action, quit } = partition(
|
||||||
["ready", "action", "quit"],
|
["name", "ready", "action", "quit"],
|
||||||
messages
|
messages
|
||||||
) as unknown as {
|
) as unknown as {
|
||||||
// yuck
|
// yuck
|
||||||
|
name: Observable<Attributed & { name: string }, any>;
|
||||||
ready: Observable<Attributed & { ready: boolean }, any>;
|
ready: Observable<Attributed & { ready: boolean }, any>;
|
||||||
action: Observable<Attributed & { action: GameAction }, any>;
|
action: Observable<Attributed & { action: GameAction }, any>;
|
||||||
quit: Observable<Attributed, any>;
|
quit: Observable<Attributed, any>;
|
||||||
@@ -145,17 +149,23 @@ export const liveTable = <
|
|||||||
|
|
||||||
const gameEnds = quit.map((_) => null);
|
const gameEnds = quit.map((_) => null);
|
||||||
|
|
||||||
|
const gameIsActivePool = pool<boolean, any>();
|
||||||
|
const gameIsActive = merge([
|
||||||
|
constant(false),
|
||||||
|
gameIsActivePool,
|
||||||
|
]).toProperty();
|
||||||
|
|
||||||
const playersReady = multiScan(
|
const playersReady = multiScan(
|
||||||
null as {
|
null as {
|
||||||
[key: string]: boolean;
|
[key: string]: boolean;
|
||||||
} | null,
|
} | null,
|
||||||
[
|
[
|
||||||
playersPresent, // TODO: filter to only outside active games
|
playersPresent.filterBy(invert(gameIsActive)),
|
||||||
(prev, players: ValueWithin<typeof playersPresent>) =>
|
(prev, players: ValueWithin<typeof playersPresent>) =>
|
||||||
Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])),
|
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: ValueWithin<typeof ready>) =>
|
||||||
prev?.[evt.humanKey] != null
|
prev?.[evt.humanKey] != null
|
||||||
? {
|
? {
|
||||||
@@ -172,6 +182,13 @@ export const liveTable = <
|
|||||||
]
|
]
|
||||||
).toProperty();
|
).toProperty();
|
||||||
|
|
||||||
|
const playerNames = name
|
||||||
|
.scan(
|
||||||
|
(prev, n) => ({ ...prev, [n.humanKey]: n.name }),
|
||||||
|
{} as { [key: string]: string }
|
||||||
|
)
|
||||||
|
.toProperty();
|
||||||
|
|
||||||
const gameStarts = playersReady
|
const gameStarts = playersReady
|
||||||
.filter(
|
.filter(
|
||||||
(pr) =>
|
(pr) =>
|
||||||
@@ -194,12 +211,6 @@ export const liveTable = <
|
|||||||
const resultsPool = pool<GameResult | null, any>();
|
const resultsPool = pool<GameResult | null, any>();
|
||||||
const results = merge([constant(null), resultsPool]).toProperty();
|
const results = merge([constant(null), resultsPool]).toProperty();
|
||||||
|
|
||||||
const gameIsActivePool = pool<boolean, any>();
|
|
||||||
const gameIsActive = merge([
|
|
||||||
constant(false),
|
|
||||||
gameIsActivePool,
|
|
||||||
]).toProperty();
|
|
||||||
|
|
||||||
const gameState = multiScan(
|
const gameState = multiScan(
|
||||||
null as GameState | null,
|
null as GameState | null,
|
||||||
[
|
[
|
||||||
@@ -270,6 +281,7 @@ export const liveTable = <
|
|||||||
playersReady,
|
playersReady,
|
||||||
gameConfig,
|
gameConfig,
|
||||||
results,
|
results,
|
||||||
|
playerNames,
|
||||||
},
|
},
|
||||||
player: playerStreams,
|
player: playerStreams,
|
||||||
},
|
},
|
||||||
|
|||||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
|||||||
'@solid-primitives/scheduled':
|
'@solid-primitives/scheduled':
|
||||||
specifier: ^1.5.2
|
specifier: ^1.5.2
|
||||||
version: 1.5.2(solid-js@1.9.9)
|
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':
|
'@solidjs/router':
|
||||||
specifier: ^0.15.3
|
specifier: ^0.15.3
|
||||||
version: 0.15.3(solid-js@1.9.9)
|
version: 0.15.3(solid-js@1.9.9)
|
||||||
@@ -592,6 +595,23 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
solid-js: ^1.6.12
|
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':
|
'@solidjs/router@0.15.3':
|
||||||
resolution: {integrity: sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==}
|
resolution: {integrity: sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2811,6 +2831,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
solid-js: 1.9.9
|
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)':
|
'@solidjs/router@0.15.3(solid-js@1.9.9)':
|
||||||
dependencies:
|
dependencies:
|
||||||
solid-js: 1.9.9
|
solid-js: 1.9.9
|
||||||
|
|||||||
Reference in New Issue
Block a user