diff --git a/packages/client/package.json b/packages/client/package.json index 3e20d5d..f50e6e7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -11,12 +11,14 @@ "@solid-primitives/scheduled": "^1.5.2", "@solidjs/router": "^0.15.3", "js-cookie": "^3.0.5", + "kefir-bus": "^2.3.1", "object-hash": "^3.0.0", "solid-js": "^1.9.5" }, "devDependencies": { "@iconify-json/solar": "^1.2.4", "@types/js-cookie": "^3.0.6", + "@types/kefir": "^3.8.11", "@unocss/preset-icons": "^66.4.2", "@unocss/preset-wind4": "^66.4.2", "unocss": "^66.4.2", diff --git a/packages/client/src/api.ts b/packages/client/src/api.ts index 251f18b..ea60fbc 100644 --- a/packages/client/src/api.ts +++ b/packages/client/src/api.ts @@ -1,6 +1,8 @@ import { createResource } from "solid-js"; import { type Api } from "../../server/src/api"; import { treaty } from "@elysiajs/eden"; +import { EdenWS } from "@elysiajs/eden/treaty"; +import { fromEvents } from "kefir"; const { api } = treaty( import.meta.env.DEV ? "http://localhost:5001" : window.location.origin, @@ -10,4 +12,5 @@ const { api } = treaty( ); export default api; -export const [me] = createResource(() => api.whoami.post().then((r) => r.data)); +export const fromWebsocket = (ws: any) => + fromEvents(ws, "message"); diff --git a/packages/client/src/components/Table.tsx b/packages/client/src/components/Table.tsx index 117a385..67dc607 100644 --- a/packages/client/src/components/Table.tsx +++ b/packages/client/src/components/Table.tsx @@ -1,65 +1,48 @@ import { Accessor, createContext, - createEffect, - createResource, createSignal, For, onCleanup, - Resource, Show, - untrack, } from "solid-js"; -import { createStore } from "solid-js/store"; import { SimplePlayerView } from "../../../server/src/games/simple"; -import api, { me } from "../api"; -import { ApiType } from "../fn"; -import Game from "./Game"; - -const [playerProfiles, setPlayerProfiles] = createStore< - Record>> ->({}); +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 { EdenWS } from "@elysiajs/eden/treaty"; +import { TWsIn, TWsOut } from "../../../server/src/table"; export const TableContext = createContext<{ players: Accessor; - view: Accessor; - // submitAction: (action: Action) => Promise; + view: Accessor; + sendWs: (msg: TWsIn) => void; }>(); export default (props: { tableKey: string }) => { - const [players, setPlayers] = createSignal([]); - const [view, setView] = createSignal(); - const ws = api.ws(props).subscribe(); + const wsEvents = fromWebsocket(ws); onCleanup(() => ws.close()); - ws.on("message", (evt) => { - if (evt.data.players) { - setPlayers(evt.data.players); - } - if (evt.data.view) { - setView(evt.data.view); - } - }); + const presenceEvents = wsEvents.filter((evt) => evt.players != null); + const gameEvents = wsEvents.filter((evt) => evt.view != null); - createEffect(() => { - players().forEach((player) => { - if (!untrack(() => playerProfiles[player])) { - const [playerProfile] = createResource(() => - api.profile - .get({ query: { otherHumanKey: player } }) - .then((r) => r.data) - ); - setPlayerProfiles((prev) => ({ - ...prev, - [player]: playerProfile, - })); - } - }); - }); + const players = createObservableWithInit( + presenceEvents.map((evt) => evt.players!), + [] + ); + const view = createObservable(gameEvents.map((evt) => evt.view)); return ( - +
p != me())}> {(player, i) => { @@ -75,11 +58,12 @@ export default (props: { tableKey: string }) => { transform: `translate(0, ${ verticalOffset() * 150 }vh)`, + "background-color": playerColor(player), }} - class="w-20 h-20 rounded-full bg-red-900 flex justify-center items-center" + class="w-20 h-20 rounded-full flex justify-center items-center" >

- {playerProfiles[player]?.()?.name} + {profile(player)()?.name}

); @@ -105,7 +89,7 @@ export default (props: { tableKey: string }) => { - +
Game started!
); diff --git a/packages/client/src/fn.ts b/packages/client/src/fn.ts index fbbf4be..6d2b368 100644 --- a/packages/client/src/fn.ts +++ b/packages/client/src/fn.ts @@ -1,3 +1,6 @@ +import { Observable } from "kefir"; +import { Accessor, createSignal } from "solid-js"; + declare global { interface Array { thru(fn: (arr: T[]) => S): S; @@ -11,3 +14,24 @@ export const clone = (o: T): T => JSON.parse(JSON.stringify(o)); export type ApiType Promise<{ data: any }>> = Awaited< ReturnType >["data"]; + +export type WSEvent< + T extends { subscribe: (handler: (...args: any[]) => any) => any } +> = Parameters[0]>[0]; + +export const createObservable = (obs: Observable) => { + const [signal, setSignal] = createSignal(); + obs.onValue((val) => setSignal(() => val)); + + return signal; +}; + +export const createObservableWithInit = ( + obs: Observable, + init: T +) => { + const [signal, setSignal] = createSignal(init); + obs.onValue((val) => setSignal(() => val)); + + return signal; +}; diff --git a/packages/client/src/global.d.ts b/packages/client/src/global.d.ts deleted file mode 100644 index dc6f10c..0000000 --- a/packages/client/src/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/packages/client/src/profile.ts b/packages/client/src/profile.ts new file mode 100644 index 0000000..e2828ca --- /dev/null +++ b/packages/client/src/profile.ts @@ -0,0 +1,25 @@ +import { createResource, Resource } from "solid-js"; +import { ApiType } from "./fn"; +import api from "./api"; +import hash from "object-hash"; + +export const [me] = createResource(() => api.whoami.post().then((r) => r.data)); + +const playerProfiles: { + [humanKey: string]: Resource>; +} = {}; + +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) => + "#" + hash(humanKey).substring(0, 6); diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index 92c078c..2575a83 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -8,7 +8,7 @@ import { import { human } from "./human"; import dayjs from "dayjs"; import db from "./db"; -import { liveTable, WebsocketIncomingMessage } from "./table"; +import { liveTable, WsOut, WsIn } from "./table"; const api = new Elysia({ prefix: "/api" }) .post("/whoami", async ({ cookie: { token } }) => { @@ -62,11 +62,8 @@ const api = new Elysia({ prefix: "/api" }) ) .get("/games", () => [{ key: "simple", name: "simple" }]) .ws("/ws/:tableKey", { - response: t.Object({ - players: t.Optional(t.Array(t.String())), - view: t.Optional(t.Any()), - }), - body: WebsocketIncomingMessage, + response: WsOut, + body: WsIn, message( { diff --git a/packages/server/src/table.ts b/packages/server/src/table.ts index 07b2c7f..6ee460b 100644 --- a/packages/server/src/table.ts +++ b/packages/server/src/table.ts @@ -9,11 +9,17 @@ import { } from "./games/simple"; import { transform } from "./kefir-extension"; -export const WebsocketIncomingMessage = t.Union([ +export const WsOut = t.Object({ + players: t.Optional(t.Array(t.String())), + view: t.Optional(t.Any()), +}); +export type TWsOut = typeof WsOut.static; +export const WsIn = t.Union([ t.Object({ proposeGame: t.String() }), t.Object({ startGame: t.Literal(true) }), t.Object({ action: t.Any() }), ]); +export type TWsIn = typeof WsIn.static; type TablePayload = { inputs: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6673d3e..0c24abf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: js-cookie: specifier: ^3.0.5 version: 3.0.5 + kefir-bus: + specifier: ^2.3.1 + version: 2.3.1(kefir@3.8.8) object-hash: specifier: ^3.0.0 version: 3.0.0 @@ -42,6 +45,9 @@ importers: '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 + '@types/kefir': + specifier: ^3.8.11 + version: 3.8.11 '@unocss/preset-icons': specifier: ^66.4.2 version: 66.4.2