...

/

Improving the Chat Application

Improving the Chat Application

Implement a simple multi-user chat application with WebSocket API.

We'll cover the following...

Multi-user chat

A multi-user chat application involves several users connected on different WebSockets. Our application keeps track of individual connections and passes messages to the right user using the corresponding WebSocket connections. First, we create a multi-user chat application to understand the typical WebSocket architecture pattern with the API Gateway.

import {
  DynamoDBClient,
} from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  PutCommand,
  GetCommand,
  DeleteCommand,
} from "@aws-sdk/lib-dynamodb";
import {
  ApiGatewayManagementApiClient,
  PostToConnectionCommand,
} from "@aws-sdk/client-apigatewaymanagementapi";

// Initialize DynamoDB Document Client
const ddbClient = new DynamoDBClient({});
const ddb = DynamoDBDocumentClient.from(ddbClient);

// DynamoDB Table Name
const TABLE_NAME = "WebSocketConnections";

/**
 * When connected to the WebSocket, add records in DynamoDB
 */
const onConnect = async (event) => {
  const connectionId = event.requestContext.connectionId;
  const userName = event.headers?.msgfrom || "unknown";
  const friendName = event.headers?.msgto || "unknown";

  // Store connection details
  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: {
        context: "Connection",
        id: connectionId,
        userName,
        friendName,
      },
    })
  );

  // Store user-to-connection mapping
  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: {
        context: "User",
        id: userName,
        friendName,
        connectionId,
      },
    })
  );
};

/**
 * When a user disconnects, remove connection records
 */
const onDisconnect = async (event) => {
  const connectionId = event.requestContext.connectionId;

  await ddb.send(
    new DeleteCommand({
      TableName: TABLE_NAME,
      Key: {
        context: "Connection",
        id: connectionId,
      },
    })
  );
};

/**
 * On receiving a message, forward it to the friend
 */
const onMessage = async (event) => {
  const connectionId = event.requestContext.connectionId;
  const apiId = event.requestContext.apiId;

  // Get the user's connection record
  const userResponse = await ddb.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: {
        context: "Connection",
        id: connectionId,
      },
    })
  );

  const friendName = userResponse.Item?.friendName;
  if (!friendName) {
    console.log("Friend not found for connection:", connectionId);
    return;
  }

  // Get the friend's connection record
  const friendResponse = await ddb.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: {
        context: "User",
        id: friendName,
      },
    })
  );

  const apigwManagementApi = new ApiGatewayManagementApiClient({
    endpoint: `https://${apiId}.execute-api.us-east-1.amazonaws.com/v1`,
  });

  const friendConnectionId = friendResponse.Item?.connectionId;

  try {
    // Send message to friend
    await apigwManagementApi.send(
      new PostToConnectionCommand({
        ConnectionId: friendConnectionId,
        Data: event.body || "",
      })
    );
  } catch (err) {
    console.log("Error posting to connection:", err);

    // Notify sender if friend is not reachable
    await apigwManagementApi.send(
      new PostToConnectionCommand({
        ConnectionId: connectionId,
        Data: "Your friend is not reachable. Connect from the other terminal and try again.",
      })
    );
  }
};

/**
 * Lambda handler
 */
export const handler = async (event) => {
  const routeKey = event.requestContext?.routeKey;

  if (routeKey === "$connect") {
    await onConnect(event);
  } else if (routeKey === "$disconnect") {
    await onDisconnect(event);
  } else if (routeKey === "$default") {
    await onMessage(event);
  }

  return { statusCode: 200, body: event.body || "{}" };
};
WebSocket chat

Click "Run" to trigger the script and deploy the WebSocket API. The script deploys the CloudFormation template and, in the end, provides us with two commands for connecting to the WebSocket server (where XXXXXXXX is the API ID for the API in your account).

  1. The first command is wscat -H msgfrom:Tom -H msgto:Jerry -c wss://XXXXXXXX.execute-api.us-east-1.amazonaws.com/v1.

  2. The second command is wscat -H msgfrom:Jerry -H msgto:Tom -c ...

Ask