Watch N Chill uses Redis to persist room state, user data, and chat messages. The Redis layer is organized into two main repositories accessed through a singleton service.
RedisService
The RedisService class provides centralized access to Redis repositories.
src/backend/redis/index.ts
export class RedisService {
private static instance : RedisService ;
public readonly rooms : RoomRepository ;
public readonly chat : ChatRepository ;
private constructor () {
this . rooms = RoomRepository . getInstance ();
this . chat = ChatRepository . getInstance ();
}
static getInstance () : RedisService {
if ( ! RedisService . instance ) {
RedisService . instance = new RedisService ();
}
return RedisService . instance ;
}
}
export const redisService = RedisService . getInstance ();
Usage
import { redisService } from '@/backend/redis' ;
// Access room operations
const room = await redisService . rooms . getRoom ( roomId );
// Access chat operations
const messages = await redisService . chat . getMessages ( roomId );
RoomRepository
Manages room data, user memberships, and video state in Redis.
Data structures
interface Room {
id : string ;
hostId : string ;
hostName : string ;
hostToken ?: string ;
videoUrl : string | null ;
videoType : 'youtube' | null ;
videoState : VideoState ;
users : User [];
createdAt : Date ;
}
interface VideoState {
isPlaying : boolean ;
currentTime : number ;
duration : number ;
lastUpdateTime : number ;
}
interface User {
id : string ;
name : string ;
isHost : boolean ;
joinedAt : Date ;
}
Methods
createRoom
(room: Room) => Promise<void>
Stores a new room in Redis with 24-hour TTL and adds it to the active rooms set. src/backend/redis/room-handler.ts:14-17
async createRoom ( room : Room ): Promise < void > {
await redis.setex( `room: ${ room . id } ` , 86400 , JSON.stringify(room));
await redis.sadd( 'active-rooms' , room.id);
}
getRoom
(roomId: string) => Promise<Room | null>
Retrieves a room by ID, parsing JSON and converting date strings back to Date objects. src/backend/redis/room-handler.ts:19-32
async getRoom ( roomId : string ): Promise < Room | null > {
const roomData = await redis . get ( `room: ${ roomId } ` );
if (! roomData ) return null;
const room = JSON . parse ( roomData ) as Room ;
room. createdAt = new Date ( room . createdAt );
room. users = room . users . map ( user => ({
... user ,
joinedAt: new Date ( user . joinedAt ),
}));
return room;
}
updateRoom
(roomId: string, room: Room) => Promise<void>
Updates an existing room’s data in Redis, refreshing the 24-hour TTL.
deleteRoom
(roomId: string) => Promise<void>
Removes a room from Redis and the active rooms set. src/backend/redis/room-handler.ts:38-41
async deleteRoom ( roomId : string ): Promise < void > {
await redis.del( `room: ${ roomId } ` );
await redis.srem( 'active-rooms' , roomId);
}
roomExists
(roomId: string) => Promise<boolean>
Checks if a room exists in Redis.
addUserToRoom
(roomId: string, user: User) => Promise<void>
Adds a user to a room, removing any existing user with the same ID (handles rejoin cases). src/backend/redis/room-handler.ts:47-56
async addUserToRoom ( roomId : string , user : User ): Promise < void > {
const room = await this . getRoom ( roomId );
if (! room ) throw new Error ( 'Room not found' );
room. users = room . users . filter ( u => u . id !== user . id );
room.users.push(user);
await this.updateRoom( roomId , room);
}
removeUserFromRoom
(roomId: string, userId: string) => Promise<void>
Removes a user from a room. If the room becomes empty, it’s deleted. If the host leaves and users remain, a new host is automatically assigned. src/backend/redis/room-handler.ts:58-77
async removeUserFromRoom ( roomId : string , userId : string ): Promise < void > {
const room = await this . getRoom ( roomId );
if (! room ) return;
room. users = room . users . filter ( u => u . id !== userId );
if (room.users.length === 0) {
await this . deleteRoom ( roomId );
} else {
if ( room . hostId === userId && room . users . length > 0 ) {
const newHost = room . users [ 0 ];
room . hostId = newHost . id ;
room . hostName = newHost . name ;
newHost . isHost = true ;
}
await this . updateRoom ( roomId , room );
}
}
updateVideoState
(roomId: string, videoState: VideoState) => Promise<void>
Updates the video playback state (playing, paused, current time, etc.).
setVideoUrl
(roomId: string, videoUrl: string, videoType: 'youtube') => Promise<void>
Sets a new video URL and resets the video state to initial values. src/backend/redis/room-handler.ts:87-100
async setVideoUrl ( roomId : string , videoUrl : string , videoType : 'youtube' ): Promise < void > {
const room = await this . getRoom ( roomId );
if (! room ) throw new Error ( 'Room not found' );
room. videoUrl = videoUrl ;
room. videoType = videoType ;
room. videoState = {
isPlaying: false ,
currentTime: 0 ,
duration: 0 ,
lastUpdateTime: Date . now (),
};
await this.updateRoom( roomId , room);
}
Redis key structure
Room data Key : room:{roomId}Type : String (JSON)TTL : 24 hoursStores complete room state including users, video URL, and playback state.
Active rooms set Key : active-roomsType : SetTTL : NoneTracks all currently active room IDs for cleanup and monitoring.
ChatRepository
Manages chat message history for each room.
Data structures
interface ChatMessage {
id : string ;
roomId : string ;
userId : string ;
userName : string ;
message : string ;
timestamp : number ;
}
Methods
addMessage
(message: ChatMessage) => Promise<void>
Adds a message to the room’s chat history, maintaining only the last 20 messages. src/backend/redis/chat-handler.ts:29-37
async addMessage ( message : ChatMessage ): Promise < void > {
const key = `chat: ${ message . roomId } ` ;
await redis.rpush( key , JSON.stringify(message));
await redis.ltrim( key , - 20 , - 1 ); // Keep last 20 messages
await redis.expire ( key , 86400 ); // 24 hours TTL
}
Chat history is limited to the last 20 messages to keep memory usage bounded.
getMessages
(roomId: string) => Promise<ChatMessage[]>
Retrieves all stored messages for a room (up to 20). src/backend/redis/chat-handler.ts:39-47
async getMessages ( roomId : string ): Promise < ChatMessage [] > {
const key = `chat: ${ roomId } ` ;
const messages = await redis . lrange ( key , 0 , - 1 );
return messages.map( msg => JSON . parse ( msg ));
}
clearMessages
(roomId: string) => Promise<void>
Deletes all chat messages for a room. Called when a room is closed. src/backend/redis/chat-handler.ts:49-52
async clearMessages ( roomId : string ): Promise < void > {
const key = `chat: ${ roomId } ` ;
await redis.del(key);
}
Redis key structure
Chat messages Key : chat:{roomId}Type : List (JSON messages)TTL : 24 hoursMax size : 20 messages (oldest messages are trimmed)Stores the most recent chat messages for a room, automatically pruned to the last 20 entries.
TTL and cleanup
All Redis keys use a 24-hour TTL to ensure automatic cleanup of abandoned rooms:
Room data expires 24 hours after last update
Chat messages expire 24 hours after creation
Socket connection counters expire after 1 hour
Rate limit keys expire after the rate limit window
When a room’s TTL expires, both the room data and associated chat history are lost. Users cannot rejoin expired rooms.
Redis configuration
The Redis client is initialized from the REDIS_URL environment variable:
src/backend/redis/client.ts
import IORedis from 'ioredis' ;
const redisUrl = process . env . REDIS_URL || 'redis://localhost:6379' ;
export const redis = new IORedis ( redisUrl );
See Environment variables for Redis connection configuration options.