This commit is contained in:
2025-09-01 22:53:57 -04:00
parent b433a26fc6
commit fd342e7d47
24 changed files with 132 additions and 332 deletions

View File

@@ -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,

View File

@@ -1,5 +0,0 @@
"use server";
import { PrismaClient } from "@prisma/client";
export default new PrismaClient();

17
pkg/server/src/human.ts Normal file
View 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);

View File

@@ -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,
},