laconia icon indicating copy to clipboard operation
laconia copied to clipboard

WebSocket: Publish message to clients

Open ceilfors opened this issue 5 years ago • 9 comments

Continuation of the discussion that we have at #41

As using combining DynamoDB and WebSocket is a common use case, laconia can make users life easier by publishing the below public contracts.

Discussions

  • Is broadcasting a common scenario?
  • If WebSocket is used to reply to individual clients, how do we know which client do we want to retrieve? It looks like there's an additional info from the user land that we need to store if we want to support this

On connect / disconnect

const { websocketHandler } = require('@laconia/websocket')

// Understands event type of $connect and $disconnect, automatically store and delete connection ID in DynamoDB
exports.handler = websocketHandler('WEBSOCKET_CONNECTION_TABLE_NAME')

On sending message API is inspired from: https://github.com/websockets/ws

const { createWss } = require("@laconia/websocket");

// wss object is injected
const app = async (event, { wss }) => {
  // Automatically get all client objects from DynamoDB
  const clients = await wss.getClients()

  // Sends data to a client, wraps apigwManagementApi
  // What's the best way to find the client here?
  await clients[0].send("something")

  // Broadcast, also wraps apigwManagementApi
  await Promise.all(clients.map(c => c.send('something')))
  await wss.broadcast('something')
};

exports.handler = laconia(app).register(createWss('WEBSOCKET_CONNECTION_TABLE_NAME'));

ceilfors avatar May 24 '19 21:05 ceilfors

AWS lambda is designed to be short lived but web sockets are long lived connections. Is it a design that aligns with AWS lambda architectural principles ?

srajat84 avatar Jun 25 '19 17:06 srajat84

@srajat84 Yes it is aligned, because the long-lived connections is actually managed by AWS API Gateway. There are a couple of entry points where you can provide hooks to Lambdas, but the hooks will be called in a short live manner. It was not possible in the past before AWS API Gateway supports WebSocket.

ceilfors avatar Jun 25 '19 19:06 ceilfors

I see, that would be great if you can publish a high level diagram for that

srajat84 avatar Jun 26 '19 16:06 srajat84

This diagram from AWS should give a high level overview:

image

ceilfors avatar Jun 26 '19 17:06 ceilfors

On sending message should work like batch

$connect should store queryStringParameters and provide someway to filter connections at reader.

ie: client could connect to ws://laconiajs.io?issue=45

exports.handler = laconiaWSS(wssOptions);
    // filter connections with issue == event.body.issue
    .reader((event, laconiaContext) => ({ issue: event.body.issue }))
    .on("client", (laconiaContext, event, client) => {
        client.send(event.body.message)
    });

:thinking:

hugosenari avatar Sep 14 '19 01:09 hugosenari

@hugosenari Thanks for the idea! One of the challenge on finding the connection is a developer might want to store the connection by multiple attributes. For example in a mobile application development, they might store connection id, user name, device id. Developers might only want to get a connection for a particular device for a particular user for example, and send a push there. This means, the way data we store to DynamoDB will have an additional field of username and device id, and the query to the table will be tricky to be done from the framework level. How do you think we can solve this scenario with the laconiaWSS?

What I suspect is, this feature will require much customisation in the user land.

ceilfors avatar Sep 16 '19 21:09 ceilfors

Client connect to ws://myapidomin/websocket?something=fooBar

$connect lambda will save DynamoDB[connectionId] = { event }.

Client can send 'update', 'get' messages that works as 'per connection storage', so he can set/change object for future filtering.

Site adm post to https://myapidomin/sendMessage { filter: fooBar, message: 'uhuuuu!' }, sendHandler should recursive scan and send message to clients.

Concept version: https://gist.github.com/hugosenari/ea17a0669981079dbb6fa51ad916fbea

hugosenari avatar Sep 18 '19 16:09 hugosenari

Maybe we could at least provide a postToConnection for echo. :thinking:

Actually to send message we do something like this:

const laconia = require("@laconia/core");
const AWS = require('aws-sdk');

const postToConnection = (requestContext, Data) => {
  const ConnectionId = requestContext.connectionId;
  const apiGatewayManagement = new AWS.ApiGatewayManagementApi({
    apiVersion: '2018-11-29',
    endpoint: `https://${requestContext.domainName}/${requestContext.stage}`
  });
  return apiGatewayManagement.postToConnection({ ConnectionId, Data }).promise()
};

const app = async(_, { event: { requestContext } }) => {
  postToConnection(requestContext, "FOO WebSocket Message");
};

exports.handler = laconia(app);

Could be:

const laconia = require("@laconia/core");
const { postToConnection } = require("@laconia/event").apigateway;

const app = (_, { event: { requestContext } }) => {
  postToConnection(requestContext, "FOO WebSocket Message");
};

exports.handler = laconia(app);

hugosenari avatar Dec 11 '19 20:12 hugosenari

We are not dependant of serverless.js but is currently most easily way to deploy lambda and serverless/components have two interesting components PubSub (push message with websocket isn't too different) and chat-app (that is what we tried to achieve) we could mashup both.

off-topic: maybe we could abstract some other components. 🤔

hugosenari avatar Feb 26 '20 15:02 hugosenari