lots more kefir tinkering
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
1
packages/client/src/global.d.ts
vendored
1
packages/client/src/global.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="@solidjs/start/env" />
|
|
||||||
25
packages/client/src/profile.ts
Normal file
25
packages/client/src/profile.ts
Normal 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);
|
||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
6
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user