[wip] so close; check the ws messages
This commit is contained in:
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -79,9 +79,7 @@ export default (props: { tableKey: string }) => {
|
|||||||
<Player
|
<Player
|
||||||
playerKey={player}
|
playerKey={player}
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(0, ${
|
transform: `translate(0, ${verticalOffset() * 150}vh)`,
|
||||||
verticalOffset() * 150
|
|
||||||
}vh)`,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -114,9 +112,7 @@ export default (props: { tableKey: string }) => {
|
|||||||
<div class="absolute tc mt-8 flex gap-4">
|
<div class="absolute tc mt-8 flex gap-4">
|
||||||
<select>
|
<select>
|
||||||
<For each={Object.entries(games)}>
|
<For each={Object.entries(games)}>
|
||||||
{([gameId, game]) => (
|
{([gameId, game]) => <option value={gameId}>{gameId}</option>}
|
||||||
<option value={gameId}>{game.title}</option>
|
|
||||||
)}
|
|
||||||
</For>
|
</For>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { combine } from "kefir";
|
|||||||
import Bus from "kefir-bus";
|
import Bus from "kefir-bus";
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
import { liveTable, WsIn, WsOut } from "./table";
|
import { liveTable, WsIn, WsOut } from "./table";
|
||||||
|
import { err } from "./logging";
|
||||||
|
|
||||||
export const WS = Bus<
|
export const WS = Bus<
|
||||||
{
|
{
|
||||||
@@ -76,13 +77,17 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.ws("/ws/:tableKey", {
|
.ws("/ws/:tableKey", {
|
||||||
async open({
|
body: WsIn,
|
||||||
|
response: WsOut,
|
||||||
|
|
||||||
|
open: ({
|
||||||
data: {
|
data: {
|
||||||
params: { tableKey },
|
params: { tableKey },
|
||||||
humanKey,
|
humanKey,
|
||||||
},
|
},
|
||||||
send,
|
send,
|
||||||
}) {
|
}) => {
|
||||||
|
console.log("websocket opened");
|
||||||
const table = liveTable(tableKey);
|
const table = liveTable(tableKey);
|
||||||
|
|
||||||
table.inputs.connectionChanges.emit({
|
table.inputs.connectionChanges.emit({
|
||||||
@@ -90,22 +95,14 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
presence: "joined",
|
presence: "joined",
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.entries(table.outputs.global).forEach(([type, stream]) =>
|
Object.entries({
|
||||||
|
...table.outputs.global,
|
||||||
|
...(table.outputs.player[humanKey] ?? {}),
|
||||||
|
}).forEach(([type, stream]) =>
|
||||||
stream.onValue((v) => send({ [type]: v }))
|
stream.onValue((v) => send({ [type]: v }))
|
||||||
);
|
);
|
||||||
|
|
||||||
combine(
|
|
||||||
[table.outputs.gameState],
|
|
||||||
[table.outputs.gameImpl],
|
|
||||||
(state, game: Game) => state && game.getView({ state, humanKey })
|
|
||||||
)
|
|
||||||
.toProperty()
|
|
||||||
.onValue((view) => send({ view }));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
body: WsIn,
|
|
||||||
response: WsOut,
|
|
||||||
|
|
||||||
message: (
|
message: (
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
@@ -114,19 +111,20 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
body
|
body
|
||||||
) => WS.emit({ ...body, type: "message", humanKey, tableKey }),
|
) => liveTable(tableKey).inputs.messages.emit({ ...body, humanKey }),
|
||||||
|
|
||||||
close({
|
close: ({
|
||||||
data: {
|
data: {
|
||||||
params: { tableKey },
|
params: { tableKey },
|
||||||
humanKey,
|
humanKey,
|
||||||
},
|
},
|
||||||
}) {
|
}) =>
|
||||||
liveTable(tableKey).inputs.connectionChanges.emit({
|
liveTable(tableKey).inputs.connectionChanges.emit({
|
||||||
humanKey,
|
humanKey,
|
||||||
presence: "left",
|
presence: "left",
|
||||||
});
|
}),
|
||||||
},
|
|
||||||
|
error: (error) => err(error),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ new Elysia()
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
.onRequest(({ request }) => log.log(request))
|
.onRequest(({ request }) => console.log(request.url))
|
||||||
.onError(({ error }) => log.err(error))
|
.onError(({ error }) => console.error(error))
|
||||||
|
|
||||||
.get("/ping", () => "pong")
|
.get("/ping", () => "pong")
|
||||||
.use(api)
|
.use(api)
|
||||||
|
|||||||
@@ -10,4 +10,7 @@ export const log = (value: unknown) => LogBus.emit(value);
|
|||||||
export const err = (value: unknown) =>
|
export const err = (value: unknown) =>
|
||||||
LogBus.emitEvent({ type: "error", value });
|
LogBus.emitEvent({ type: "error", value });
|
||||||
|
|
||||||
LogPool.log();
|
LogStream.log();
|
||||||
|
LogStream.onError((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import GAMES, { Game, GameKey } from "@games/shared/games";
|
import GAMES, { Game, GameKey } from "@games/shared/games";
|
||||||
import { isEmpty, multiScan, ValueWithin } from "@games/shared/kefir";
|
import {
|
||||||
|
isEmpty,
|
||||||
|
multiScan,
|
||||||
|
partition,
|
||||||
|
set,
|
||||||
|
setDiff,
|
||||||
|
ValueWithin,
|
||||||
|
} from "@games/shared/kefirs";
|
||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import { combine, pool, Property } from "kefir";
|
import { combine, Observable, pool, Property } from "kefir";
|
||||||
import Bus, { type Bus as TBus } from "kefir-bus";
|
import Bus, { type Bus as TBus } from "kefir-bus";
|
||||||
import { log } from "./logging";
|
import { log } from "./logging";
|
||||||
|
|
||||||
@@ -31,15 +38,7 @@ type TablePayload<
|
|||||||
},
|
},
|
||||||
never
|
never
|
||||||
>;
|
>;
|
||||||
|
messages: TBus<Attributed & TWsIn, any>;
|
||||||
readys: TBus<
|
|
||||||
Attributed & {
|
|
||||||
ready: boolean;
|
|
||||||
},
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
actions: TBus<Attributed & GameAction, any>;
|
|
||||||
quits: TBus<Attributed, any>;
|
|
||||||
};
|
};
|
||||||
outputs: {
|
outputs: {
|
||||||
global: {
|
global: {
|
||||||
@@ -77,14 +76,11 @@ export const liveTable = <
|
|||||||
if (!(key in tables)) {
|
if (!(key in tables)) {
|
||||||
const inputs: TablePayload<GameConfig, GameState, GameAction>["inputs"] = {
|
const inputs: TablePayload<GameConfig, GameState, GameAction>["inputs"] = {
|
||||||
connectionChanges: Bus(),
|
connectionChanges: Bus(),
|
||||||
readys: Bus(),
|
messages: Bus(),
|
||||||
actions: Bus(),
|
|
||||||
quits: Bus(),
|
|
||||||
};
|
};
|
||||||
const { connectionChanges, readys, actions, quits } = inputs;
|
const { connectionChanges, messages } = inputs;
|
||||||
|
|
||||||
// =======
|
// =======
|
||||||
const playerStreams = {};
|
|
||||||
|
|
||||||
// players who have at least one connection to the room
|
// players who have at least one connection to the room
|
||||||
const playersPresent = connectionChanges
|
const playersPresent = connectionChanges
|
||||||
@@ -102,7 +98,37 @@ export const liveTable = <
|
|||||||
.map((counts) => Object.keys(counts))
|
.map((counts) => Object.keys(counts))
|
||||||
.toProperty();
|
.toProperty();
|
||||||
|
|
||||||
const gameEnds = quits.map((_) => null);
|
const playerStreams: TablePayload<
|
||||||
|
GameConfig,
|
||||||
|
GameState,
|
||||||
|
GameAction
|
||||||
|
>["outputs"]["player"] = {};
|
||||||
|
playersPresent
|
||||||
|
.map(set)
|
||||||
|
.slidingWindow(2, 2)
|
||||||
|
.map(([prev, cur]) => setDiff([prev, cur]))
|
||||||
|
.onValue(({ added, removed }) => {
|
||||||
|
added.forEach((p) => {
|
||||||
|
playerStreams[p] = {
|
||||||
|
view: Bus(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
removed.forEach((p) => {
|
||||||
|
delete playerStreams[p];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ready, action, quit } = partition(
|
||||||
|
["ready", "action", "quit"],
|
||||||
|
messages
|
||||||
|
) as unknown as {
|
||||||
|
// yuck
|
||||||
|
ready: Observable<Attributed & { ready: boolean }, any>;
|
||||||
|
action: Observable<Attributed & { action: GameAction }, any>;
|
||||||
|
quit: Observable<Attributed, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gameEnds = quit.map((_) => null);
|
||||||
|
|
||||||
const playersReady = multiScan(
|
const playersReady = multiScan(
|
||||||
null as {
|
null as {
|
||||||
@@ -114,8 +140,8 @@ export const liveTable = <
|
|||||||
Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])),
|
Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
readys,
|
ready,
|
||||||
(prev, evt: ValueWithin<typeof readys>) =>
|
(prev, evt: ValueWithin<typeof ready>) =>
|
||||||
prev?.[evt.humanKey] != null
|
prev?.[evt.humanKey] != null
|
||||||
? {
|
? {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -165,7 +191,7 @@ export const liveTable = <
|
|||||||
prev || (game.init() as GameState),
|
prev || (game.init() as GameState),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
combine([actions], [gameImpl], (action, impl) => ({
|
combine([action], [gameImpl], (action, impl) => ({
|
||||||
action,
|
action,
|
||||||
...impl,
|
...impl,
|
||||||
})),
|
})),
|
||||||
@@ -185,7 +211,7 @@ export const liveTable = <
|
|||||||
action,
|
action,
|
||||||
}) as GameState),
|
}) as GameState),
|
||||||
],
|
],
|
||||||
[quits, () => null]
|
[quit, () => null]
|
||||||
).toProperty();
|
).toProperty();
|
||||||
|
|
||||||
const gameIsActive = gameState
|
const gameIsActive = gameState
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { merge, Observable } from "kefir";
|
|
||||||
|
|
||||||
export type ValueWithin<O extends Observable<any, any>> = Parameters<
|
|
||||||
Parameters<O["map"]>[0]
|
|
||||||
>[0];
|
|
||||||
|
|
||||||
type Mutation<A, O extends Observable<any, any>> = [
|
|
||||||
O,
|
|
||||||
(prev: A, value: ValueWithin<O>) => A
|
|
||||||
];
|
|
||||||
|
|
||||||
export const multiScan = <A, M extends Mutation<A, any>[]>(
|
|
||||||
initValue: A,
|
|
||||||
...mutations: M
|
|
||||||
): Observable<A, any> =>
|
|
||||||
merge(
|
|
||||||
mutations.map(([source, mutation]) =>
|
|
||||||
source.map((event) => ({ event, mutation }))
|
|
||||||
)
|
|
||||||
).scan((prev, { event, mutation }) => mutation(prev, event), initValue);
|
|
||||||
|
|
||||||
export const partition =
|
|
||||||
<C extends readonly [...string[]], T, E>(
|
|
||||||
classes: C,
|
|
||||||
partitionFn: (v: T) => C[number]
|
|
||||||
) =>
|
|
||||||
(obs: Observable<T, E>) => {
|
|
||||||
const assigned = obs.map((obj) => ({ obj, cls: partitionFn(obj) }));
|
|
||||||
return Object.fromEntries(
|
|
||||||
classes.map((C) => [
|
|
||||||
C,
|
|
||||||
assigned.filter(({ cls }) => cls == C).map(({ obj }) => obj),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isEmpty = (container: { length: number }) => container.length == 0;
|
|
||||||
52
pkg/shared/kefirs.ts
Normal file
52
pkg/shared/kefirs.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { merge, Observable } from "kefir";
|
||||||
|
import Bus from "kefir-bus";
|
||||||
|
|
||||||
|
export type ValueWithin<O extends Observable<any, any>> = Parameters<
|
||||||
|
Parameters<O["map"]>[0]
|
||||||
|
>[0];
|
||||||
|
|
||||||
|
type Mutation<A, O extends Observable<any, any>> = [
|
||||||
|
O,
|
||||||
|
(prev: A, value: ValueWithin<O>) => A
|
||||||
|
];
|
||||||
|
|
||||||
|
export const multiScan = <A, M extends Mutation<A, any>[]>(
|
||||||
|
initValue: A,
|
||||||
|
...mutations: M
|
||||||
|
): Observable<A, any> =>
|
||||||
|
merge(
|
||||||
|
mutations.map(([source, mutation]) =>
|
||||||
|
source.map((event) => ({ event, mutation }))
|
||||||
|
)
|
||||||
|
).scan((prev, { event, mutation }) => mutation(prev, event), initValue);
|
||||||
|
|
||||||
|
export const partition = <
|
||||||
|
C extends readonly [...string[]],
|
||||||
|
T extends { [key: string]: any },
|
||||||
|
E
|
||||||
|
>(
|
||||||
|
classes: C,
|
||||||
|
obs: Observable<T, E>
|
||||||
|
) => {
|
||||||
|
const classBuses = Object.fromEntries(classes.map((c) => [c, Bus()]));
|
||||||
|
obs.onValue((v) => {
|
||||||
|
for (const _class of classes) {
|
||||||
|
if (_class in v) {
|
||||||
|
classBuses[_class].emit(v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return classBuses;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isEmpty = (container: { length: number }) => container.length == 0;
|
||||||
|
|
||||||
|
export const setDiff = <T>(
|
||||||
|
sets: [Set<T>, s2: Set<T>]
|
||||||
|
): { added: T[]; removed: T[] } => ({
|
||||||
|
added: [...sets[1].difference(sets[0])],
|
||||||
|
removed: [...sets[0].difference(sets[1])],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const set = <T>(arr: T[]) => new Set<T>(arr);
|
||||||
@@ -3,9 +3,11 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"kefir": "^3.8.8",
|
"kefir": "^3.8.8",
|
||||||
|
"kefir-bus": "^2.3.1",
|
||||||
"object-hash": "^3.0.0"
|
"object-hash": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/kefir": "^3.8.11"
|
"@types/kefir": "^3.8.11",
|
||||||
|
"ts-xor": "^1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true,
|
||||||
|
"target": "esnext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"module": "nodenext"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -130,6 +130,9 @@ importers:
|
|||||||
kefir:
|
kefir:
|
||||||
specifier: ^3.8.8
|
specifier: ^3.8.8
|
||||||
version: 3.8.8
|
version: 3.8.8
|
||||||
|
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
|
||||||
@@ -137,6 +140,9 @@ importers:
|
|||||||
'@types/kefir':
|
'@types/kefir':
|
||||||
specifier: ^3.8.11
|
specifier: ^3.8.11
|
||||||
version: 3.8.11
|
version: 3.8.11
|
||||||
|
ts-xor:
|
||||||
|
specifier: ^1.3.0
|
||||||
|
version: 1.3.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -983,6 +989,7 @@ packages:
|
|||||||
|
|
||||||
dayjs@1.11.17:
|
dayjs@1.11.17:
|
||||||
resolution: {integrity: sha512-qgiVOerp5QCVMfE7hZ6z/gM+ug52I+xv70rCf6rWJXf0b4d8Kv6Yw9BmcLZlCvP3Xz8i3fjXbaU6Hwu7aGRXfA==}
|
resolution: {integrity: sha512-qgiVOerp5QCVMfE7hZ6z/gM+ug52I+xv70rCf6rWJXf0b4d8Kv6Yw9BmcLZlCvP3Xz8i3fjXbaU6Hwu7aGRXfA==}
|
||||||
|
deprecated: This version has a bug that adds semantic-release dependencies by mistake. Please upgrade to 1.11.18.
|
||||||
|
|
||||||
debug@4.3.4:
|
debug@4.3.4:
|
||||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||||
|
|||||||
Reference in New Issue
Block a user