H4ad / serverless-adapter

Run REST APIs and other web applications using your existing Node.js application framework (NestJS, Express, Koa, tRPC, Fastify and many others), on top of AWS, Azure, Huawei and many other clouds.
https://serverless-adapter.viniciusl.com.br/
MIT License
132 stars 8 forks source link

Adapter - AWS API Gateway Websocket #211

Open leonardodimarchi opened 6 months ago

leonardodimarchi commented 6 months ago

Feature Request

Is your feature request related to a problem? Please describe.

A few days ago i implemented a chat functionality at my api using api gateway websocket from aws. To do that, i've created an adapter (to forward the request context to my application) by extending aws simple adapter (just like SQS adapter) and creating the required interfaces.

It would be nice to have this adapter available in the library πŸš€

Describe the solution you'd like

Create an adapter to handle api gateway websocket requests, maybe a more robust one than mine (that only forwards the request context).

Describe alternatives you've considered

Are you willing to resolve this issue by submitting a Pull Request?

Right now i don't have the time for it, but it would be awesome to contribute!

H4ad commented 6 months ago

Hm... I already thought we could add support for Websocket but I never found any reasonable API Design to work with Websockets.

Websockets has the following events:

The support to receive data is very easy, and return data in the same connection is also easy since we can get the connectionId from the context and forward the response to the user using @connections

The main issue is when the API wants to send data to one or more users that are not related to the current connectionId, like when someone wins a game and they need to notify everyone connected, they have two choices:

The first option is preferable for faster responses, the second one is slower but also works.

But in any case, to support via my library, I will need to create some "Websocket -> HTTP", I don't think there is a way to map 1:1 with websocket protocol that we usually use (socket.io, ...etc).

Storing the users is something my library will not deal but maybe I could create an abstraction the developers could use, something like:

// my lib
export type WebsocketConnection<TExtraValues> = { connectionId: string } & TExtraValues;

export interface WebsocketStore<TExtraValues = {}> {
  getConnections(): Promise<WebsocketConnection<TExtraValues>[]>;
  storeConnection(connection: WebsocketConnection<TExtraValues>): Promise<void>;
  deleteConnection(connectionId: string): Promise<void>;
}

export function getCurrentStore<TExtraValues>(): WebsocketStore<TExtraValues>;

// on index.ts
type WebsocketCustomData = { name: string };
const customStore = new RedisWebsocketStore<WebsocketCustomData>(); // implements WebsocketStore
const websocketHandler = new WebsocketHandler(customStore);

// developers
const store = getCurrentStore<WebsocketCustomData>();
const connections = store.getConnections();

What do you think, this is something that could work for you?

leonardodimarchi commented 6 months ago

It would be great to have the possibility to just create a store, specify the headers that i want to use as my extra values and delegate the connect and disconnect part for the library!

I personally don't know how exactly it would work if my store were a nestjs service with typeorm dependency.

For me, the best thing that the adapter could do would be to forward the request context to my controller with the correct interface, i mean, it would be great to use something like this:

index.ts

const express = new ExpressFramework();
const framework = new LazyFramework(express, bootstrap);

export const handler = ServerlessAdapter.new(null)
  .setFramework(framework)
  .setHandler(new DefaultHandler())
  .setResolver(new PromiseResolver())
  .setLogger(createDefaultLogger({ level: 'error' }))
  .addAdapter(new ApiGatewayV2Adapter())
  .addAdapter(new ApiGatewayWebsocketAdapter({
       connectOptions: { // If not using the custom store for some reason
          path: '/websocket/connect',
          method: 'POST'
       },
       disconnectOptions: { // If not using the custom store for some reason
          path: '/websocket/disconnect',
          method: 'POST'
       },
       defaultOptions: { 
          path: '/websocket/default',
          method: 'POST'
       },
       messageOptions: { 
          basePath: '/websocket',  // Maybe append the custom route here as well, e.g: /websocket/new-chat-message
          method: 'POST'
       },
   }))
  .build();

websocket.controller.ts

@Controller('websocket')
export class WebsocketController {
  constructor(
      connectionRepo: TypeormConnectionRepo,
      messageRepo: TypeormMessageRepo
  ) {}

  // If not using the custom store for some reason
  @Post('connect')
  connect(connection: {  connectionId: string, headers:   {  [key:string]: string }}) {
      this.connectionRepo.insert(...)
  }

  [...]

  @Post('new-chat-message')
  newMessage(connectionId: string) {
      this.messageRepo.insert(...)

      // Iterate through all connections to send the message using @connections via aws-sdk for example
  }
}