From 7d6835380610b978ddb8eebf7f1d7257392cea25 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Wed, 10 Dec 2025 14:34:02 +0100 Subject: [PATCH 1/4] block entry for user from backend --- deep-sea-stories/packages/backend/src/game/room.ts | 8 +++++++- deep-sea-stories/packages/common/src/constants.ts | 2 ++ deep-sea-stories/packages/common/src/index.ts | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/deep-sea-stories/packages/backend/src/game/room.ts b/deep-sea-stories/packages/backend/src/game/room.ts index 46fd19e..b7bb93a 100644 --- a/deep-sea-stories/packages/backend/src/game/room.ts +++ b/deep-sea-stories/packages/backend/src/game/room.ts @@ -1,4 +1,7 @@ -import { GAME_TIME_LIMIT_SECONDS } from '@deep-sea-stories/common'; +import { + GAME_TIME_LIMIT_SECONDS, + ROOM_PLAYERS_LIMIT, +} from '@deep-sea-stories/common'; import { type FishjamClient, type Peer, @@ -61,6 +64,9 @@ export class GameRoom { } async addPlayer(name: string): Promise<{ peer: Peer; peerToken: string }> { + if (this.players.size >= ROOM_PLAYERS_LIMIT) { + throw new Error(`Cannot join: limit of ${ROOM_PLAYERS_LIMIT} reached.`); + } const { peer, peerToken } = await this.fishjamClient.createPeer( this.roomId, ); diff --git a/deep-sea-stories/packages/common/src/constants.ts b/deep-sea-stories/packages/common/src/constants.ts index 4942cc9..fbfb6bc 100644 --- a/deep-sea-stories/packages/common/src/constants.ts +++ b/deep-sea-stories/packages/common/src/constants.ts @@ -1,3 +1,5 @@ export const GAME_TIME_LIMIT_MINUTES = 30; export const GAME_TIME_LIMIT_SECONDS = GAME_TIME_LIMIT_MINUTES * 60; + +export const ROOM_PLAYERS_LIMIT = 4; diff --git a/deep-sea-stories/packages/common/src/index.ts b/deep-sea-stories/packages/common/src/index.ts index a97bd81..8d4e2aa 100644 --- a/deep-sea-stories/packages/common/src/index.ts +++ b/deep-sea-stories/packages/common/src/index.ts @@ -1,6 +1,7 @@ export { GAME_TIME_LIMIT_MINUTES, GAME_TIME_LIMIT_SECONDS, + ROOM_PLAYERS_LIMIT, } from './constants.js'; export type { AgentEvent } from './events.js'; export type { StoryData } from './types.js'; From 72303f2f28eeacbf15e5c062cd79784d49f71459 Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Wed, 10 Dec 2025 15:39:52 +0100 Subject: [PATCH 2/4] display message in join view --- .../packages/backend/src/controllers/peers.ts | 23 ++++++++++++++----- .../packages/backend/src/domain/errors.ts | 11 +++++++++ .../packages/backend/src/game/room.ts | 3 ++- .../packages/web/src/views/JoinView.tsx | 9 +++++++- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/deep-sea-stories/packages/backend/src/controllers/peers.ts b/deep-sea-stories/packages/backend/src/controllers/peers.ts index e9a8c6a..94a0575 100644 --- a/deep-sea-stories/packages/backend/src/controllers/peers.ts +++ b/deep-sea-stories/packages/backend/src/controllers/peers.ts @@ -1,4 +1,6 @@ import type { RoomId } from '@fishjam-cloud/js-server-sdk'; +import { TRPCError } from '@trpc/server'; +import { GameRoomFullError } from '../domain/errors.js'; import { GameRoom } from '../game/room.js'; import { createPeerInputSchema } from '../schemas.js'; import { roomService } from '../service/room.js'; @@ -18,10 +20,19 @@ export const createPeer = publicProcedure roomService.setGameRoom(room.id, gameRoom); } - const { peer, peerToken } = await gameRoom.addPlayer(input.name); - - return { - peer, - token: peerToken, - }; + try { + const { peer, peerToken } = await gameRoom.addPlayer(input.name); + return { + peer, + token: peerToken, + }; + } catch (error) { + if (error instanceof GameRoomFullError) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: error.message, + }); + } + throw error; + } }); diff --git a/deep-sea-stories/packages/backend/src/domain/errors.ts b/deep-sea-stories/packages/backend/src/domain/errors.ts index bfcebac..86fbe3b 100644 --- a/deep-sea-stories/packages/backend/src/domain/errors.ts +++ b/deep-sea-stories/packages/backend/src/domain/errors.ts @@ -66,3 +66,14 @@ export class FailedToStartStoryError extends DomainError { this.name = 'FailedToStartStoryError'; } } + +export class GameRoomFullError extends DomainError { + constructor() { + super( + 'GAME_ROOM_FULL', + 'Room is full. Please wait for a spot or create a new room.', + 400, + ); + this.name = 'GameRoomFullError'; + } +} diff --git a/deep-sea-stories/packages/backend/src/game/room.ts b/deep-sea-stories/packages/backend/src/game/room.ts index b7bb93a..f69396e 100644 --- a/deep-sea-stories/packages/backend/src/game/room.ts +++ b/deep-sea-stories/packages/backend/src/game/room.ts @@ -16,6 +16,7 @@ import { AudioStreamingOrchestrator } from '../service/audio-streaming-orchestra import type { NotifierService } from '../service/notifier.js'; import type { Story } from '../types.js'; import { GameSession } from './session.js'; +import { GameRoomFullError } from '../domain/errors.js'; type Player = { name: string; @@ -65,7 +66,7 @@ export class GameRoom { async addPlayer(name: string): Promise<{ peer: Peer; peerToken: string }> { if (this.players.size >= ROOM_PLAYERS_LIMIT) { - throw new Error(`Cannot join: limit of ${ROOM_PLAYERS_LIMIT} reached.`); + throw new GameRoomFullError(); } const { peer, peerToken } = await this.fishjamClient.createPeer( this.roomId, diff --git a/deep-sea-stories/packages/web/src/views/JoinView.tsx b/deep-sea-stories/packages/web/src/views/JoinView.tsx index ecc8c89..f8ee376 100644 --- a/deep-sea-stories/packages/web/src/views/JoinView.tsx +++ b/deep-sea-stories/packages/web/src/views/JoinView.tsx @@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui/sonner'; import { useTRPCClient } from '@/contexts/trpc'; +import { TRPCClientError } from '@trpc/client'; interface JoinViewProps { roomId: string; @@ -64,7 +65,13 @@ const JoinView: FC = ({ roomId }) => { peerMetadata: { name }, }); } catch (error) { - console.error('Failed to join room:', error); + if ( + error instanceof TRPCClientError && + error.data?.code === 'BAD_REQUEST' + ) { + toast(error.message, User); + } + console.error(error); } }, [trpcClient, roomId, joinRoom, name]); From f2d6b5df41edf35caa36d19adb12436a891cf27d Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Wed, 10 Dec 2025 15:40:26 +0100 Subject: [PATCH 3/4] remove unused domain errors --- .../backend/src/controllers/stories.ts | 3 +- .../packages/backend/src/domain/errors.ts | 59 ------------------- 2 files changed, 1 insertion(+), 61 deletions(-) diff --git a/deep-sea-stories/packages/backend/src/controllers/stories.ts b/deep-sea-stories/packages/backend/src/controllers/stories.ts index a5436aa..a50ff87 100644 --- a/deep-sea-stories/packages/backend/src/controllers/stories.ts +++ b/deep-sea-stories/packages/backend/src/controllers/stories.ts @@ -1,6 +1,5 @@ import type { RoomId } from '@fishjam-cloud/js-server-sdk'; import { stories } from '../config.js'; -import { FailedToStartStoryError } from '../domain/errors.js'; import { selectStoryInputSchema, startStoryInputSchema, @@ -57,7 +56,7 @@ export const startStory = publicProcedure }; } catch (error) { console.error(`Failed to start story: %o`, error); - throw new FailedToStartStoryError(0, (error as Error).message); + throw new Error((error as Error).message); } }); diff --git a/deep-sea-stories/packages/backend/src/domain/errors.ts b/deep-sea-stories/packages/backend/src/domain/errors.ts index 86fbe3b..640bb6b 100644 --- a/deep-sea-stories/packages/backend/src/domain/errors.ts +++ b/deep-sea-stories/packages/backend/src/domain/errors.ts @@ -8,65 +8,6 @@ export abstract class DomainError extends Error { this.statusCode = statusCode; } } - -export class GameSessionNotFoundError extends DomainError { - constructor(roomId: string) { - super( - 'GAME_SESSION_NOT_FOUND', - `No game session found for room ${roomId}`, - 404, - ); - this.name = 'GameSessionNotFoundError'; - } -} - -export class StoryNotFoundError extends DomainError { - constructor(roomId: string) { - super('STORY_NOT_FOUND', `No story available for room ${roomId}`, 400); - this.name = 'StoryNotFoundError'; - } -} - -export class NoPeersConnectedError extends DomainError { - constructor(roomId: string) { - super('NO_PEERS_CONNECTED', `No connected peers in room ${roomId}`, 400); - this.name = 'NoPeersConnectedError'; - } -} - -export class NoVoiceSessionManagerError extends DomainError { - constructor(roomId: string) { - super( - 'NO_VOICE_SESSION_MANAGER', - `No voice session manager configured for room ${roomId}`, - 500, - ); - this.name = 'NoVoiceSessionManagerError'; - } -} - -export class AudioConnectionError extends DomainError { - constructor(peerId: string, reason: string) { - super( - 'AUDIO_CONNECTION_ERROR', - `Failed to establish audio connection for peer ${peerId}: ${reason}`, - 500, - ); - this.name = 'AudioConnectionError'; - } -} - -export class FailedToStartStoryError extends DomainError { - constructor(storyId: number, reason: string) { - super( - 'FAILED_TO_START_STORY', - `Failed to start story ${storyId}: ${reason}`, - 500, - ); - this.name = 'FailedToStartStoryError'; - } -} - export class GameRoomFullError extends DomainError { constructor() { super( From 4cea62ceb2c7010e275fb65a6f731dbfd1982cad Mon Sep 17 00:00:00 2001 From: Bernard Gawor Date: Wed, 10 Dec 2025 16:18:33 +0100 Subject: [PATCH 4/4] add indicator in game view --- .../src/components/PlayerCountIndicator.tsx | 18 ++++++++++++++++++ .../packages/web/src/views/GameView.tsx | 18 +++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 deep-sea-stories/packages/web/src/components/PlayerCountIndicator.tsx diff --git a/deep-sea-stories/packages/web/src/components/PlayerCountIndicator.tsx b/deep-sea-stories/packages/web/src/components/PlayerCountIndicator.tsx new file mode 100644 index 0000000..3430881 --- /dev/null +++ b/deep-sea-stories/packages/web/src/components/PlayerCountIndicator.tsx @@ -0,0 +1,18 @@ +import { ROOM_PLAYERS_LIMIT } from '@deep-sea-stories/common'; +import { Users } from 'lucide-react'; +import type { FC } from 'react'; + +type PlayerCountIndicatorProps = { + count: number; +}; + +export const PlayerCountIndicator: FC = ({ + count, +}) => { + return ( +
+ + {count}/{ROOM_PLAYERS_LIMIT} +
+ ); +}; diff --git a/deep-sea-stories/packages/web/src/views/GameView.tsx b/deep-sea-stories/packages/web/src/views/GameView.tsx index c56c7fd..fb78786 100644 --- a/deep-sea-stories/packages/web/src/views/GameView.tsx +++ b/deep-sea-stories/packages/web/src/views/GameView.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react'; import { useEffect, useMemo, useRef } from 'react'; import GameControlPanel from '@/components/GameControlPanel'; import PeerGrid from '@/components/PeerGrid'; +import { PlayerCountIndicator } from '@/components/PlayerCountIndicator'; export type GameViewProps = { roomId: string; @@ -29,6 +30,8 @@ const GameView: FC = ({ roomId }) => { }, [agentPeer?.tracks[0]?.stream]); const userName = localPeer?.metadata?.peer?.name ?? 'Unknown'; + const playerCount = (localPeer ? 1 : 0) + displayedPeers.length; + return (
= ({ roomId }) => { agentStream={agentPeer?.tracks[0]?.stream} /> - +
+
+ +
+ +
{/* biome-ignore lint/a11y/useMediaCaption: Peer audio feed from WebRTC doesn't have captions */}