Compare commits

..

8 Commits

Author SHA1 Message Date
3347452ec4 end to end! 2025-08-23 20:33:52 -04:00
a2e8887a0b lots more kefir tinkering 2025-08-23 17:34:50 -04:00
cc53470ddf deep in kefir lore 2025-08-22 00:19:40 -04:00
7d8ac0db76 around the table 2025-08-21 00:27:58 -04:00
35a5af154f cooking with kefir 2025-08-20 21:56:23 -04:00
265aad4522 checkpoint 2025-08-19 22:19:24 -04:00
287c19fc0d cooking with websockets 2025-08-18 23:31:28 -04:00
3f1635880a tokens fr 2025-08-18 17:47:39 -04:00
30 changed files with 756 additions and 292 deletions

View File

@@ -1,10 +0,0 @@
node_modules
Dockerfile
Makefile
README.md
.output
.vinxi
.git
.gitignore
.dockerignore
*.db

View File

@@ -1,11 +0,0 @@
FROM node:22-alpine
WORKDIR /app
EXPOSE 3000
COPY package.json ./
RUN --mount=type=cache,target=/root/.npm npm install
COPY . .
RUN --mount=type=cache,target=/app/.vinxi npm run build
CMD ["npm", "run", "start"]

View File

@@ -1,7 +1,7 @@
{
"name": "games",
"type": "module",
"version": "0.0.2",
"version": "0.0.4",
"scripts": {
"dev": "pnpm --parallel dev",
"build": "pnpm run -F client build",
@@ -10,7 +10,14 @@
"pnpm": {
"overrides": {
"object-hash": "^3.0.0"
}
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"@prisma/client",
"@prisma/engines",
"esbuild",
"prisma"
]
},
"devDependencies": {
"@types/object-hash": "^3.0.6"

View File

@@ -1,7 +1,7 @@
{
"name": "@games/client",
"type": "module",
"version": "0.0.3",
"version": "0.0.4",
"scripts": {
"dev": "vite --port 3000",
"build": "vite build"
@@ -11,12 +11,15 @@
"@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.15.3",
"js-cookie": "^3.0.5",
"kefir": "^3.8.8",
"kefir-bus": "^2.3.1",
"object-hash": "^3.0.0",
"solid-js": "^1.9.5"
},
"devDependencies": {
"@iconify-json/solar": "^1.2.4",
"@types/js-cookie": "^3.0.6",
"@types/kefir": "^3.8.11",
"@unocss/preset-icons": "^66.4.2",
"@unocss/preset-wind4": "^66.4.2",
"unocss": "^66.4.2",

View File

@@ -1,5 +1,8 @@
import { createResource } from "solid-js";
import { type Api } from "../../server/src/api";
import { treaty } from "@elysiajs/eden";
import { EdenWS } from "@elysiajs/eden/treaty";
import { fromEvents } from "kefir";
const { api } = treaty<Api>(
import.meta.env.DEV ? "http://localhost:5001" : window.location.origin,
@@ -8,3 +11,8 @@ const { api } = treaty<Api>(
}
);
export default api;
export const fromWebsocket = <T>(ws: any) =>
fromEvents(ws, "message").map(
(evt) => (evt as unknown as { data: T }).data
);

View File

@@ -34,7 +34,8 @@ const Profile = () => {
);
};
const App = () => (
const App = () => {
return (
<Router
root={(props) => (
<>
@@ -50,17 +51,11 @@ const App = () => (
>
<Route path="/" component={lazy(() => import("./routes/index"))} />
<Route
path="/:game"
component={lazy(() => import("./routes/[game]/index"))}
/>
<Route
path="/:game/:instance"
component={lazy(() => import("./routes/[game]/[instance]"))}
path="/:tableKey"
component={lazy(() => import("./routes/[table]"))}
/>
</Router>
);
};
// todo: fix this
(Cookies.get("token") == null ? api.whoami.post() : Promise.resolve()).then(
() => render(App, document.getElementById("app")!)
);
render(App, document.getElementById("app")!);

View File

@@ -1,43 +1,36 @@
import { Accessor, createContext, createResource, Show } from "solid-js";
import { Accessor, createContext, useContext } from "solid-js";
import {
GameState,
Action,
vGameState,
PlayerView,
SimpleAction,
SimplePlayerView,
vSimpleGameState,
} from "../../../server/src/games/simple";
import api from "../api";
import Hand from "./Hand";
import Pile from "./Pile";
import { TableContext } from "./Table";
import Hand from "./Hand";
export const GameContext = createContext<{
view: Accessor<PlayerView | undefined>;
submitAction: (action: Action) => Promise<any>;
view: Accessor<SimplePlayerView>;
submitAction: (action: SimpleAction) => any;
}>();
export default (props: { instanceId: string }) => {
const [view, { mutate }] = createResource(() =>
api
.simple(props)
.get()
.then((res) => res.data as PlayerView)
);
const submitAction = (action: Action) =>
api
.simple(props)
.post({ action })
.then((res) => res.status == 200 && mutate(res.data as PlayerView));
export default () => {
const table = useContext(TableContext)!;
const view = table.view as Accessor<SimplePlayerView>;
const submitAction = (action: SimpleAction) => table.sendWs({ action });
return (
<GameContext.Provider value={{ view, submitAction }}>
<Show when={view.latest != undefined}>
<Pile
count={view.latest!.deckCount}
count={view().deckCount}
class="cursor-pointer fixed center"
onClick={() => submitAction({ type: "draw" })}
/>
<Hand class="fixed bc" hand={view.latest!.myHand} />
</Show>
<Hand
class="fixed bc"
hand={view().myHand}
onClickCard={(card) => submitAction({ type: "discard", card })}
/>
</GameContext.Provider>
);
};

View File

@@ -1,26 +1,26 @@
import { Component, For, useContext } from "solid-js";
import Card from "./Card";
import { Hand } from "../../../shared/cards";
import type { Hand as THand, Card as TCard } from "../../../shared/cards";
import { GameContext } from "./Game";
import { produce } from "solid-js/store";
import { Stylable } from "./toolbox";
export default ((props) => {
const { submitAction, view } = useContext(GameContext)!;
return (
<div class={"hand " + props.class} style={props.style}>
<For each={props.hand}>
{(card) => (
{(card, i) => (
<Card
card={card}
style={{
cursor: "pointer",
}}
onClick={() => submitAction({ type: "discard", card })}
onClick={() => props.onClickCard?.(card, i())}
/>
)}
</For>
</div>
);
}) satisfies Component<{ hand: Hand } & Stylable>;
}) satisfies Component<
{ hand: THand; onClickCard?: (card: TCard, i: number) => any } & Stylable
>;

View File

@@ -0,0 +1,18 @@
import { playerColor, profile } from "../profile";
import { Stylable } from "./toolbox";
export default (props: { playerKey: string } & Stylable) => {
return (
<div
style={{
...props.style,
"background-color": playerColor(props.playerKey),
}}
class={`${props.class} w-20 h-20 rounded-full flex justify-center items-center`}
>
<p style={{ "font-size": "1em" }}>
{profile(props.playerKey)()?.name}
</p>
</div>
);
};

View File

@@ -0,0 +1,96 @@
import {
Accessor,
createContext,
createEffect,
createSignal,
For,
onCleanup,
Show,
} from "solid-js";
import { SimplePlayerView } from "../../../server/src/games/simple";
import api, { fromWebsocket } from "../api";
import { me, playerColor, profile } from "../profile";
import { fromEvents, Stream, stream } from "kefir";
import Bus from "kefir-bus";
import { createObservable, createObservableWithInit, WSEvent } from "../fn";
import { EdenWS } from "@elysiajs/eden/treaty";
import { TWsIn, TWsOut } from "../../../server/src/table";
import Player from "./Player";
import Game from "./Game";
export const TableContext = createContext<{
players: Accessor<string[]>;
view: Accessor<any>;
sendWs: (msg: TWsIn) => void;
}>();
export default (props: { tableKey: string }) => {
const ws = api.ws(props).subscribe();
const wsEvents = fromWebsocket<TWsOut>(ws);
onCleanup(() => ws.close());
const presenceEvents = wsEvents.filter((evt) => evt.players != null);
const gameEvents = wsEvents.filter((evt) => evt.view != null);
const players = createObservableWithInit<string[]>(
presenceEvents.map((evt) => evt.players!),
[]
);
const view = createObservable(gameEvents.map((evt) => evt.view));
return (
<TableContext.Provider
value={{
sendWs: (evt) => ws.send(evt),
view,
players,
}}
>
<div class="flex justify-around p-t-10">
<For each={players().filter((p) => p != me())}>
{(player, i) => {
const verticalOffset = () => {
const N = players().length - 1;
const x = Math.abs((2 * i() + 1) / (N * 2) - 0.5);
const y = Math.sqrt(1 - x * x);
return 1 - y;
};
return (
<Player
playerKey={player}
style={{
transform: `translate(0, ${
verticalOffset() * 150
}vh)`,
}}
/>
);
}}
</For>
</div>
<div
id="table"
class="fixed bg-radial from-orange-950 to-stone-950 border-neutral-950 border-4 top-40 bottom-20 left-10 right-10 shadow-lg"
style={{
"border-radius": "50% 50%",
}}
>
<Show when={view() == null}>
<div class="absolute center">
<button
onClick={() => ws.send({ startGame: true })}
class="button p-1 "
>
Start Game!
</button>
</div>
</Show>
</div>
<Show when={view() != null}>
<Game />
</Show>
</TableContext.Provider>
);
};

View File

@@ -1,3 +1,6 @@
import { Observable } from "kefir";
import { Accessor, createSignal } from "solid-js";
declare global {
interface Array<T> {
thru<S>(fn: (arr: T[]) => S): S;
@@ -7,3 +10,28 @@ Array.prototype.thru = function <T, S>(this: T[], fn: (arr: T[]) => S) {
return fn(this);
};
export const clone = <T>(o: T): T => JSON.parse(JSON.stringify(o));
export type ApiType<T extends () => Promise<{ data: any }>> = Awaited<
ReturnType<T>
>["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

@@ -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

@@ -15,12 +15,7 @@ export default () => {
<Suspense>
<div style={{ padding: "20px" }}>
<p class="text-[40px]">{param.game}</p>
<button
class="px-2 py-1.5 m-4 button rounded"
onClick={() => api.simple.newGame.post().then(refetch)}
>
New Game
</button>
<button class="px-2 py-1.5 m-4 button">New Game</button>
<ul>
<For each={instances() ?? []}>
{(instance) => (

View File

@@ -0,0 +1,16 @@
import { A, useParams } from "@solidjs/router";
import Table from "../components/Table";
export default () => {
const { tableKey } = useParams();
return (
<>
<Table tableKey={tableKey} />
<A href={"/"} class="fixed tl m-4 px-2 py-1.5 button">
Back
</A>
</>
);
};

View File

@@ -29,7 +29,6 @@ a:visited {
background-color: white;
color: black;
box-shadow: 0px 5px 10px black;
border-radius: 10%;
transition: background-color 0.15s, color 0.15s, transform 0.15s;
}
.button:hover {

View File

@@ -31,6 +31,16 @@ export default defineConfig({
"margin-right": "auto",
},
],
[
"tc",
{
top: 0,
left: 0,
right: 0,
"margin-left": "auto",
"margin-right": "auto",
},
],
[
"center",

View File

@@ -0,0 +1,45 @@
/*
Warnings:
- The required column `token` was added to the `Human` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
*/
-- CreateTable
CREATE TABLE "_HumanToInstance" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_HumanToInstance_A_fkey" FOREIGN KEY ("A") REFERENCES "Human" ("key") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_HumanToInstance_B_fkey" FOREIGN KEY ("B") REFERENCES "Instance" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Human" (
"key" TEXT NOT NULL PRIMARY KEY,
"token" TEXT NOT NULL,
"name" TEXT NOT NULL DEFAULT '__name__',
"lastActive" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_Human" ("key", "name") SELECT "key", "name" FROM "Human";
DROP TABLE "Human";
ALTER TABLE "new_Human" RENAME TO "Human";
CREATE UNIQUE INDEX "Human_token_key" ON "Human"("token");
CREATE TABLE "new_Instance" (
"id" TEXT NOT NULL PRIMARY KEY,
"createdByKey" TEXT NOT NULL,
"gameKey" TEXT NOT NULL,
"gameState" JSONB NOT NULL,
CONSTRAINT "Instance_gameKey_fkey" FOREIGN KEY ("gameKey") REFERENCES "Game" ("key") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Instance" ("createdByKey", "gameKey", "gameState", "id") SELECT "createdByKey", "gameKey", "gameState", "id" FROM "Instance";
DROP TABLE "Instance";
ALTER TABLE "new_Instance" RENAME TO "Instance";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "_HumanToInstance_AB_unique" ON "_HumanToInstance"("A", "B");
-- CreateIndex
CREATE INDEX "_HumanToInstance_B_index" ON "_HumanToInstance"("B");

View File

@@ -0,0 +1,15 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Instance" (
"id" TEXT NOT NULL PRIMARY KEY,
"createdByKey" TEXT NOT NULL,
"gameKey" TEXT NOT NULL,
"gameState" JSONB,
CONSTRAINT "Instance_gameKey_fkey" FOREIGN KEY ("gameKey") REFERENCES "Game" ("key") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Instance" ("createdByKey", "gameKey", "gameState", "id") SELECT "createdByKey", "gameKey", "gameState", "id" FROM "Instance";
DROP TABLE "Instance";
ALTER TABLE "new_Instance" RENAME TO "Instance";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -17,16 +17,21 @@ model Game {
}
model Human {
key String @id @default(cuid(2))
name String @default("")
Instance Instance[]
key String @id @default(cuid())
token String @unique @default(cuid())
name String @default("__name__")
lastActive DateTime @default(now())
playingInstances Instance[]
}
model Instance {
id String @id @default(cuid(2))
id String @id @default(cuid())
createdByKey String
createdBy Human @relation(fields: [createdByKey], references: [key])
gameKey String
players Human[]
game Game @relation(fields: [gameKey], references: [key])
gameState Json
gameState Json?
}

View File

@@ -16,12 +16,19 @@
"@elysiajs/static": "^1.3.0",
"@games/shared": "workspace:*",
"@prisma/client": "6.13.0",
"dayjs": "^1.11.13",
"elysia": "^1.3.8",
"elysia-ip": "^1.0.10",
"elysia-rate-limit": "^4.4.0",
"kefir": "^3.8.8",
"kefir-bus": "^2.3.1",
"object-hash": "^3.0.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/kefir": "^3.8.11",
"concurrently": "^9.2.0",
"prisma": "6.13.0"
"prisma": "6.13.0",
"ts-xor": "^1.3.0"
}
}

View File

@@ -1,24 +1,41 @@
import { prisma } from "./db/db";
import { Elysia, t } from "elysia";
import { Prisma } from "@prisma/client";
import { simpleApi } from "./games/simple";
import {
SimpleAction,
SimpleGameState,
getKnowledge,
getView,
} from "./games/simple";
import { human } from "./human";
import dayjs from "dayjs";
import db from "./db";
import { liveTable, WsOut, WsIn } from "./table";
const api = new Elysia({ prefix: "/api" })
.post("/whoami", async ({ cookie: { token } }) => {
let human;
if (token.value == null) {
const newHuman = await prisma.human.create({
human = await db.human.create({
data: {},
});
token.value = newHuman.key;
token.set({
value: human.token,
expires: dayjs().add(1, "year").toDate(),
httpOnly: true,
});
} else {
human = await db.human.findUniqueOrThrow({
where: {
token: token.value,
},
});
}
return token.value;
return human.key;
})
.use(human)
.post(
"/setName",
({ body: { name }, humanKey }) =>
prisma.human.update({
db.human.update({
where: {
key: humanKey,
},
@@ -32,25 +49,78 @@ const api = new Elysia({ prefix: "/api" })
}),
}
)
.get("/profile", ({ humanKey }) =>
prisma.human.findFirst({ where: { key: humanKey } })
)
.get("/games", () => prisma.game.findMany())
.get("/instances", ({ query: { game } }) =>
prisma.instance.findMany({
where: {
game: {
name: game,
},
},
select: {
id: true,
},
.get("/profile", ({ humanKey, query: { otherHumanKey } }) =>
db.human
.findFirst({ where: { key: otherHumanKey ?? humanKey } })
.then((human) => {
if (human == null) {
return null;
}
const { token, ...safeProfile } = human;
return safeProfile;
})
)
.get("/games", () => [{ key: "simple", name: "simple" }])
.ws("/ws/:tableKey", {
async open({
data: {
params: { tableKey },
humanKey,
},
send,
}) {
const table = liveTable<SimpleGameState, SimpleAction>(tableKey);
.use(simpleApi);
table.outputs.playersPresent.onValue((players) =>
send({ players })
);
table.outputs.gameState.onValue((gameState) =>
send({
view:
gameState &&
getView(getKnowledge(gameState, humanKey), humanKey),
})
);
table.inputs.presenceChanges.emit({ humanKey, presence: "joined" });
},
response: WsOut,
body: WsIn,
message(
{
data: {
humanKey,
params: { tableKey },
},
},
body
) {
const {
inputs: { gameProposals, gameStarts, gameActions },
} = liveTable(tableKey);
if ("proposeGame" in body) {
gameProposals.emit(body);
} else if ("startGame" in body) {
gameStarts.emit(body);
} else if ("action" in body) {
gameActions.emit({ humanKey, ...body.action });
}
},
async close({
data: {
params: { tableKey },
humanKey,
},
}) {
liveTable(tableKey).inputs.presenceChanges.emit({
humanKey,
presence: "left",
});
},
});
export default api;
export type Api = typeof api;

View File

@@ -2,4 +2,4 @@
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
export default new PrismaClient();

View File

@@ -1,4 +0,0 @@
"use server";
import { prisma } from "./db";
export const queryAll = async () => await prisma.game.findMany();

View File

@@ -1,25 +0,0 @@
"use server";
import { GameState, newDeck, shuffle } from "../types/cards";
import { prisma } from "./db";
export const queryInstances = async (gameName: string) =>
prisma.instance.findMany({ where: { game: { name: gameName } } });
export const createInstance = (gameName: string) =>
prisma.instance.create({
data: {
gameState: { deck: shuffle(newDeck()), hand: [] } as GameState,
game: { connect: { name: gameName } },
},
});
export const getGameState = (instanceId: number) =>
prisma.instance
.findUnique({ where: { id: instanceId } })
.then((i) => i?.gameState as GameState | undefined);
export const updateGameState = async (
instanceId: number,
gameState: GameState
) => prisma.instance.update({ where: { id: instanceId }, data: { gameState } });

View File

@@ -1,20 +1,10 @@
import {
Card,
Hand,
newDeck,
Pile,
shuffle,
vCard,
} from "../../../shared/cards";
import { Card, Hand, newDeck, Pile, shuffle, vCard } from "@games/shared/cards";
import { heq } from "@games/shared/utils";
import { Elysia, t } from "elysia";
import { prisma } from "../db/db";
import { human } from "../human";
// omniscient game state
export type GameState = {
export type SimpleGameState = {
prev?: {
action: Action;
action: SimpleAction;
};
deck: Pile;
@@ -22,7 +12,7 @@ export type GameState = {
};
// a particular player's knowledge of the global game state
export type vGameState = {
export type vSimpleGameState = {
humanId: string;
deck: Pile<vCard>;
@@ -30,7 +20,7 @@ export type vGameState = {
};
// a particular player's point of view in the game
export type PlayerView = {
export type SimplePlayerView = {
humanId: string;
deckCount: number;
@@ -38,20 +28,20 @@ export type PlayerView = {
myHand: Hand<Card>;
};
export type Action = { type: "draw" } | { type: "discard"; card: Card };
export type SimpleAction = { type: "draw" } | { type: "discard"; card: Card };
export const newGame = (players: string[]) => {
console.log("new game called with", JSON.stringify(players));
return {
deck: shuffle(newDeck()),
players: Object.fromEntries(players.map((humanId) => [humanId, []])),
} as GameState;
} as SimpleGameState;
};
export const getKnowledge = (
state: GameState,
state: SimpleGameState,
humanId: string
): vGameState => ({
): vSimpleGameState => ({
humanId,
deck: state.deck.map((_) => null),
players: Object.fromEntries(
@@ -62,7 +52,10 @@ export const getKnowledge = (
),
});
const getView = (state: vGameState, humanId: string): PlayerView => ({
export const getView = (
state: vSimpleGameState,
humanId: string
): SimplePlayerView => ({
humanId,
deckCount: state.deck.length,
myHand: state.players[humanId] as Hand,
@@ -74,21 +67,20 @@ const getView = (state: vGameState, humanId: string): PlayerView => ({
});
export const resolveAction = (
state: GameState,
state: SimpleGameState,
humanId: string,
action: Action
): GameState => {
// if (action.prevHash != hash(state)) {
// throw new Error(
// `action thinks it's applying to ${
// action.prevHash
// }, but we're checking it against ${hash(state)}`
// );
// }
action: SimpleAction
): SimpleGameState => {
console.log("attempting to resolve action", JSON.stringify(action));
if (!(humanId in state.players)) {
throw Error(
`${humanId} is not a player in this game; they cannot perform actions`
);
}
const playerHand = state.players[humanId];
if (action.type == "draw") {
const [drawn, ...rest] = state.deck;
console.log("drew card", JSON.stringify(drawn));
return {
deck: rest,
players: {
@@ -110,67 +102,3 @@ export const resolveAction = (
},
};
};
export const simpleApi = new Elysia({ prefix: "/simple" })
.use(human)
.post("/newGame", ({ humanKey }) => {
return prisma.instance.create({
data: {
gameState: newGame([humanKey]),
gameKey: "simple",
createdByKey: humanKey,
},
});
})
.group("/:instanceId", (app) =>
app
.get("/", ({ params: { instanceId }, humanKey }) =>
prisma.instance
.findUnique({
where: {
id: instanceId,
},
})
.then((game) =>
getView(
getKnowledge(
game!.gameState as GameState,
humanKey
),
humanKey
)
)
)
.post(
"/",
({ params: { instanceId }, body: { action }, humanKey }) =>
prisma.instance
.findUniqueOrThrow({
where: {
id: instanceId,
},
})
.then(async (game) => {
const newState = resolveAction(
game.gameState as GameState,
humanKey,
action
);
await prisma.instance.update({
data: { gameState: newState },
where: {
id: instanceId,
},
});
return getView(
getKnowledge(newState, humanKey),
humanKey
);
}),
{
body: t.Object({
action: t.Any(),
}),
}
)
);

View File

@@ -1,8 +1,13 @@
import Elysia from "elysia";
import db from "./db";
export const human = new Elysia({ name: "human" })
.derive(async ({ cookie: { token }, status }) => {
const humanKey = token.value;
const humanKey = await db.human
.findUnique({
where: { token: token.value },
})
.then((human) => human?.key);
return humanKey != null ? { humanKey } : status(401);
})
.as("scoped");

View File

@@ -9,7 +9,7 @@ const port = env.PORT || 5001;
const app = new Elysia()
.use(
cors({
origin: ["localhost:3000", "games.drm.dev"],
origin: ["http://localhost:3000", "https://games.drm.dev"],
})
)
.onRequest(({ request }) => {

View File

@@ -0,0 +1,29 @@
import { merge, Observable } from "kefir";
export const transform = <
T,
Mutations extends [Observable<any, any>, (prev: T, evt: any) => T][]
>(
initValue: T,
...mutations: Mutations
): Observable<T, 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),
])
);
};

View File

@@ -0,0 +1,104 @@
import { t } from "elysia";
import { combine, Property } from "kefir";
import Bus, { type Bus as TBus } from "kefir-bus";
import {
newGame,
resolveAction,
SimpleAction,
SimpleGameState,
} from "./games/simple";
import { transform } from "./kefir-extension";
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({ startGame: t.Literal(true) }),
t.Object({ action: t.Any() }),
]);
export type TWsIn = typeof WsIn.static;
type Attributed = { humanKey: string };
type TablePayload<GameState, GameAction> = {
inputs: {
presenceChanges: TBus<
Attributed & { presence: "joined" | "left" },
never
>;
gameProposals: TBus<{ proposeGame: string }, never>;
gameStarts: TBus<{ startGame: true }, never>;
gameActions: TBus<Attributed & GameAction, never>;
};
outputs: {
playersPresent: Property<string[], never>;
gameState: Property<GameState | null, never>;
};
};
const tables: {
[key: string]: TablePayload<unknown, unknown>;
} = {};
export const liveTable = <GameState, GameAction>(key: string) => {
if (!(key in tables)) {
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 SimpleGameState | null,
[
combine([gameStarts], [playersPresent], (evt, players) => ({
...evt,
players,
})),
(prev, evt: { players: string[] }) =>
prev == null
? (newGame(evt.players) as SimpleGameState)
: prev,
],
[
gameActions,
(prev, evt: Attributed & SimpleAction) =>
prev != null
? resolveAction(prev, evt.humanKey, evt)
: prev,
]
).toProperty();
tables[key] = {
inputs,
outputs: {
playersPresent,
gameState: gameState as Property<unknown, never>,
},
};
// 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<GameState, GameAction>;
};

197
pnpm-lock.yaml generated
View File

@@ -19,22 +19,28 @@ importers:
dependencies:
'@elysiajs/eden':
specifier: ^1.3.2
version: 1.3.2(elysia@1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))
version: 1.3.2(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))
'@solid-primitives/scheduled':
specifier: ^1.5.2
version: 1.5.2(solid-js@1.9.8)
version: 1.5.2(solid-js@1.9.9)
'@solidjs/router':
specifier: ^0.15.3
version: 0.15.3(solid-js@1.9.8)
version: 0.15.3(solid-js@1.9.9)
js-cookie:
specifier: ^3.0.5
version: 3.0.5
kefir:
specifier: ^3.8.8
version: 3.8.8
kefir-bus:
specifier: ^2.3.1
version: 2.3.1(kefir@3.8.8)
object-hash:
specifier: ^3.0.0
version: 3.0.0
solid-js:
specifier: ^1.9.5
version: 1.9.8
version: 1.9.9
devDependencies:
'@iconify-json/solar':
specifier: ^1.2.4
@@ -42,6 +48,9 @@ importers:
'@types/js-cookie':
specifier: ^3.0.6
version: 3.0.6
'@types/kefir':
specifier: ^3.8.11
version: 3.8.11
'@unocss/preset-icons':
specifier: ^66.4.2
version: 66.4.2
@@ -56,25 +65,40 @@ importers:
version: 4.5.14(@types/node@24.2.0)
vite-plugin-solid:
specifier: ^2.11.8
version: 2.11.8(solid-js@1.9.8)(vite@4.5.14(@types/node@24.2.0))
version: 2.11.8(solid-js@1.9.9)(vite@4.5.14(@types/node@24.2.0))
packages/server:
dependencies:
'@elysiajs/cors':
specifier: ^1.3.3
version: 1.3.3(elysia@1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))
version: 1.3.3(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))
'@elysiajs/static':
specifier: ^1.3.0
version: 1.3.0(elysia@1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))
version: 1.3.0(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))
'@games/shared':
specifier: workspace:*
version: link:../shared
'@prisma/client':
specifier: 6.13.0
version: 6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2)
dayjs:
specifier: ^1.11.13
version: 1.11.13
elysia:
specifier: ^1.3.8
version: 1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
version: 1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
elysia-ip:
specifier: ^1.0.10
version: 1.0.10(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))
elysia-rate-limit:
specifier: ^4.4.0
version: 4.4.0(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))
kefir:
specifier: ^3.8.8
version: 3.8.8
kefir-bus:
specifier: ^2.3.1
version: 2.3.1(kefir@3.8.8)
object-hash:
specifier: ^3.0.0
version: 3.0.0
@@ -82,12 +106,18 @@ importers:
'@types/bun':
specifier: latest
version: 1.2.20(@types/react@19.1.9)
'@types/kefir':
specifier: ^3.8.11
version: 3.8.11
concurrently:
specifier: ^9.2.0
version: 9.2.0
prisma:
specifier: 6.13.0
version: 6.13.0(typescript@5.9.2)
ts-xor:
specifier: ^1.3.0
version: 1.3.0
packages/shared:
dependencies:
@@ -97,6 +127,10 @@ importers:
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
@@ -188,6 +222,9 @@ packages:
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'}
'@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
'@elysiajs/cors@1.3.3':
resolution: {integrity: sha512-mYIU6PyMM6xIJuj7d27Vt0/wuzVKIEnFPjcvlkyd7t/m9xspAG37cwNjFxVOnyvY43oOd2I/oW2DB85utXpA2Q==}
peerDependencies:
@@ -434,6 +471,9 @@ packages:
'@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
'@types/kefir@3.8.11':
resolution: {integrity: sha512-5TRdFXQYsVUvqIH6nYjslHzBgn4hnptcutXnqAhfbKdWD/799c44hFhQGF3887E2t/Q4jSp3RvNFCaQ+b9w6vQ==}
'@types/node@24.2.0':
resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==}
@@ -658,6 +698,18 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
@@ -690,8 +742,18 @@ packages:
electron-to-chromium@1.5.198:
resolution: {integrity: sha512-G5COfnp3w+ydVu80yprgWSfmfQaYRh9DOxfhAxstLyetKaLyl55QrNjx8C38Pc/C+RaDmb1M0Lk8wPEMQ+bGgQ==}
elysia@1.3.8:
resolution: {integrity: sha512-kxYFhegJbUEf5otzmisEvGt3R7d/dPBNVERO2nHo0kFqKBHyj5slArc90mSRKLfi1vamMtPcz67rL6Zeg5F2yg==}
elysia-ip@1.0.10:
resolution: {integrity: sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ==}
peerDependencies:
elysia: '>= 1.0.9'
elysia-rate-limit@4.4.0:
resolution: {integrity: sha512-pyQdFEdjgf5ELx5CAEfOZ2IWhPaYv8WIQMrXimzHzslsJ9awDHoK6rcF9K7k/yAOh4qB1UhiasNeMMBGtxAwYQ==}
peerDependencies:
elysia: '>= 1.0.0'
elysia@1.3.11:
resolution: {integrity: sha512-iTBdfLe+CL8UvnqP+TB4NlUUqxhlKGEIxLMUZqlylUp4yGq2lTdFbxlItZuA7Z4/mlv5wC3GfjTd587Iwo552Q==}
peerDependencies:
exact-mirror: '>= 0.0.9'
file-type: '>= 20.0.0'
@@ -846,6 +908,14 @@ packages:
engines: {node: '>=6'}
hasBin: true
kefir-bus@2.3.1:
resolution: {integrity: sha512-wLCQfEw8PddSNeyjDCH2WNgNg3Rb/c+OaG5WEPfEwod+LQfGX4isHcHRWsYNLmdFEw3/KyA+9qDSy+VC4NsifA==}
peerDependencies:
kefir: ^3.5.1
kefir@3.8.8:
resolution: {integrity: sha512-xWga7QCZsR2Wjy2vNL3Kq/irT+IwxwItEWycRRlT5yhqHZK2fmEhziP+LzcJBWSTAMranGKtGTQ6lFpyJS3+jA==}
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
@@ -879,6 +949,9 @@ packages:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1034,8 +1107,8 @@ packages:
resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==}
engines: {node: '>=18'}
solid-js@1.9.8:
resolution: {integrity: sha512-zF9Whfqk+s8wWuyDKnE7ekl+dJburjdZq54O6X1k4XChA57uZ5FOauYAa0s4I44XkBOM3CZmPrZC0DGjH9fKjQ==}
solid-js@1.9.9:
resolution: {integrity: sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==}
solid-refresh@0.6.3:
resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==}
@@ -1089,8 +1162,8 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
token-types@6.0.4:
resolution: {integrity: sha512-MD9MjpVNhVyH4fyd5rKphjvt/1qj+PtQUz65aFqAZA6XniWAuSFRjLk3e2VALEFlh9OwBpXUN7rfeqSnT/Fmkw==}
token-types@6.1.1:
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
engines: {node: '>=14.16'}
totalist@3.0.1:
@@ -1101,6 +1174,9 @@ packages:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
ts-xor@1.3.0:
resolution: {integrity: sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -1116,8 +1192,8 @@ packages:
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
uint8array-extras@1.4.0:
resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==}
uint8array-extras@1.4.1:
resolution: {integrity: sha512-+NWHrac9dvilNgme+gP4YrBSumsaMZP0fNBtXXFIf33RLLKEcBUKaQZ7ULUbS0sBfcjxIZ4V96OTRkCbM7hxpw==}
engines: {node: '>=18'}
unconfig@7.3.2:
@@ -1228,6 +1304,8 @@ packages:
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.12
@@ -1351,17 +1429,19 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@elysiajs/cors@1.3.3(elysia@1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))':
dependencies:
elysia: 1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
'@borewit/text-codec@0.1.1': {}
'@elysiajs/eden@1.3.2(elysia@1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))':
'@elysiajs/cors@1.3.3(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))':
dependencies:
elysia: 1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
elysia: 1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
'@elysiajs/static@1.3.0(elysia@1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))':
'@elysiajs/eden@1.3.2(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))':
dependencies:
elysia: 1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
elysia: 1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
'@elysiajs/static@1.3.0(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2))':
dependencies:
elysia: 1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
node-cache: 5.1.2
'@esbuild/android-arm64@0.18.20':
@@ -1507,13 +1587,13 @@ snapshots:
'@sinclair/typebox@0.34.38':
optional: true
'@solid-primitives/scheduled@1.5.2(solid-js@1.9.8)':
'@solid-primitives/scheduled@1.5.2(solid-js@1.9.9)':
dependencies:
solid-js: 1.9.8
solid-js: 1.9.9
'@solidjs/router@0.15.3(solid-js@1.9.8)':
'@solidjs/router@0.15.3(solid-js@1.9.9)':
dependencies:
solid-js: 1.9.8
solid-js: 1.9.9
'@standard-schema/spec@1.0.0': {}
@@ -1521,7 +1601,7 @@ snapshots:
dependencies:
debug: 4.4.1
fflate: 0.8.2
token-types: 6.0.4
token-types: 6.1.1
transitivePeerDependencies:
- supports-color
@@ -1556,6 +1636,10 @@ snapshots:
'@types/js-cookie@3.0.6': {}
'@types/kefir@3.8.11':
dependencies:
'@types/node': 24.2.0
'@types/node@24.2.0':
dependencies:
undici-types: 7.10.0
@@ -1741,12 +1825,12 @@ snapshots:
parse5: 7.3.0
validate-html-nesting: 1.2.3
babel-preset-solid@1.9.8(@babel/core@7.28.0)(solid-js@1.9.8):
babel-preset-solid@1.9.8(@babel/core@7.28.0)(solid-js@1.9.9):
dependencies:
'@babel/core': 7.28.0
babel-plugin-jsx-dom-expressions: 0.40.0(@babel/core@7.28.0)
optionalDependencies:
solid-js: 1.9.8
solid-js: 1.9.9
binary-extensions@2.3.0: {}
@@ -1853,6 +1937,12 @@ snapshots:
csstype@3.1.3: {}
dayjs@1.11.13: {}
debug@4.3.4:
dependencies:
ms: 2.1.2
debug@4.4.1:
dependencies:
ms: 2.1.3
@@ -1874,7 +1964,19 @@ snapshots:
electron-to-chromium@1.5.198: {}
elysia@1.3.8(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2):
elysia-ip@1.0.10(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)):
dependencies:
elysia: 1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
elysia-rate-limit@4.4.0(elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)):
dependencies:
'@alloc/quick-lru': 5.2.0
debug: 4.3.4
elysia: 1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2)
transitivePeerDependencies:
- supports-color
elysia@1.3.11(exact-mirror@0.1.5(@sinclair/typebox@0.34.38))(file-type@21.0.0)(typescript@5.9.2):
dependencies:
cookie: 1.0.2
exact-mirror: 0.1.5(@sinclair/typebox@0.34.38)
@@ -1938,8 +2040,8 @@ snapshots:
dependencies:
'@tokenizer/inflate': 0.2.7
strtok3: 10.3.4
token-types: 6.0.4
uint8array-extras: 1.4.0
token-types: 6.1.1
uint8array-extras: 1.4.1
transitivePeerDependencies:
- supports-color
@@ -2013,6 +2115,12 @@ snapshots:
json5@2.2.3: {}
kefir-bus@2.3.1(kefir@3.8.8):
dependencies:
kefir: 3.8.8
kefir@3.8.8: {}
kolorist@1.8.0: {}
local-pkg@1.1.1:
@@ -2048,6 +2156,8 @@ snapshots:
mrmime@2.0.1: {}
ms@2.1.2: {}
ms@2.1.3: {}
nanoid@3.3.11: {}
@@ -2195,18 +2305,18 @@ snapshots:
mrmime: 2.0.1
totalist: 3.0.1
solid-js@1.9.8:
solid-js@1.9.9:
dependencies:
csstype: 3.1.3
seroval: 1.3.2
seroval-plugins: 1.3.2(seroval@1.3.2)
solid-refresh@0.6.3(solid-js@1.9.8):
solid-refresh@0.6.3(solid-js@1.9.9):
dependencies:
'@babel/generator': 7.28.0
'@babel/helper-module-imports': 7.27.1
'@babel/types': 7.28.2
solid-js: 1.9.8
solid-js: 1.9.9
transitivePeerDependencies:
- supports-color
@@ -2259,8 +2369,9 @@ snapshots:
dependencies:
is-number: 7.0.0
token-types@6.0.4:
token-types@6.1.1:
dependencies:
'@borewit/text-codec': 0.1.1
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
@@ -2268,6 +2379,8 @@ snapshots:
tree-kill@1.2.2: {}
ts-xor@1.3.0: {}
tslib@2.8.1: {}
type-fest@4.41.0: {}
@@ -2276,7 +2389,7 @@ snapshots:
ufo@1.6.1: {}
uint8array-extras@1.4.0: {}
uint8array-extras@1.4.1: {}
unconfig@7.3.2:
dependencies:
@@ -2334,14 +2447,14 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
vite-plugin-solid@2.11.8(solid-js@1.9.8)(vite@4.5.14(@types/node@24.2.0)):
vite-plugin-solid@2.11.8(solid-js@1.9.9)(vite@4.5.14(@types/node@24.2.0)):
dependencies:
'@babel/core': 7.28.0
'@types/babel__core': 7.20.5
babel-preset-solid: 1.9.8(@babel/core@7.28.0)(solid-js@1.9.8)
babel-preset-solid: 1.9.8(@babel/core@7.28.0)(solid-js@1.9.9)
merge-anything: 5.1.7
solid-js: 1.9.8
solid-refresh: 0.6.3(solid-js@1.9.8)
solid-js: 1.9.9
solid-refresh: 0.6.3(solid-js@1.9.9)
vite: 4.5.14(@types/node@24.2.0)
vitefu: 1.1.1(vite@4.5.14(@types/node@24.2.0))
transitivePeerDependencies: