deep in kefir lore

This commit is contained in:
2025-08-22 00:19:40 -04:00
parent 7d8ac0db76
commit cc53470ddf
8 changed files with 187 additions and 173 deletions

View File

@@ -1,28 +1,34 @@
import { XOR } from "ts-xor";
import { t } from "elysia";
import { combine, Property } from "kefir";
import Bus, { type Bus as TBus } from "kefir-bus";
import { merge, Observable, Property, Stream } from "kefir";
import { Game } from "@prisma/client";
import {
newGame,
resolveAction,
SimpleAction,
SimpleGameState,
} from "./games/simple";
import { transform } from "./kefir-extension";
type TableInputEvent<GameAction> = { humanKey: string } & XOR<
{ presence: "joined" | "left" },
{ proposeGame: string },
{ startGame: true },
{ action: GameAction }
>;
export const WebsocketIncomingMessage = t.Union([
t.Object({ proposeGame: t.String() }),
t.Object({ startGame: t.Literal(true) }),
t.Object({ action: t.Any() }),
]);
type InMemoryTable<GameState> = {
playersPresent: string[];
gameState: GameState | null;
};
type TableOutputEvents<GameState> = {
playersPresent: Property<string[], never>;
gameState: Property<GameState | null, never>;
};
type TablePayload<GameAction, GameState> = {
input: TBus<TableInputEvent<GameAction>, never>;
outputs: TableOutputEvents<GameState>;
type TablePayload<GameState, GameAction> = {
inputs: {
presenceChanges: TBus<
{ humanKey: string; presence: "joined" | "left" },
never
>;
gameProposals: TBus<{ proposeGame: string }, never>;
gameStarts: TBus<{ startGame: true }, never>;
gameActions: TBus<GameAction, never>;
};
outputs: {
playersPresent: Property<string[], never>;
gameState: Property<GameState | null, never>;
};
};
const tables: {
@@ -31,26 +37,55 @@ const tables: {
export const liveTable = <GameState, GameAction>(key: string) => {
if (!(key in tables)) {
const inputEvents = Bus<TableInputEvent<GameAction>, never>();
const inputs: TablePayload<GameState, GameAction>["inputs"] = {
presenceChanges: Bus(),
gameProposals: Bus(),
gameStarts: Bus(),
gameActions: Bus(),
};
const { presenceChanges, 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);
}
return prev;
}, [] as string[]);
const gameState = transform(
null as GameState | null,
[
gameStarts.thru((Evt) =>
combine([Evt], [playersPresent], (evt, players) => ({
...evt,
players,
}))
),
(prev, evt: { players: string[] }) =>
prev == null ? (newGame(evt.players) as GameState) : prev,
],
[
gameActions,
(prev, evt: GameAction) =>
prev != null
? (resolveAction(
prev as unknown as SimpleGameState,
"evt",
evt as SimpleAction
) as GameState)
: prev,
]
).toProperty();
tables[key] = {
input: inputEvents,
inputs,
outputs: {
playersPresent: inputEvents
.filter((evt) => Boolean(evt.presence))
.scan((prev, evt) => {
if (evt.presence == "joined") {
prev.push(evt.humanKey);
} else if (evt.presence == "left") {
prev.splice(prev.indexOf(evt.humanKey), 1);
}
return prev;
}, [] as string[]),
gameState: inputEvents
.filter((evt) => Boolean(evt.startGame || evt.action))
.scan((prev, evt) => {
return prev;
}, null as GameState | null),
playersPresent,
gameState: gameState as Property<unknown, never>,
},
};
@@ -62,5 +97,5 @@ export const liveTable = <GameState, GameAction>(key: string) => {
delete tables[key];
});
}
return tables[key] as TablePayload<GameAction, GameState>;
return tables[key] as TablePayload<GameState, GameAction>;
};