Skip to content

Commit

Permalink
feat(v2): room state persisted in Redis & TRPC endpoints to update state
Browse files Browse the repository at this point in the history
This way the clients don't confuse each other with individual problems or race conditions.

Closes #65
  • Loading branch information
jkrumm committed Jan 19, 2024
1 parent 8b54207 commit 91b72b4
Show file tree
Hide file tree
Showing 20 changed files with 889 additions and 506 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ yarn-error.log*
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
.envrc

# vercel
.vercel
Expand Down
135 changes: 56 additions & 79 deletions src/components/room/interactions.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,47 @@
import { Button, Switch } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { type PresenceUpdate, useWsStore } from "fpp/store/ws.store";
import { useEffect, useRef } from "react";
import { useRef } from "react";
import { useRouter } from "next/router";
import { useLocalstorageStore } from "fpp/store/local-storage.store";
import { fibonacciSequence } from "fpp/constants/fibonacci.constant";
import { type Logger } from "next-axiom";
import { logMsg, roomEvent } from "fpp/constants/logging.constant";
import { RouteType } from "fpp/server/db/schema";
import { useRoomStateStore } from "fpp/store/room-state.store";
import { useLocalstorageStore } from "fpp/store/local-storage.store";
import { api } from "fpp/utils/api";

export const Interactions = ({
roomId,
roomReadable,
username,
userId,
logger,
}: {
roomId: number;
roomReadable: string;
username: string;
userId: string;
logger: Logger;
}) => {
const router = useRouter();

const clientId = useWsStore((store) => store.clientId);
const channel = useWsStore((store) => store.channel);

const userId = useLocalstorageStore((state) => state.userId);
const voting = useLocalstorageStore((store) => store.voting);
const setVoting = useLocalstorageStore((store) => store.setVoting);
const setSpectator = useLocalstorageStore((store) => store.setSpectator);
const setRoomId = useLocalstorageStore((store) => store.setRoomId);
const setRoomReadable = useLocalstorageStore(
(store) => store.setRoomReadable,
);

const votes = useWsStore((store) => store.votes);
const spectators = useWsStore((store) => store.spectators);
const presences = useWsStore((store) => store.presences);
const flipped = useWsStore((store) => store.flipped);
const autoShow = useWsStore((store) => store.autoShow);
// User state
const estimation = useRoomStateStore((store) => store.estimation);
const estimateMutation = api.roomState.estimate.useMutation();
const isSpectator = useRoomStateStore((store) => store.isSpectator);
const setIsSpectator = useLocalstorageStore((store) => store.setIsSpectator);
const spectatorMutation = api.roomState.spectator.useMutation();
const leaveMutation = api.roomState.leave.useMutation();

useEffect(() => {
if (!channel || !clientId) return;
const presenceUpdate: PresenceUpdate = {
username,
voting,
spectator: spectators.includes(clientId),
presencesLength: presences.length,
};
logger.debug("SEND OWN PRESENCE ON INIT", presenceUpdate);
channel.presence.update(presenceUpdate);
}, [channel]);
// Room state
const isFlipped = useRoomStateStore((store) => store.isFlipped);
const resetMutation = api.roomState.reset.useMutation();
const isAutoFlip = useRoomStateStore((store) => store.isAutoFlip);
const autoFlipMutation = api.roomState.autoFlip.useMutation();
const resetRoomState = useRoomStateStore((store) => store.reset);

const roomRef = useRef(null);

Expand All @@ -67,16 +58,15 @@ export const Interactions = ({
<h2
className="uppercase"
ref={roomRef}
onKeyPress={() => ({})}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={async () => {
onClick={() => {
if (!window.location) {
return;
}
if ("clipboard" in navigator) {
await navigator.clipboard.writeText(
window.location.toString(),
);
navigator.clipboard
.writeText(window.location.toString())
.then(() => ({}))
.catch(() => ({}));
} else {
document.execCommand(
"copy",
Expand All @@ -102,15 +92,16 @@ export const Interactions = ({
</Button>
<div>
<Button
variant={flipped ? "default" : "filled"}
disabled={flipped}
variant={isFlipped ? "filled" : "default"}
className={"mr-5"}
onClick={() => {
if (!channel) return;
channel.publish("reset", {});
resetMutation.mutate({
roomId,
userId,
});
}}
>
New Round
{isFlipped ? "New Round" : "Reset"}
</Button>
<Button
variant={"default"}
Expand All @@ -123,7 +114,11 @@ export const Interactions = ({
});
setRoomId(null);
setRoomReadable(null);
setVoting(null);
leaveMutation.mutate({
roomId,
userId,
});
resetRoomState();
router
.push(`/`)
.then(() => ({}))
Expand All @@ -138,31 +133,17 @@ export const Interactions = ({
<Button.Group className="w-full">
{fibonacciSequence.map((number) => (
<Button
disabled={
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(clientId && spectators.includes(clientId)) || !flipped
}
variant={
votes.some(
(item) =>
item.clientId === clientId && item.number === number,
)
? "filled"
: "default"
}
disabled={isSpectator || isFlipped}
variant={estimation === number ? "filled" : "default"}
size={"lg"}
fullWidth
key={number}
onClick={() => {
if (!channel || !clientId) return;
setVoting(number);
const presenceUpdate: PresenceUpdate = {
username,
voting: number,
spectator: spectators.includes(clientId),
presencesLength: presences.length,
};
channel.presence.update(presenceUpdate);
estimateMutation.mutate({
roomId,
userId,
estimation: estimation === number ? null : number,
});
}}
>
{number}
Expand All @@ -174,31 +155,27 @@ export const Interactions = ({
<div className="switch-bar">
<Switch
className="mb-2 cursor-pointer"
disabled={!flipped}
disabled={isFlipped}
label="Spectator"
checked={clientId ? spectators.includes(clientId) : false}
checked={isSpectator}
onChange={(event) => {
if (!channel) return;
setSpectator(event.currentTarget.checked);
setVoting(null);
const presenceUpdate: Partial<PresenceUpdate> = {
username,
voting: null,
spectator: event.currentTarget.checked,
presencesLength: presences.length,
};
channel.presence.update(presenceUpdate);
setIsSpectator(event.currentTarget.checked);
spectatorMutation.mutate({
roomId,
userId,
isSpectator: event.currentTarget.checked,
});
}}
/>
<Switch
label="Auto Show"
disabled={true}
label="Auto Flip"
className="cursor-pointer"
checked={autoShow}
checked={isAutoFlip}
onChange={(event) => {
if (!channel) return;
channel.publish("auto-show", {
autoShow: event.currentTarget.checked,
autoFlipMutation.mutate({
roomId,
userId,
isAutoFlip: event.currentTarget.checked,
});
}}
/>
Expand Down
53 changes: 26 additions & 27 deletions src/components/room/room-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,38 @@
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import { UsernameModel } from "fpp/components/room/username-model";
import { Table } from "fpp/components/room/table";
import { WebsocketReceiver } from "fpp/components/room/websocket-receiver";
import { Interactions } from "fpp/components/room/interactions";
import { useLocalstorageStore } from "fpp/store/local-storage.store";
import { sendTrackPageView } from "fpp/hooks/use-tracking.hook";
import { useLogger } from "next-axiom";
import { logMsg, roomEvent } from "fpp/constants/logging.constant";
import { type ClientLog } from "fpp/constants/error.constant";
import { api } from "fpp/utils/api";
import { RouteType } from "fpp/server/db/schema";
import { configureAbly } from "@ably-labs/react-hooks";
import { env } from "fpp/env.mjs";
import { Loader } from "@mantine/core";
import * as Ably from "ably";
import { AblyProvider } from "ably/react";
import { Room } from "fpp/components/room/room";
import { useRoomStateStore } from "fpp/store/room-state.store";

const RoomWrapper = () => {
const router = useRouter();
const logger = useLogger().with({ route: RouteType.ROOM });

const username = useLocalstorageStore((store) => store.username);
const userId = useLocalstorageStore((state) => state.userId);
const setUserId = useLocalstorageStore((state) => state.setUserId);
const setUserIdLocalStorage = useLocalstorageStore(
(state) => state.setUserId,
);

const setUserIdRoomState = useRoomStateStore((state) => state.setUserId);
if (userId) {
configureAbly({
setUserIdRoomState(userId);
}

let ablyClient;
if (userId) {
ablyClient = new Ably.Realtime.Promise({
authUrl: `${env.NEXT_PUBLIC_API_ROOT}api/ably-token`,
clientId: userId,
});
Expand All @@ -42,9 +50,6 @@ const RoomWrapper = () => {
);
const setRecentRoom = useLocalstorageStore((store) => store.setRecentRoom);

const setVoting = useLocalstorageStore((store) => store.setVoting);
const setSpectator = useLocalstorageStore((store) => store.setSpectator);

const [firstLoad, setFirstLoad] = React.useState(true);
const [modelOpen, setModelOpen] = React.useState(false);

Expand Down Expand Up @@ -105,17 +110,16 @@ const RoomWrapper = () => {
joinRoomMutation.mutate(
{ roomReadable: queryRoom },
{
onSuccess: (data) => {
setRoomId(data.roomId);
setRoomReadable(data.roomReadable);
setRecentRoom(data.roomReadable);
setVoting(null);
setSpectator(false);
onSuccess: ({ roomId, roomReadable }) => {
setRoomId(roomId);
setRoomReadable(roomReadable);
setRecentRoom(roomReadable);
sendTrackPageView({
userId,
route: RouteType.ROOM,
roomId: data.roomId,
setUserId,
roomId,
setUserIdLocalStorage,
setUserIdRoomState,
logger,
});
},
Expand Down Expand Up @@ -143,22 +147,17 @@ const RoomWrapper = () => {
/>
);
}
if (roomId && roomReadable) {
if (roomId && userId && ablyClient && roomReadable) {
return (
<>
<WebsocketReceiver
username={username}
roomId={roomId}
logger={logger}
/>
<Table roomId={roomId} username={username} logger={logger} />
<Interactions
<AblyProvider client={ablyClient}>
<Room
roomId={roomId}
roomReadable={roomReadable}
userId={userId}
username={username}
logger={logger}
/>
</>
</AblyProvider>
);
}
return <Loader variant="bars" />;
Expand Down
47 changes: 47 additions & 0 deletions src/components/room/room.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type Logger } from "next-axiom";
import { Table } from "fpp/components/room/table";
import { Interactions } from "fpp/components/room/interactions";
import React from "react";
import { useHeartbeat } from "fpp/hooks/use-heartbeat.hook";
import { useRoomState } from "fpp/hooks/use-room-state.hook";

export const Room = ({
roomId,
roomReadable,
userId,
username,
logger,
}: {
roomId: number;
roomReadable: string;
userId: string;
username: string;
logger: Logger;
}) => {
// Listen to room state updates
useRoomState({
roomId,
userId,
username,
logger,
});

// Send heartbeats every 10 seconds
useHeartbeat({
roomId,
userId,
logger,
});

return (
<>
<Table roomId={roomId} userId={userId} logger={logger} />
<Interactions
roomId={roomId}
roomReadable={roomReadable}
userId={userId}
logger={logger}
/>
</>
);
};
Loading

0 comments on commit 91b72b4

Please sign in to comment.