FoalTS / foal

Full-featured Node.js framework 🚀
https://foalts.org/
MIT License
1.9k stars 141 forks source link

Web socket support #481

Closed kingdun3284 closed 2 years ago

kingdun3284 commented 5 years ago

Is there any road map on supporting web socket? Or any suggested temporary solution for now?

rustamwin commented 5 years ago

If do you want socket.io based - socket-controllers ;)

LoicPoullain commented 5 years ago

We can add it in the roadmap (August release will focus on adding social auth though).

Websockets use a different protocol from http and so it will take some time to implement. The idea would to use the same Context object for controllers but with a different Context.request property. In this way, we will be able to keep hooks that use ctx.user, ctx.session and ctx.state.

I'll think on how to implement a temporary solution for now. Is your frontend in JS (or TS)?

kingdun3284 commented 5 years ago

My frontend is TS

LoicPoullain commented 5 years ago

Ok so we could go with socket.io then. I'll take at look today on what can be done. 👍

LoicPoullain commented 5 years ago

A workaround could be this one:

index.ts

import * as socketIO from 'socket.io';

// ...

async function main() {
  // ...

  const app = createApp(AppController);

  const httpServer = http.createServer(app);
  const io = socketIO(httpServer);

  io.on('connection', socket => {
    socket.on('chat message', msg => {
      console.log('message: ' + msg);
      // Use foal.services.get(MyService) to call a service
    });
  });

  const port = Config.get('port', 3001);
  httpServer.listen(port, () => {
    console.log(`Listening on port ${port}...`);
  });
}

main();

It's pretty ugly, but it's the only thing I can propose for now.

LoicPoullain commented 5 years ago

Depending on the app you're creating, there is also the polling technique that can be another workaround.

osman-mohamad commented 4 years ago

any news about supporting socket.io out of the box ?

LoicPoullain commented 4 years ago

@osman-mohamad I won't have time to implement something before 2020.

Just to know, in which case would you like to use socket.io / websockets. Are there particular features that like / love ?

Edo78 commented 4 years ago

@LoicPoullain personally I'm looking into socket.io integration to achieve realtime collaboration otherwise I'll have to move and adapt my db to firebase (or have to use my db AND firebase)

osman-mohamad commented 4 years ago

@LoicPoullain some thing like https://laravel.com/docs/5.7/broadcasting

damien-white commented 4 years ago

I think that supporting native WebSockets would be far better of an idea. Socket.io was created a long time ago in order to support browsers that had not yet had proper support for native WebSockets.

This is no longer the case. There is a much better library simply called "ws" that I use for actual WebSockets. I don't see why socket.io is the default go-to here. I would strongly recommend at least entertaining the idea of native WebSockets instead of the hacky version also known as "socket.io".

WebSockets are natively supported these days within all modern browsers. There is really no longer a need for socket.io. It's also far more simple to work with the "ws" library IMHO than "socket.io". You also don't lock people into using "socket.io-client" on their frontend if you're using actual WebSockets. If you go with "socket.io" you are automatically locking everybody into having to use the "socket.io-client" library on their frontends -- whether they want to use this library or not.

Edit: I wanted to point out that "ws" is potentially also actually far more popular than "socket.io". The following is a graph that shows monthly npm downloads of all the "major" WebSocket-related libraries:

https://www.npmtrends.com/socket.io-vs-sockjs-vs-websocket-vs-ws

These download trends might not be the best metric, but it does have some merit to it. I could be wrong, here, and this metric may mean nothing to you. Still, I figured it was worth including.

dengue8830 commented 4 years ago

I started a topic here. Personally I use socketio because

Socket.io is very old yes but it uses ws behind the scenes and have a lot of boilerplate battle tested. So I prefer that instead to re invent the wheel.

Also I think a framework should support both (would be ideal) or at least ws.

We can check how nestjs solved this problem.

damien-white commented 4 years ago

@dengue8830 You make some very good and valid points. Also, a good point RE: how Nest.js solved the problem. I completely forgot about their implementation. That would be like the best of both worlds. Really nice idea, in my opinion.

LoicPoullain commented 4 years ago

Thank you for sharing your experience on the subject.

Here is a how I see things at the moment based on your comments and my own research:

Requirements / Goal

Build apps with real-time collaboration.

Solution

To achieve this, the framework must allow to receive and broadcast notifications using WebSockets.

Constaints

Library comparison

Both librairies are widely used (> 3 000 000 downloads per week).

Pros of ws Pros of socket.io
Number of dependencies: 0 (ws) vs 40 (socketio) Auto-reconnection support
node_modules size: 0,1 MB (ws) vs 2,9 MB (socketio) Disconnection detection
Cumplies with WebSockets protocol. No need to add socketio client library. Handles proxies & load balancers
Network traffic Namespaces and rooms
Maybe easier to implement with Foal's stores.

Addditional link

https://stackoverflow.com/questions/10112178/differences-between-socket-io-and-websockets/38558531#38558531

damien-white commented 4 years ago

I think you raise some excellent points. My only real "concern" here, then, would be authenticating Websocket connections before allowing them to upgrade as this is a rather expensive operation to perform.

For libraries, I know that I had initially advocated for the "ws" library, but if Socket.IO is easier to implement and is easier to integrate into the framework, I would not be opposed to choosing Socket.IO.

I am indifferent, really, as to what lib is chosen. As long as Wesocket clients can be authenticated properly (ideally with the auth "baked in", but I understand that is, perhaps, much easier said than done) and the framework supports Websockets, I would be happy.

FoalTS is a fantastic framework. The one and only thing that is currently stopping me from using it is the fact that it does not support Websockets. It's much more simple for me to spin up a custom express or koa server and handle all the details myself.

I would love to be able to use FoalTS and I would even be willing to help out with integrating websockets support if you are planning to implement support for them.

jonathanroze commented 3 years ago

Any news about WebSockets in FoalTS ? :)

LoicPoullain commented 3 years ago

@jonathanroze This feature will probably be implemented this year. But this is a tough subject that involves many parts of the framework so it will take some months to be coded and tested. It won't be available this spring for sure.

LoicPoullain commented 3 years ago

Aim

Be able to build web apps with real-time communication.

Here are three possible use cases:

Problem with the Current Version

The HTTP protocol does not allow real-time communication. The only solution to simulate it at present is to use HTTP long-polling, but this technique is not very practical and does not meet all needs.

To solve this, Foal will have to support another protocol: WebSocket.

The problem is that Websocket and HTTP are two very different protocols. So we won't be able to reuse our controllers and hooks as they are by simply adding a new parameter or a small feature.

As an example of difference, HTTP supports authentication management through cookies. Websockets don't.

Solution Philosophy

However, even though these protocols are very different, the framework should provide a Websocket architecture similar to what Foal's HTTP architecture already looks like (hooks, controllers, etc).

The Websocket integration should also provide the ability to reuse things that are already provided by the framework (@UserRequired hook, etc).

Expected Features

Basic

This section describes the minimum features required to have a working Websocket solution.

Advanced

This section presents advanced features that are more difficult to implement but that may be of greater interest.

Outstanding issues

Choice of Technology (ws vs socket.io)

Websocket integration will use socket.io under the hood. The reasons for this choice are the following:

LoicPoullain commented 3 years ago

Specification

Architecture

Websocket controller

import { SocketIOService, UseWebsocketSessions, ValidatePayload } from '@foal/socket.io';

export class WebSocketController extends SocketIOController {
  // Optional
  options = { path, adapter, etc }; // socket.io options when creating the connection.

  subControllers = [
    wsController('users ', UserController)
  ];

  // Optional
  async onConnection(ctx: WebsocketContext) {
    // Do stuff
  }

  @EventName('create product')
  @UseWebsocketSessions()
  @ValidatePayload(/* ... */)
  createProduct(ctx: WebsocketContext, payload: any) {
    // Do stuff
  }
}

Client code

socket.emit('create product', { name: 'Jane Doe' });

src/index.ts

async function main() {
  const app = await createApp(AppController);
  const serviceManager = new ServiceManager();

  const httpServer = http.createServer(app, { serviceManager });
  const port = Config.get('port', 'number', 3001);
  httpServer.listen(port, () => displayServerURL(port));

  // Instanciate, init and connect websocket controllers.
  await serviceManager.get(WebSocketController).attachHttpServer(server);
}

Websocket context interface

class WebsocketContext<User = any, ContextSession = Session | undefined, ContextState = any> {
  state: ContextState = {} as ContextState;
  user: User;
  session: ContextSession;
  socket: Socket;
  eventName: string;
  payload: any;
}

Websocket hook

// Without a post funciton
@WebsocketHook((ctx: WebsocketContext, services: ServiceManager) => {

})

// With a post function
@WebsocketHook((ctx: WebsocketContext, services: ServiceManager) => {
  return (response: WebsocketResponse | WebsocketErrorResponse | undefined) => {

  }
})

Optional response

@EventName('create product')
createProduct(ctx: WebsocketContext, payload: any) {
  // Do stuff

  return new WebsocketResponse({ foo: 'bar' });
}

Sending messages

export class WebSocketController extends SocketIOController {
  @dependency
  websocket: WebsocketService;

  @EventName('create product')
  createProduct(ctx: WebsocketContext, payload: any) {
    ctx.socket.emit('foo', 'bar');
    ctx.socket.broadcast.emit('foo', 'bar');
  }
}

Joining rooms

export class WebSocketController extends SocketIOController {
  @dependency
  websocket: WebsocketService;

  @EventName('create product')
  createProduct(ctx: WebsocketContext, payload: any) {
    ctx.socket.join('room1');
    ctx.socket.to('room1').emit('foo', 'bar');
  }
}

Using multiple server instances

import { createAdapter } from 'socket.io-redis';

export class WebSocketController extends SocketIOController {
  adapters = createAdapter('redis://localhost:6379');
}

Error handling

A controller method can either return a WebsocketResponse or a WebsocketErrorResponse or it returns nothing.

export class WebSocketController extends SocketIOController {

  // Optional
  handleError() {
    // This method works exactly like `AppController.handleError` but for websockets.
  }

  @EventName('create')
  createUser(ctx, payload) {
    if (1 === 1) {
      return new WebsocketErrorResponse({
        message: 'something went wrong'
      });
    }

    return new WebSocketResponse();
  }
}

The client always receives an object of this shape (if the method sends a response):

interface Response {
  status: 'ok'|'error';
  error?: any;
  data?: any;
}

Examples

// Server
return new WebSocketResponse();
// Client
{
  status: 'ok'
}

// Server
return new WebSocketResponse('foo');
// Client
{
  status: 'ok',
  data: 'foo'
}

// Server
return new WebsocketErrorResponse('foo');
// Client
{
  status: 'error',
  error: 'foo'
}

Re-using HTTP hooks

@EventName('create product')
@HttpToWebsocketHook(UserRequired())
createProduct() {

}

Keeping users logged in

export class WebSocketController extends SocketIOController {

  @EventName('create product')
  @HttpToWebsocketHook(UseSessions())
  @HttpToWebsocketHook(UserRequired())
  createProduct() {
    // ...
  }
}
pospile commented 2 years ago

Any update on this one?

LoicPoullain commented 2 years ago

@pospile still in progress. I just need to find some time to finish this PR.

LoicPoullain commented 2 years ago

Update : the code, the tests and the documentation are now ready.

What remains to be done: write the release notes.

Websockets support will be published with version 2.8.

LoicPoullain commented 2 years ago

Feature added in v2.8 🎉