fix multi tab presence

This commit is contained in:
2025-08-24 15:09:58 -04:00
parent 6c64886f2a
commit 4bcf071668
6 changed files with 59 additions and 18 deletions

View File

@@ -12,7 +12,7 @@ import api, { fromWebsocket } from "../api";
import { me, playerColor, profile } from "../profile"; import { me, playerColor, profile } from "../profile";
import { fromEvents, Stream, stream } from "kefir"; import { fromEvents, Stream, stream } from "kefir";
import Bus from "kefir-bus"; 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 { EdenWS } from "@elysiajs/eden/treaty";
import { TWsIn, TWsOut } from "../../../server/src/table"; import { TWsIn, TWsOut } from "../../../server/src/table";
import Player from "./Player"; import Player from "./Player";
@@ -72,9 +72,24 @@ export default (props: { tableKey: string }) => {
</div> </div>
<div <div
id="table" id="table"
class="fixed bg-radial from-orange-950 to-stone-950 border-neutral-950 border-4 top-40 bottom-20 left-10 right-10 shadow-lg" class={cx(
"fixed",
"bg-radial",
"from-orange-950",
"to-stone-950",
"border-4",
"border-neutral-950",
"shadow-lg",
"top-40",
"bottom-20",
"left-10",
"right-10"
)}
style={{ style={{
"border-radius": "50% 50%", "border-radius": "50%",
}} }}
> >
<Show when={view() == null}> <Show when={view() == null}>

View File

@@ -35,3 +35,5 @@ export const createObservableWithInit = <T>(
return signal; return signal;
}; };
export const cx = (...classes: string[]) => classes.join(" ");

View File

@@ -22,11 +22,13 @@
"elysia-rate-limit": "^4.4.0", "elysia-rate-limit": "^4.4.0",
"kefir": "^3.8.8", "kefir": "^3.8.8",
"kefir-bus": "^2.3.1", "kefir-bus": "^2.3.1",
"lodash": "^4.17.21",
"object-hash": "^3.0.0" "object-hash": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/kefir": "^3.8.11", "@types/kefir": "^3.8.11",
"@types/lodash": "^4.17.20",
"concurrently": "^9.2.0", "concurrently": "^9.2.0",
"prisma": "6.13.0", "prisma": "6.13.0",
"ts-xor": "^1.3.0" "ts-xor": "^1.3.0"

View File

@@ -10,6 +10,7 @@ import dayjs from "dayjs";
import db from "./db"; import db from "./db";
import { liveTable, WsOut, WsIn } from "./table"; import { liveTable, WsOut, WsIn } from "./table";
import { Human } from "@prisma/client"; import { Human } from "@prisma/client";
import _ from "lodash";
const api = new Elysia({ prefix: "/api" }) const api = new Elysia({ prefix: "/api" })
.post("/whoami", async ({ cookie: { token } }) => { .post("/whoami", async ({ cookie: { token } }) => {
@@ -74,9 +75,9 @@ const api = new Elysia({ prefix: "/api" })
}) { }) {
const table = liveTable<SimpleGameState, SimpleAction>(tableKey); const table = liveTable<SimpleGameState, SimpleAction>(tableKey);
table.outputs.playersPresent.onValue((players) => table.outputs.playersPresent
send({ players }) .skipDuplicates((p1, p2) => _.isEqual(new Set(p1), new Set(p2)))
); .onValue((players) => send({ players }));
table.outputs.gameState.onValue((gameState) => table.outputs.gameState.onValue((gameState) =>
send({ send({
view: view:
@@ -84,7 +85,10 @@ const api = new Elysia({ prefix: "/api" })
getView(getKnowledge(gameState, humanKey), humanKey), getView(getKnowledge(gameState, humanKey), humanKey),
}) })
); );
table.inputs.presenceChanges.emit({ humanKey, presence: "joined" }); table.inputs.connectionChanges.emit({
humanKey,
presence: "joined",
});
}, },
response: WsOut, response: WsOut,
@@ -118,7 +122,7 @@ const api = new Elysia({ prefix: "/api" })
humanKey, humanKey,
}, },
}) { }) {
liveTable(tableKey).inputs.presenceChanges.emit({ liveTable(tableKey).inputs.connectionChanges.emit({
humanKey, humanKey,
presence: "left", presence: "left",
}); });

View File

@@ -24,7 +24,7 @@ export type TWsIn = typeof WsIn.static;
type Attributed = { humanKey: string }; type Attributed = { humanKey: string };
type TablePayload<GameState, GameAction> = { type TablePayload<GameState, GameAction> = {
inputs: { inputs: {
presenceChanges: TBus< connectionChanges: TBus<
Attributed & { presence: "joined" | "left" }, Attributed & { presence: "joined" | "left" },
never never
>; >;
@@ -45,23 +45,30 @@ const tables: {
export const liveTable = <GameState, GameAction>(key: string) => { export const liveTable = <GameState, GameAction>(key: string) => {
if (!(key in tables)) { if (!(key in tables)) {
const inputs: TablePayload<GameState, GameAction>["inputs"] = { const inputs: TablePayload<GameState, GameAction>["inputs"] = {
presenceChanges: Bus(), connectionChanges: Bus(),
gameProposals: Bus(), gameProposals: Bus(),
gameStarts: Bus(), gameStarts: Bus(),
gameActions: Bus(), gameActions: Bus(),
}; };
const { presenceChanges, gameProposals, gameStarts, gameActions } = const { connectionChanges, gameProposals, gameStarts, gameActions } =
inputs; inputs;
// ======= // =======
const playersPresent = presenceChanges.scan((prev, evt) => { const playerConnectionCounts = connectionChanges.scan((prev, evt) => {
if (evt.presence == "joined") { if (evt.presence == "left" && prev[evt.humanKey] == 1) {
prev.push(evt.humanKey); const { [evt.humanKey]: _, ...rest } = prev;
} else if (evt.presence == "left") { return rest;
prev.splice(prev.indexOf(evt.humanKey), 1);
} }
return prev; return {
}, [] as string[]); ...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( const gameState = transform(
null as SimpleGameState | null, null as SimpleGameState | null,

11
pnpm-lock.yaml generated
View File

@@ -99,6 +99,9 @@ importers:
kefir-bus: kefir-bus:
specifier: ^2.3.1 specifier: ^2.3.1
version: 2.3.1(kefir@3.8.8) version: 2.3.1(kefir@3.8.8)
lodash:
specifier: ^4.17.21
version: 4.17.21
object-hash: object-hash:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
@@ -109,6 +112,9 @@ importers:
'@types/kefir': '@types/kefir':
specifier: ^3.8.11 specifier: ^3.8.11
version: 3.8.11 version: 3.8.11
'@types/lodash':
specifier: ^4.17.20
version: 4.17.20
concurrently: concurrently:
specifier: ^9.2.0 specifier: ^9.2.0
version: 9.2.0 version: 9.2.0
@@ -474,6 +480,9 @@ packages:
'@types/kefir@3.8.11': '@types/kefir@3.8.11':
resolution: {integrity: sha512-5TRdFXQYsVUvqIH6nYjslHzBgn4hnptcutXnqAhfbKdWD/799c44hFhQGF3887E2t/Q4jSp3RvNFCaQ+b9w6vQ==} resolution: {integrity: sha512-5TRdFXQYsVUvqIH6nYjslHzBgn4hnptcutXnqAhfbKdWD/799c44hFhQGF3887E2t/Q4jSp3RvNFCaQ+b9w6vQ==}
'@types/lodash@4.17.20':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/node@24.2.0': '@types/node@24.2.0':
resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==} resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==}
@@ -1640,6 +1649,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 24.2.0 '@types/node': 24.2.0
'@types/lodash@4.17.20': {}
'@types/node@24.2.0': '@types/node@24.2.0':
dependencies: dependencies:
undici-types: 7.10.0 undici-types: 7.10.0