diff --git a/packages/client/src/components/Table.tsx b/packages/client/src/components/Table.tsx index c5fc83a..2782a04 100644 --- a/packages/client/src/components/Table.tsx +++ b/packages/client/src/components/Table.tsx @@ -12,7 +12,7 @@ import api, { fromWebsocket } from "../api"; import { me, playerColor, profile } from "../profile"; import { fromEvents, Stream, stream } from "kefir"; import Bus from "kefir-bus"; -import { createObservable, createObservableWithInit, WSEvent } from "../fn"; +import { createObservable, createObservableWithInit, cx, WSEvent } from "../fn"; import { EdenWS } from "@elysiajs/eden/treaty"; import { TWsIn, TWsOut } from "../../../server/src/table"; import Player from "./Player"; @@ -72,9 +72,24 @@ export default (props: { tableKey: string }) => {
diff --git a/packages/client/src/fn.ts b/packages/client/src/fn.ts index 6d2b368..968e092 100644 --- a/packages/client/src/fn.ts +++ b/packages/client/src/fn.ts @@ -35,3 +35,5 @@ export const createObservableWithInit = ( return signal; }; + +export const cx = (...classes: string[]) => classes.join(" "); diff --git a/packages/server/package.json b/packages/server/package.json index a0356b8..f760194 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -22,11 +22,13 @@ "elysia-rate-limit": "^4.4.0", "kefir": "^3.8.8", "kefir-bus": "^2.3.1", + "lodash": "^4.17.21", "object-hash": "^3.0.0" }, "devDependencies": { "@types/bun": "latest", "@types/kefir": "^3.8.11", + "@types/lodash": "^4.17.20", "concurrently": "^9.2.0", "prisma": "6.13.0", "ts-xor": "^1.3.0" diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index e09fe59..1454fe2 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -10,6 +10,7 @@ import dayjs from "dayjs"; import db from "./db"; import { liveTable, WsOut, WsIn } from "./table"; import { Human } from "@prisma/client"; +import _ from "lodash"; const api = new Elysia({ prefix: "/api" }) .post("/whoami", async ({ cookie: { token } }) => { @@ -74,9 +75,9 @@ const api = new Elysia({ prefix: "/api" }) }) { const table = liveTable(tableKey); - table.outputs.playersPresent.onValue((players) => - send({ players }) - ); + table.outputs.playersPresent + .skipDuplicates((p1, p2) => _.isEqual(new Set(p1), new Set(p2))) + .onValue((players) => send({ players })); table.outputs.gameState.onValue((gameState) => send({ view: @@ -84,7 +85,10 @@ const api = new Elysia({ prefix: "/api" }) getView(getKnowledge(gameState, humanKey), humanKey), }) ); - table.inputs.presenceChanges.emit({ humanKey, presence: "joined" }); + table.inputs.connectionChanges.emit({ + humanKey, + presence: "joined", + }); }, response: WsOut, @@ -118,7 +122,7 @@ const api = new Elysia({ prefix: "/api" }) humanKey, }, }) { - liveTable(tableKey).inputs.presenceChanges.emit({ + liveTable(tableKey).inputs.connectionChanges.emit({ humanKey, presence: "left", }); diff --git a/packages/server/src/table.ts b/packages/server/src/table.ts index a5a884c..d6b1f58 100644 --- a/packages/server/src/table.ts +++ b/packages/server/src/table.ts @@ -24,7 +24,7 @@ export type TWsIn = typeof WsIn.static; type Attributed = { humanKey: string }; type TablePayload = { inputs: { - presenceChanges: TBus< + connectionChanges: TBus< Attributed & { presence: "joined" | "left" }, never >; @@ -45,23 +45,30 @@ const tables: { export const liveTable = (key: string) => { if (!(key in tables)) { const inputs: TablePayload["inputs"] = { - presenceChanges: Bus(), + connectionChanges: Bus(), gameProposals: Bus(), gameStarts: Bus(), gameActions: Bus(), }; - const { presenceChanges, gameProposals, gameStarts, gameActions } = + const { connectionChanges, gameProposals, gameStarts, gameActions } = inputs; // ======= - const playersPresent = presenceChanges.scan((prev, evt) => { - if (evt.presence == "joined") { - prev.push(evt.humanKey); - } else if (evt.presence == "left") { - prev.splice(prev.indexOf(evt.humanKey), 1); + const playerConnectionCounts = connectionChanges.scan((prev, evt) => { + if (evt.presence == "left" && prev[evt.humanKey] == 1) { + const { [evt.humanKey]: _, ...rest } = prev; + return rest; } - return prev; - }, [] as string[]); + return { + ...prev, + [evt.humanKey]: + (prev[evt.humanKey] ?? 0) + + (evt.presence == "joined" ? 1 : -1), + }; + }, {} as { [key: string]: number }); + const playersPresent = playerConnectionCounts.map((counts) => + Object.keys(counts) + ); const gameState = transform( null as SimpleGameState | null, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 055da3f..3ef1cbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,9 @@ importers: kefir-bus: specifier: ^2.3.1 version: 2.3.1(kefir@3.8.8) + lodash: + specifier: ^4.17.21 + version: 4.17.21 object-hash: specifier: ^3.0.0 version: 3.0.0 @@ -109,6 +112,9 @@ importers: '@types/kefir': specifier: ^3.8.11 version: 3.8.11 + '@types/lodash': + specifier: ^4.17.20 + version: 4.17.20 concurrently: specifier: ^9.2.0 version: 9.2.0 @@ -474,6 +480,9 @@ packages: '@types/kefir@3.8.11': resolution: {integrity: sha512-5TRdFXQYsVUvqIH6nYjslHzBgn4hnptcutXnqAhfbKdWD/799c44hFhQGF3887E2t/Q4jSp3RvNFCaQ+b9w6vQ==} + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/node@24.2.0': resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==} @@ -1640,6 +1649,8 @@ snapshots: dependencies: '@types/node': 24.2.0 + '@types/lodash@4.17.20': {} + '@types/node@24.2.0': dependencies: undici-types: 7.10.0