nearst / laconia

Create well-crafted serverless applications, effortlessly.
http://laconiajs.io
Apache License 2.0
326 stars 30 forks source link

WebSocket: Publish message to clients #45

Open ceilfors opened 5 years ago

ceilfors commented 5 years ago

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

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'));
srajat84 commented 5 years ago

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 ?

ceilfors commented 5 years ago

@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.

srajat84 commented 5 years ago

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

ceilfors commented 5 years ago

This diagram from AWS should give a high level overview:

image

hugosenari commented 5 years ago

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:

ceilfors commented 5 years ago

@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.

hugosenari commented 5 years ago

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 commented 4 years ago

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 commented 4 years ago

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. 🤔