cooking with kefir

This commit is contained in:
2025-08-20 21:56:23 -04:00
parent 265aad4522
commit 35a5af154f
9 changed files with 245 additions and 188 deletions

View File

@@ -0,0 +1,66 @@
import { XOR } from "ts-xor";
import Bus, { type Bus as TBus } from "kefir-bus";
import { merge, Observable, Property, Stream } from "kefir";
import { Game } from "@prisma/client";
type TableInputEvent<GameAction> = { humanKey: string } & XOR<
{ presence: "joined" | "left" },
{ proposeGame: string },
{ startGame: true },
{ action: GameAction }
>;
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>;
};
const tables: {
[key: string]: TablePayload<unknown, unknown>;
} = {};
export const liveTable = <GameState, GameAction>(key: string) => {
if (!(key in tables)) {
const inputEvents = Bus<TableInputEvent<GameAction>, never>();
tables[key] = {
input: inputEvents,
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),
},
};
// cleanup
tables[key].outputs.playersPresent
.slidingWindow(2)
.filter(([prev, curr]) => prev.length > 0 && curr.length == 0)
.onValue((_) => {
delete tables[key];
});
}
return tables[key] as TablePayload<GameAction, GameState>;
};