https://serverless-stack.com/ logo
#help
Title
# help
k

Karolis Stulgys

05/23/2022, 4:32 PM
hi 👋 I'm exploring sst websockets. How can I send message from server to client on connect? Do I need to connect to that same ws in
connect
handler and send a message or how is that done?
r

Ross Coundon

05/23/2022, 4:35 PM
I was playing with a very very simple setup a while back that worked Stack looked like
Copy code
import * as sst from '@serverless-stack/resources';
import {RemovalPolicy} from 'aws-cdk-lib';

export default class WebSocketStack extends sst.Stack {

  private api: sst.WebSocketApi;
  private readonly connectionsTable: sst.Table;

  constructor(scope: <http://sst.App|sst.App>, id: string, props?: sst.StackProps) {
    super(scope, id, props)

    this.connectionsTable = this.createConnectionsTable();
    this.api = this.createApi();
    this.api.attachPermissions([this.connectionsTable]);

    this.addOutputs({
      ApiEndpoint: this.api.url
    })
  }

  private createConnectionsTable(): sst.Table {
    return new sst.Table(this, 'connections-table', {
      dynamodbTable: {
        removalPolicy: RemovalPolicy.DESTROY,
        timeToLiveAttribute: 'ttl',
      },
      fields: {
        PK: sst.TableFieldType.STRING,
        connectionId: sst.TableFieldType.STRING,
        ttl: sst.TableFieldType.NUMBER,
      },
      primaryIndex: {
        partitionKey: 'PK',
      }
    })
  }

  private createApi(): sst.WebSocketApi {
    return new sst.WebSocketApi(this, 'WebsocketApi', {
      accessLog: false,
      routes: {
        $connect: "src/main/handlers/wsHandlers.webSocketHandler",
        $disconnect: "src/main/handlers/wsHandlers.webSocketHandler",
        sendmessage: "src/main/handlers/wsHandlers.webSocketHandler",
        $default: "src/main/handlers/wsHandlers.webSocketHandler",
      },
      defaultFunctionProps: {
        environment: {
          CONNECTIONS_TABLE: this.connectionsTable.tableName,
          REGION: this.region,
        },
      }
    })
  }
}
Handlers like
Copy code
import { APIGatewayProxyHandler } from 'aws-lambda'
import { ApiGatewayManagementApiClient, PostToConnectionCommand  } from '@aws-sdk/client-apigatewaymanagementapi'

import { ConnectionsDao } from '@/transit/db/connectionsTable/ConnectionsDao'

export const webSocketHandler: APIGatewayProxyHandler = async (event) => {
  console.log(event);
  const { body, requestContext: { connectionId, routeKey }} = event;
  console.log(body, routeKey, connectionId);
  if(!connectionId) {
    return {
      statusCode: 400,
      body: JSON.stringify({
        message: 'ConnectionId is required'
      })
    }
  }
  switch (routeKey) {
    case '$connect':
      await ConnectionsDao.save(connectionId);
      return {
        statusCode: 200,
        body: JSON.stringify({
          message: 'Connected'
        })
      }

    case '$disconnect':
      await ConnectionsDao.delete(connectionId);
      return {
        statusCode: 200,
        body: JSON.stringify({
          message: 'Disconnected'
        })
      };

    case 'sendmessage': {
      let messageData = {};
      if(event.body){
        messageData = JSON.parse(event.body).data;
      }
      const { stage, domainName } = event.requestContext;

      // Get all the connections
      const connections = await ConnectionsDao.getAll();

      const apigw = new ApiGatewayManagementApiClient({
        region: 'eu-west-2',
        apiVersion: "2018-11-29",
        endpoint: `https://${domainName}/${stage}`,
      });

      const sendPromises = connections.map(connection => {
        const command = new PostToConnectionCommand({
          ConnectionId: connection.connectionId,
          // @ts-ignore
          Data: JSON.stringify(messageData),
        })
        return apigw.send(command);
      })
      await Promise.allSettled(sendPromises);

      return {
        statusCode: 200,
        body: JSON.stringify({
          message: 'Sent'
        })
      }
    }

    case '$default': {
      console.log('nothing to see here');
      return {
        statusCode: 401,
        body: JSON.stringify({
          message: '$default not in use'
        })
      }
    }
  }
  return {
    statusCode: 400,
    body: JSON.stringify({
      message: 'Unknown routeKey'
    })
  }
}
k

Karolis Stulgys

05/23/2022, 4:38 PM
Thanks, I got his working now. I'm now wonder how can I send message back immediately when client is connected
k

Klaus

05/23/2022, 4:44 PM
you can also look into the way SST does it's live debugging for more inspiration - the lambdas there are also web socket based
f

Frank

05/25/2022, 4:14 PM
@Karolis Stulgys here’s how
sst start
sends a msg back on connect as @Klaus suggested https://github.com/serverless-stack/serverless-stack/blob/master/packages/core/src/runtime/ws.ts#L72-L76