lots more kefir tinkering

This commit is contained in:
2025-08-23 17:34:50 -04:00
parent cc53470ddf
commit a2e8887a0b
9 changed files with 99 additions and 53 deletions

View File

@@ -11,12 +11,14 @@
"@solid-primitives/scheduled": "^1.5.2", "@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.15.3", "@solidjs/router": "^0.15.3",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"kefir-bus": "^2.3.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"solid-js": "^1.9.5" "solid-js": "^1.9.5"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/solar": "^1.2.4", "@iconify-json/solar": "^1.2.4",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/kefir": "^3.8.11",
"@unocss/preset-icons": "^66.4.2", "@unocss/preset-icons": "^66.4.2",
"@unocss/preset-wind4": "^66.4.2", "@unocss/preset-wind4": "^66.4.2",
"unocss": "^66.4.2", "unocss": "^66.4.2",

View File

@@ -1,6 +1,8 @@
import { createResource } from "solid-js"; import { createResource } from "solid-js";
import { type Api } from "../../server/src/api"; import { type Api } from "../../server/src/api";
import { treaty } from "@elysiajs/eden"; import { treaty } from "@elysiajs/eden";
import { EdenWS } from "@elysiajs/eden/treaty";
import { fromEvents } from "kefir";
const { api } = treaty<Api>( const { api } = treaty<Api>(
import.meta.env.DEV ? "http://localhost:5001" : window.location.origin, import.meta.env.DEV ? "http://localhost:5001" : window.location.origin,
@@ -10,4 +12,5 @@ const { api } = treaty<Api>(
); );
export default api; export default api;
export const [me] = createResource(() => api.whoami.post().then((r) => r.data)); export const fromWebsocket = <T>(ws: any) =>
fromEvents<T, never>(ws, "message");

View File

@@ -1,65 +1,48 @@
import { import {
Accessor, Accessor,
createContext, createContext,
createEffect,
createResource,
createSignal, createSignal,
For, For,
onCleanup, onCleanup,
Resource,
Show, Show,
untrack,
} from "solid-js"; } from "solid-js";
import { createStore } from "solid-js/store";
import { SimplePlayerView } from "../../../server/src/games/simple"; import { SimplePlayerView } from "../../../server/src/games/simple";
import api, { me } from "../api"; import api, { fromWebsocket } from "../api";
import { ApiType } from "../fn"; import { me, playerColor, profile } from "../profile";
import Game from "./Game"; import { fromEvents, Stream, stream } from "kefir";
import Bus from "kefir-bus";
const [playerProfiles, setPlayerProfiles] = createStore< import { createObservable, createObservableWithInit, WSEvent } from "../fn";
Record<string, Resource<ApiType<typeof api.profile.get>>> import { EdenWS } from "@elysiajs/eden/treaty";
>({}); import { TWsIn, TWsOut } from "../../../server/src/table";
export const TableContext = createContext<{ export const TableContext = createContext<{
players: Accessor<string[]>; players: Accessor<string[]>;
view: Accessor<SimplePlayerView | undefined>; view: Accessor<any>;
// submitAction: (action: Action) => Promise<any>; sendWs: (msg: TWsIn) => void;
}>(); }>();
export default (props: { tableKey: string }) => { export default (props: { tableKey: string }) => {
const [players, setPlayers] = createSignal<string[]>([]);
const [view, setView] = createSignal<SimplePlayerView>();
const ws = api.ws(props).subscribe(); const ws = api.ws(props).subscribe();
const wsEvents = fromWebsocket<TWsOut>(ws);
onCleanup(() => ws.close()); onCleanup(() => ws.close());
ws.on("message", (evt) => { const presenceEvents = wsEvents.filter((evt) => evt.players != null);
if (evt.data.players) { const gameEvents = wsEvents.filter((evt) => evt.view != null);
setPlayers(evt.data.players);
}
if (evt.data.view) {
setView(evt.data.view);
}
});
createEffect(() => { const players = createObservableWithInit<string[]>(
players().forEach((player) => { presenceEvents.map((evt) => evt.players!),
if (!untrack(() => playerProfiles[player])) { []
const [playerProfile] = createResource(() => );
api.profile const view = createObservable(gameEvents.map((evt) => evt.view));
.get({ query: { otherHumanKey: player } })
.then((r) => r.data)
);
setPlayerProfiles((prev) => ({
...prev,
[player]: playerProfile,
}));
}
});
});
return ( return (
<TableContext.Provider value={{ view, players }}> <TableContext.Provider
value={{
sendWs: ws.send,
view,
players,
}}
>
<div class="flex justify-around p-t-10"> <div class="flex justify-around p-t-10">
<For each={players().filter((p) => p != me())}> <For each={players().filter((p) => p != me())}>
{(player, i) => { {(player, i) => {
@@ -75,11 +58,12 @@ export default (props: { tableKey: string }) => {
transform: `translate(0, ${ transform: `translate(0, ${
verticalOffset() * 150 verticalOffset() * 150
}vh)`, }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"
> >
<p style={{ "font-size": "1em" }}> <p style={{ "font-size": "1em" }}>
{playerProfiles[player]?.()?.name} {profile(player)()?.name}
</p> </p>
</div> </div>
); );
@@ -105,7 +89,7 @@ export default (props: { tableKey: string }) => {
</Show> </Show>
</div> </div>
<Show when={view() != null}> <Show when={view() != null}>
<Game tableKey={props.tableKey} /> <div>Game started!</div>
</Show> </Show>
</TableContext.Provider> </TableContext.Provider>
); );

View File

@@ -1,3 +1,6 @@
import { Observable } from "kefir";
import { Accessor, createSignal } from "solid-js";
declare global { declare global {
interface Array<T> { interface Array<T> {
thru<S>(fn: (arr: T[]) => S): S; thru<S>(fn: (arr: T[]) => S): S;
@@ -11,3 +14,24 @@ export const clone = <T>(o: T): T => JSON.parse(JSON.stringify(o));
export type ApiType<T extends () => Promise<{ data: any }>> = Awaited< export type ApiType<T extends () => Promise<{ data: any }>> = Awaited<
ReturnType<T> ReturnType<T>
>["data"]; >["data"];
export type WSEvent<
T extends { subscribe: (handler: (...args: any[]) => any) => any }
> = Parameters<Parameters<T["subscribe"]>[0]>[0];
export const createObservable = <T>(obs: Observable<T, any>) => {
const [signal, setSignal] = createSignal<T>();
obs.onValue((val) => setSignal(() => val));
return signal;
};
export const createObservableWithInit = <T>(
obs: Observable<T, any>,
init: T
) => {
const [signal, setSignal] = createSignal<T>(init);
obs.onValue((val) => setSignal(() => val));
return signal;
};

View File

@@ -1 +0,0 @@
/// <reference types="@solidjs/start/env" />

View File

@@ -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<ApiType<typeof api.profile.get>>;
} = {};
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);

View File

@@ -8,7 +8,7 @@ import {
import { human } from "./human"; import { human } from "./human";
import dayjs from "dayjs"; import dayjs from "dayjs";
import db from "./db"; import db from "./db";
import { liveTable, WebsocketIncomingMessage } from "./table"; import { liveTable, WsOut, WsIn } from "./table";
const api = new Elysia({ prefix: "/api" }) const api = new Elysia({ prefix: "/api" })
.post("/whoami", async ({ cookie: { token } }) => { .post("/whoami", async ({ cookie: { token } }) => {
@@ -62,11 +62,8 @@ const api = new Elysia({ prefix: "/api" })
) )
.get("/games", () => [{ key: "simple", name: "simple" }]) .get("/games", () => [{ key: "simple", name: "simple" }])
.ws("/ws/:tableKey", { .ws("/ws/:tableKey", {
response: t.Object({ response: WsOut,
players: t.Optional(t.Array(t.String())), body: WsIn,
view: t.Optional(t.Any()),
}),
body: WebsocketIncomingMessage,
message( message(
{ {

View File

@@ -9,11 +9,17 @@ import {
} from "./games/simple"; } from "./games/simple";
import { transform } from "./kefir-extension"; 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({ proposeGame: t.String() }),
t.Object({ startGame: t.Literal(true) }), t.Object({ startGame: t.Literal(true) }),
t.Object({ action: t.Any() }), t.Object({ action: t.Any() }),
]); ]);
export type TWsIn = typeof WsIn.static;
type TablePayload<GameState, GameAction> = { type TablePayload<GameState, GameAction> = {
inputs: { inputs: {

6
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
js-cookie: js-cookie:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5 version: 3.0.5
kefir-bus:
specifier: ^2.3.1
version: 2.3.1(kefir@3.8.8)
object-hash: object-hash:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
@@ -42,6 +45,9 @@ importers:
'@types/js-cookie': '@types/js-cookie':
specifier: ^3.0.6 specifier: ^3.0.6
version: 3.0.6 version: 3.0.6
'@types/kefir':
specifier: ^3.8.11
version: 3.8.11
'@unocss/preset-icons': '@unocss/preset-icons':
specifier: ^66.4.2 specifier: ^66.4.2
version: 66.4.2 version: 66.4.2