Code structure

In this guide you will learn how InstaQuiz server works and how you can extend it for your own usage.

How it works

The application uses Express.Js, Postgresql in the server and Socket.IO on both ends for real-time synchronization between users in a game room.

Data handling

Users, questions and game creation are handled by REST API using express routers.

And actions related to real-time running of the game such as sending game info to all users in game, receiving answers and updating leaderboard, starting the game loop and syncing questions for all users are handled by Socket.IO event handlers.

Caching

For lowest latency while the game loop is running game info and game leaderboard are cached with node-cache that is an in memory key-value caching library

Game logic

Game has 3 statuses:

  • 0 = Created

  • 1 = Running

  • 2 = Ended

Each game contains 10 questions that are randomly selected based on the category selected by the user while creating the game.

Anyone with the link to the game can join and if users lose connection or crash can rejoin if the game is still running.

Users can see who is in the game before joining if the game is ended the results will be displayed instead.

Each question has 20 seconds timeout and if the user answers in the first 6 seconds they will receive the complete score that is 10 and after that each if they answered from 7 to 16 seconds their score is lowered by 1 each 2 seconds and from 17 to 20 by each second delayed, the code for this logic is located at ./server/src/events/answer.ts

Code

The main entry for the application is located at ./server/src/app.js and it will be run when the app starts and sets up an Express.Js app, it's middlewares, routes, Socket.io events handler and event emitters.

Setup express

const app = express();

// Create http server
const server = createServer(app);

// Mount express middlewares, located at ./server/src/util/setup.ts
setupMiddlewares(app);

// Mount express routes ./server/src/routes
app.use("/", homeRouter);
app.use("/games", gamesRouter); // Handles game creation and game info
app.use("/categories", categoriesRouter);
app.use("/users", usersRouter); // User upsert

Setup Socket.IO

// Create Socket.IO instance with http server from previous step
const io = new Server(server, {
  cors: {
    origin: corsWhiteList(), // List of client URLs that can connect to server
  },
});

// Mount auth middleware for Socket.IO
io.use(verifyTokenSocket);

// Handle socket connection and events, ./server/src/events
io.on("connection", (socket: Socket) => {
  handleGetWaitList(socket); 
  handleJoinGame(socket, updateLeaderboard, updateWaitList);
  handleAnswer(socket, updateLeaderboard);
  handleStartGame(socket, updateGame, updateLeaderboard);
});

// Socket.IO room emitters
// Sends real-time data to game rooms using gameId

// Update game subscription wait list
const updateWaitList = async (gameId: number) => {
  const waitList = await getWailList(gameId);
  io.to(gameId.toString()).emit("updateWaitList", waitList);
};

// Update game subscription game status
const updateGame = async (gameId: number, fromCache: boolean = false) => {
  const gameInfo = await getCurrentGameInfo(gameId, fromCache);
  io.to(gameId.toString()).emit("updateGame", gameInfo);
};

// Update game subscription leaderboard
const updateLeaderboard = async (
  gameId: number,
  fromCache: boolean = false
) => {
  const leaderboardInfo = await getLeaderboardInfo(gameId, fromCache);
  io.to(gameId.toString()).emit("updateLeaderboard", leaderboardInfo);
};

Socket events

Event structure

Each event is a function that takes a socket param as entry that receives events

and can have 3 different optional functions as parameters for emitting events to users:

  • updateWaitList

  • updateGameInfo

  • updateLeaderboard

functions receive gameId as parameter and have an optional parameter for getting data from cache.

A socket event handler can have below structure:

const handleEvent = (
  socket: Socket,
  updateGame: (gameId: number, fromCache: boolean) => any,
  updateLeaderboard: (gameId: number, fromCache: boolean) => any
) => {
  // callback is optional for returning data to user
  // should be included in client code to work
  socket.on("event", async (params..., callback) => {
    try {
      // Do actions

      // Return data to the user who emmited the event
      callback({
        status: 200,
        extraData: data,
      });
    } catch (error) {
      callback({
        status: 500,
        message: "error",
      });
    }
  });
};

handleGetWaitList

Listens to getWaitList event from socket.

Input parameters

If game was ended it returns the leaderboard if not returns the list of users present in game.

Path: ./server/src/events/waitlist.ts

handleJoinGame

Listens to joinGame event from socket.

Input parameters

  • userId

  • gameId

If game was ended return error and if it was not the creator of the game add user to game_users table cache leaderboard, return gameInfo and leaderboard.

Path: ./server/src/events/join.ts

handleAnswer

Listens to gameAnswer socket event.

Input parameters:

  • gameId

  • userId

  • questionId

  • answerId

  • isCorrect

Get user answer, calculate score from game last_question_time field and save to database.

Path: ./server/src/events/answer.ts

handleStartGame

Listens to startGame socket event.

Input parameters:

  • userId

  • gameId

If the game has not started it will be updated in the database, game info and leaderboard will be retrieved from the database, game loop starts and returns current data to the user who started the game.

Game loop emits questions each 15 seconds using timeout and all data is handled by cache the time of game running to ensure there is no delay.

Last updated