cooking with kefir
This commit is contained in:
@@ -52,12 +52,8 @@ const App = () => {
|
|||||||
>
|
>
|
||||||
<Route path="/" component={lazy(() => import("./routes/index"))} />
|
<Route path="/" component={lazy(() => import("./routes/index"))} />
|
||||||
<Route
|
<Route
|
||||||
path="/:game"
|
path="/:tableKey"
|
||||||
component={lazy(() => import("./routes/[game]/index"))}
|
component={lazy(() => import("./routes/[table]"))}
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/:game/:instance"
|
|
||||||
component={lazy(() => import("./routes/[game]/[instance]"))}
|
|
||||||
/>
|
/>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Resource,
|
Resource,
|
||||||
ResourceReturn,
|
ResourceReturn,
|
||||||
Show,
|
Show,
|
||||||
|
untrack,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import {
|
import {
|
||||||
GameState,
|
GameState,
|
||||||
@@ -28,35 +29,38 @@ export const GameContext = createContext<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const [playerProfiles, setPlayerProfiles] = createStore<
|
const [playerProfiles, setPlayerProfiles] = createStore<
|
||||||
Record<
|
Record<string, Resource<ApiType<typeof api.profile.get>>>
|
||||||
string,
|
|
||||||
ReturnType<typeof createResource<ApiType<typeof api.profile.get>>>[0]
|
|
||||||
>
|
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
export default (props: { instanceId: string }) => {
|
export default (props: { tableKey: string }) => {
|
||||||
const [view, setView] = createSignal<PlayerView>();
|
const [view, setView] = createSignal<PlayerView>();
|
||||||
const [players, setPlayers] = createSignal<string[]>([]);
|
const [players, setPlayers] = createSignal<string[]>([]);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
players().forEach((player) => {
|
players().forEach((player) => {
|
||||||
if (!playerProfiles[player]) {
|
if (!untrack(() => playerProfiles[player])) {
|
||||||
const [playerProfile] = createResource(() =>
|
const [playerProfile] = createResource(() =>
|
||||||
api.profile
|
api.profile
|
||||||
.get({ query: { otherHumanKey: player } })
|
.get({ query: { otherHumanKey: player } })
|
||||||
.then((r) => r.data)
|
.then((r) => r.data)
|
||||||
);
|
);
|
||||||
setPlayerProfiles(player, playerProfile);
|
setPlayerProfiles((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[player]: playerProfile,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const ws = api.simple(props).subscribe();
|
const ws = api(props).subscribe;
|
||||||
onCleanup(() => ws.close());
|
onCleanup(() => ws.close());
|
||||||
ws.on("message", (evt) => {
|
ws.on("message", (evt) => {
|
||||||
if (evt.data.players) {
|
if (evt.data.players) {
|
||||||
setPlayers(evt.data.players);
|
setPlayers(evt.data.players);
|
||||||
}
|
}
|
||||||
|
if (evt.data.view) {
|
||||||
|
setView(evt.data.view);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitAction = (action: Action) => api.simple(props).post({ action });
|
const submitAction = (action: Action) => api.simple(props).post({ action });
|
||||||
@@ -66,7 +70,11 @@ export default (props: { instanceId: string }) => {
|
|||||||
<div class="fixed tc mt-20 flex flex-col items-center">
|
<div class="fixed tc mt-20 flex flex-col items-center">
|
||||||
<button class="button p-1 m-10">Start Game!</button>
|
<button class="button p-1 m-10">Start Game!</button>
|
||||||
<For each={players()}>
|
<For each={players()}>
|
||||||
{(player) => <p>{playerProfiles[player]?.()?.name}</p>}
|
{(player) => (
|
||||||
|
<p style={{ "font-size": "2em" }}>
|
||||||
|
{playerProfiles[player]?.()?.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
Accessor,
|
||||||
|
createContext,
|
||||||
|
createEffect,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
onCleanup,
|
||||||
|
Resource,
|
||||||
|
ResourceReturn,
|
||||||
|
Show,
|
||||||
|
untrack,
|
||||||
|
} from "solid-js";
|
||||||
|
import {
|
||||||
|
GameState,
|
||||||
|
Action,
|
||||||
|
vGameState,
|
||||||
|
PlayerView,
|
||||||
|
} from "../../../server/src/games/simple";
|
||||||
|
import api from "../api";
|
||||||
|
import Hand from "./Hand";
|
||||||
|
import Pile from "./Pile";
|
||||||
|
import { ApiType } from "../fn";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
import Game from "./Game";
|
||||||
|
|
||||||
|
const [playerProfiles, setPlayerProfiles] = createStore<
|
||||||
|
Record<string, Resource<ApiType<typeof api.profile.get>>>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
export const TableContext = createContext<{
|
||||||
|
players: Accessor<string[]>;
|
||||||
|
view: Accessor<PlayerView | undefined>;
|
||||||
|
// submitAction: (action: Action) => Promise<any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
export default (props: { tableKey: string }) => {
|
||||||
|
const [players, setPlayers] = createSignal<string[]>([]);
|
||||||
|
const [view, setView] = createSignal<PlayerView>();
|
||||||
|
|
||||||
|
const ws = api(props).subscribe();
|
||||||
|
onCleanup(() => ws.close());
|
||||||
|
|
||||||
|
ws.on("message", (evt) => {
|
||||||
|
if (evt.data.players) {
|
||||||
|
setPlayers(evt.data.players);
|
||||||
|
}
|
||||||
|
if (evt.data.view) {
|
||||||
|
setView(evt.data.view);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
players().forEach((player) => {
|
||||||
|
if (!untrack(() => playerProfiles[player])) {
|
||||||
|
const [playerProfile] = createResource(() =>
|
||||||
|
api.profile
|
||||||
|
.get({ query: { otherHumanKey: player } })
|
||||||
|
.then((r) => r.data)
|
||||||
|
);
|
||||||
|
setPlayerProfiles((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[player]: playerProfile,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const Lobby = () => {
|
||||||
|
return (
|
||||||
|
<div class="fixed tc mt-20 flex flex-col items-center">
|
||||||
|
<button class="button p-1 m-10">Start Game!</button>
|
||||||
|
<For each={players()}>
|
||||||
|
{(player) => (
|
||||||
|
<p style={{ "font-size": "2em" }}>
|
||||||
|
{playerProfiles[player]?.()?.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TableContext.Provider value={{ view, players }}>
|
||||||
|
<Show when={view() != null} fallback={<Lobby />}>
|
||||||
|
<Game tableKey={props.tableKey} />
|
||||||
|
</Show>
|
||||||
|
</TableContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
16
packages/client/src/routes/[table].tsx
Normal file
16
packages/client/src/routes/[table].tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/kefir": "^3.8.11",
|
"@types/kefir": "^3.8.11",
|
||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.0",
|
||||||
"prisma": "6.13.0"
|
"prisma": "6.13.0",
|
||||||
|
"ts-xor": "^1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import { simpleApi } from "./games/simple";
|
import { Action, GameState, getKnowledge, getView } from "./games/simple";
|
||||||
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 } from "./table";
|
||||||
|
|
||||||
const api = new Elysia({ prefix: "/api" })
|
const api = new Elysia({ prefix: "/api" })
|
||||||
.post("/whoami", async ({ cookie: { token } }) => {
|
.post("/whoami", async ({ cookie: { token } }) => {
|
||||||
@@ -48,26 +49,46 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.get("/games", () => [{ key: "simple", name: "simple" }])
|
.get("/games", () => [{ key: "simple", name: "simple" }])
|
||||||
|
.ws("/:tableKey", {
|
||||||
|
response: t.Object({
|
||||||
|
players: t.Optional(t.Array(t.String())),
|
||||||
|
view: t.Optional(t.Any()),
|
||||||
|
}),
|
||||||
|
|
||||||
.get("/instances", ({ query: { game }, humanKey }) =>
|
async open({
|
||||||
db.instance.findMany({
|
data: {
|
||||||
where: {
|
params: { tableKey },
|
||||||
game: {
|
humanKey,
|
||||||
name: game,
|
|
||||||
},
|
|
||||||
players: {
|
|
||||||
some: {
|
|
||||||
key: humanKey,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
},
|
||||||
|
send,
|
||||||
|
}) {
|
||||||
|
const table = liveTable<GameState, Action>(tableKey);
|
||||||
|
|
||||||
|
table.outputs.playersPresent.onValue((players) =>
|
||||||
|
send({ players })
|
||||||
|
);
|
||||||
|
table.outputs.gameState.onValue((gameState) =>
|
||||||
|
send({
|
||||||
|
view:
|
||||||
|
gameState &&
|
||||||
|
getView(getKnowledge(gameState, humanKey), humanKey),
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
.use(simpleApi);
|
table.input.emit({ humanKey, presence: "joined" });
|
||||||
|
},
|
||||||
|
async close({
|
||||||
|
data: {
|
||||||
|
params: { tableKey },
|
||||||
|
humanKey,
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
liveTable(tableKey).input.emit({
|
||||||
|
humanKey,
|
||||||
|
presence: "left",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
export type Api = typeof api;
|
export type Api = typeof api;
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import { Elysia, t } from "elysia";
|
|||||||
import db from "../db";
|
import db from "../db";
|
||||||
import { human } from "../human";
|
import { human } from "../human";
|
||||||
import { ElysiaWS } from "elysia/dist/ws";
|
import { ElysiaWS } from "elysia/dist/ws";
|
||||||
import K, { Stream } from "kefir";
|
import K, { Property, Stream } from "kefir";
|
||||||
import Bus, { type Bus as TBus } from "kefir-bus";
|
import Bus, { type Bus as TBus } from "kefir-bus";
|
||||||
|
import { Prisma, Instance } from "@prisma/client";
|
||||||
|
import type { XOR } from "ts-xor";
|
||||||
|
import { liveTable } from "../table";
|
||||||
|
|
||||||
// omniscient game state
|
// omniscient game state
|
||||||
export type GameState = {
|
export type GameState = {
|
||||||
@@ -58,7 +61,7 @@ export const getKnowledge = (
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getView = (state: vGameState, humanId: string): PlayerView => ({
|
export const getView = (state: vGameState, humanId: string): PlayerView => ({
|
||||||
humanId,
|
humanId,
|
||||||
deckCount: state.deck.length,
|
deckCount: state.deck.length,
|
||||||
myHand: state.players[humanId] as Hand,
|
myHand: state.players[humanId] as Hand,
|
||||||
@@ -74,14 +77,6 @@ export const resolveAction = (
|
|||||||
humanId: string,
|
humanId: string,
|
||||||
action: Action
|
action: Action
|
||||||
): GameState => {
|
): 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)}`
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
const playerHand = state.players[humanId];
|
const playerHand = state.players[humanId];
|
||||||
if (action.type == "draw") {
|
if (action.type == "draw") {
|
||||||
const [drawn, ...rest] = state.deck;
|
const [drawn, ...rest] = state.deck;
|
||||||
@@ -106,147 +101,3 @@ export const resolveAction = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const instanceEvents: {
|
|
||||||
[instanceId: string]: TBus<
|
|
||||||
{ view?: PlayerView; players?: string[] },
|
|
||||||
never
|
|
||||||
>;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
export const simpleApi = new Elysia({ prefix: "/simple" })
|
|
||||||
.use(human)
|
|
||||||
.post("/newGame", ({ humanKey }) => {
|
|
||||||
return db.instance.create({
|
|
||||||
data: {
|
|
||||||
gameKey: "simple",
|
|
||||||
createdByKey: humanKey,
|
|
||||||
players: {
|
|
||||||
connect: [{ key: humanKey }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
.group("/:instanceId", (app) =>
|
|
||||||
app
|
|
||||||
.ws("/", {
|
|
||||||
async open(ws) {
|
|
||||||
console.log("Got ws connection");
|
|
||||||
ws.send("Hello!");
|
|
||||||
|
|
||||||
// send initial state
|
|
||||||
const instanceId = ws.data.params.instanceId;
|
|
||||||
const humanKey = ws.data.humanKey;
|
|
||||||
|
|
||||||
const instance = await db.instance.update({
|
|
||||||
data: {
|
|
||||||
players: {
|
|
||||||
connect: { key: humanKey },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: instanceId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
createdByKey: true,
|
|
||||||
gameState: true,
|
|
||||||
players: {
|
|
||||||
select: {
|
|
||||||
key: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (instance == null) {
|
|
||||||
ws.close(1011, "no such instance");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// register this socket as a listener for events of this instance
|
|
||||||
if (!instanceEvents[instanceId]) {
|
|
||||||
instanceEvents[instanceId] = Bus();
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
ws.data.cb = instanceEvents[instanceId].onValue((evt) =>
|
|
||||||
ws.send(evt)
|
|
||||||
);
|
|
||||||
ws.send({ creator: instance.createdByKey });
|
|
||||||
if (instance.gameState != null) {
|
|
||||||
ws.send(
|
|
||||||
getView(
|
|
||||||
getKnowledge(
|
|
||||||
instance.gameState as GameState,
|
|
||||||
humanKey
|
|
||||||
),
|
|
||||||
humanKey
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
instanceEvents[instanceId]?.emit({
|
|
||||||
players: instance.players.map((p) => p.key),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
close(ws) {
|
|
||||||
console.log("Got ws close");
|
|
||||||
const instanceId = ws.data.params.instanceId;
|
|
||||||
// @ts-ignore
|
|
||||||
instanceEvents[instanceId]?.offValue(ws.data.cb);
|
|
||||||
db.instance
|
|
||||||
.update({
|
|
||||||
where: {
|
|
||||||
id: instanceId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
players: {
|
|
||||||
disconnect: { key: ws.data.humanKey },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
players: {
|
|
||||||
select: {
|
|
||||||
key: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((instance) => {
|
|
||||||
instanceEvents[instanceId]?.emit({
|
|
||||||
players: instance.players.map((p) => p.key),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.post(
|
|
||||||
"/",
|
|
||||||
({ params: { instanceId }, body: { action }, humanKey }) =>
|
|
||||||
db.instance
|
|
||||||
.findUniqueOrThrow({
|
|
||||||
where: {
|
|
||||||
id: instanceId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (game) => {
|
|
||||||
const newState = resolveAction(
|
|
||||||
game.gameState as GameState,
|
|
||||||
humanKey,
|
|
||||||
action
|
|
||||||
);
|
|
||||||
await db.instance.update({
|
|
||||||
data: { gameState: newState },
|
|
||||||
where: {
|
|
||||||
id: instanceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return getView(
|
|
||||||
getKnowledge(newState, humanKey),
|
|
||||||
humanKey
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
body: t.Object({
|
|
||||||
action: t.Any(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|||||||
66
packages/server/src/table.ts
Normal file
66
packages/server/src/table.ts
Normal 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>;
|
||||||
|
};
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -106,6 +106,9 @@ importers:
|
|||||||
prisma:
|
prisma:
|
||||||
specifier: 6.13.0
|
specifier: 6.13.0
|
||||||
version: 6.13.0(typescript@5.9.2)
|
version: 6.13.0(typescript@5.9.2)
|
||||||
|
ts-xor:
|
||||||
|
specifier: ^1.3.0
|
||||||
|
version: 1.3.0
|
||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1162,6 +1165,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ts-xor@1.3.0:
|
||||||
|
resolution: {integrity: sha512-RLXVjliCzc1gfKQFLRpfeD0rrWmjnSTgj7+RFhoq3KRkUYa8LE/TIidYOzM5h+IdFBDSjjSgk9Lto9sdMfDFEA==}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@@ -2364,6 +2370,8 @@ snapshots:
|
|||||||
|
|
||||||
tree-kill@1.2.2: {}
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
|
ts-xor@1.3.0: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
type-fest@4.41.0: {}
|
type-fest@4.41.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user