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