Skip to content

Commit

Permalink
Merge pull request #615 from encorelab/develop
Browse files Browse the repository at this point in the history
Master <- Develop
  • Loading branch information
JoelWiebe authored Oct 23, 2024
2 parents 006a7fe + 710725b commit 9cc6906
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 94 deletions.
11 changes: 9 additions & 2 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,16 @@ const redisPassword = process.env.REDIS_PASSWORD || '';
const app = express();
app.use(
cors({
origin: process.env.CKBOARD_SERVER_ADDRESS,
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'cache', 'timeout'],
allowedHeaders: [
'Content-Type',
'Authorization',
'cache',
'timeout',
'Upgrade',
'Connection',
],
})
);
app.use(bodyParser.json());
Expand Down
169 changes: 77 additions & 92 deletions backend/src/socket/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,142 +32,127 @@ class Socket {
* @returns void
*/

init(redis: RedisClient) {
const origins = [];

if (process.env.CKBOARD_SERVER_ADDRESS) {
try {
const url = new URL(process.env.CKBOARD_SERVER_ADDRESS);
if (url.protocol === 'https:' || url.protocol === 'http:') {
origins.push(process.env.CKBOARD_SERVER_ADDRESS);
} else {
console.error('Invalid protocol in CKBOARD_SERVER_ADDRESS.');
}
} catch (error) {
console.error('Invalid URL in CKBOARD_SERVER_ADDRESS.', error);
}
} else {
console.error('CKBOARD_SERVER_ADDRESS environment variable not set.');
}

const io = new socketIO.Server(8000, {
cors: {
origin: origins,
},
adapter: createAdapter(redis.getPublisher, redis.getSubscriber),
});

this._io = io;

console.log('Socket server running...');
async init(redis: RedisClient) {
try {
// Connect Redis clients for the adapter
await Promise.all([redis.getPublisher.connect(), redis.getSubscriber.connect()]);

const io = new socketIO.Server(8000, {
transports: ['websocket', 'polling'], // Ensure WebSocket upgrades are allowed
cors: {
origin: process.env.CKBOARD_SERVER_ADDRESS || '*',
methods: ['GET', 'POST'],
},
adapter: createAdapter(redis.getPublisher, redis.getSubscriber),
});

io.on('connection', (socket) => {
// this._socket = socket;
this._io = io;
console.log('Socket server running on port 8000...');

socket.on('join', (user: string, room: string) => {
socket.data.room = room;
this._safeJoin(socket, user, room);
this._listenForEvents(io, socket);
this._logUserSocketsAndRooms(user);
});
// Handle socket connections
io.on('connection', (socket) => this._handleConnection(io, socket));
} catch (error) {
console.error('Error initializing WebSocket server:', error);
}
}

socket.on('leave', (user: string, room: string) => {
socket.leave(room);
console.log(`Socket ${socket.id} left room ${room}`);
events.map((e) => socket.removeAllListeners(e.type.toString()));
/**
* Handles new socket connections.
* @param io The WebSocket server instance.
* @param socket The connected socket.
*/
private _handleConnection(io: socketIO.Server, socket: socketIO.Socket) {
console.log(`New connection: Socket ID ${socket.id}`);

socket.on('join', (user: string, room: string) => {
socket.data.room = room;
this._safeJoin(socket, user, room);
this._listenForEvents(io, socket);
this._logUserSocketsAndRooms(user);
});

// Remove the specific socketId for the user from the SocketManager
this._socketManager.removeBySocketId(socket.id);
this._logUserSocketsAndRooms(user);
});
socket.on('leave', (user: string, room: string) => {
socket.leave(room);
console.log(`Socket ${socket.id} left room ${room}`);
events.forEach((e) => socket.removeAllListeners(e.type.toString()));
this._socketManager.removeBySocketId(socket.id);
this._logUserSocketsAndRooms(user);
});

socket.on('disconnect', () => {
const rooms = socket.rooms;
rooms.forEach((room) => {
socket.leave(room);
// Potentially update or notify the room of the disconnect
});
this._socketManager.removeBySocketId(socket.id);
});
socket.on('disconnect', () => {
console.log(`Socket ${socket.id} disconnected`);
this._cleanupSocket(socket);
});

socket.on('disconnectAll', async (room: string) => {
io.in(room).emit(SocketEvent.BOARD_CONN_UPDATE);
io.in(room).disconnectSockets(true);
});
socket.on('disconnectAll', (room: string) => {
console.log(`Disconnecting all sockets from room: ${room}`);
io.in(room).emit(SocketEvent.BOARD_CONN_UPDATE);
io.in(room).disconnectSockets(true);
});
}

/**
* Emits an event to a specific room.
*
* @param event The type of event being emitted.
* @param eventData Data associated with the event.
* @param roomId The ID of the room to which the event should be emitted.
* @param toSender Indicates whether the event should also be sent to the sender.
*/
emit(event: SocketEvent, eventData: unknown, roomId: string): void {
if (!this._io) {
throw new Error('IO not initialized. Please invoke init() first.');
}

if (!this._io) throw new Error('IO not initialized. Please invoke init() first.');
this._io.to(roomId).emit(event, eventData);
}

/**
* Setup socket to listen for all events from connected user,
* handle them accordingly, then emit resulting event back to all users.
*
* @param io socket server
* @param socket socket which will act on events
* @returns void
* Listens for events on a specific socket.
*/
private _listenForEvents(io: socketIO.Server, socket: socketIO.Socket) {
events.map((event) =>
events.forEach((event) =>
socket.on(event.type, async (data) => {
const result = await event.handleEvent(data);
return await event.handleResult(io, socket, result as never);
try {
const result = await event.handleEvent(data);
await event.handleResult(io, socket, result as never);
} catch (error) {
console.error(`Error handling event ${event.type}:`, error);
}
})
);
}

/**
* Allow user to join new room, while always leaving the previous
* room they were in (if applicable).
*
* @param socket socket which will join/leave room
* @param nextRoom new room to join
* @returns void
* Allows a socket to join a room, ensuring safe management of rooms.
*/
private _safeJoin(socket: socketIO.Socket, user: string, nextRoom: string) {
socket.join(nextRoom);
this._socketManager.add(user, socket.id);
console.log(`Socket ${socket.id} (userID ${user}) joined room ${nextRoom}`);
console.log(`Socket ${socket.id} (User ${user}) joined room ${nextRoom}`);
}

/**
* Logs the sockets and their corresponding rooms for a given user.
*
* @param userId The ID of the user whose sockets and rooms to log.
* Logs the current sockets and rooms for a given user.
*/
private _logUserSocketsAndRooms(userId: string): void {
const socketIds = this._socketManager.get(userId);
if (!socketIds) {
return;
}
if (!socketIds) return;

console.log(`User ${userId} =>`);
console.log(`User ${userId} has the following sockets and rooms:`);
socketIds.forEach((socketId) => {
const socket = this._io?.sockets.sockets.get(socketId);
if (socket && socket.data.room) {
console.log(`\tSocket ID: ${socketId}, Room: ${socket.data.room}`);
} else {
console.log(
`\tSocket ID: ${socketId} is not currently connected or has no room.`
);
console.log(`\tSocket ID: ${socketId} is not connected or has no room.`);
}
});
console.log('');
}

/**
* Cleans up a socket's rooms and listeners on disconnect.
*/
private _cleanupSocket(socket: socketIO.Socket) {
socket.rooms.forEach((room) => {
socket.leave(room);
console.log(`Socket ${socket.id} left room ${room}`);
});
this._socketManager.removeBySocketId(socket.id);
}
}

export default Socket;
export default Socket;
3 changes: 3 additions & 0 deletions frontend/src/polyfills.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Global object is defined for compatibility with Node.js libraries.
(window as any).global = window;

/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
Expand Down

0 comments on commit 9cc6906

Please sign in to comment.