Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

socket.io with ssr #846

Closed
matheusdavidson opened this issue Nov 14, 2017 · 38 comments
Closed

socket.io with ssr #846

matheusdavidson opened this issue Nov 14, 2017 · 38 comments
Labels
Milestone

Comments

@matheusdavidson
Copy link

Does universal work with socket.io? and how to?

@Toxicable Toxicable added the FAQ label Nov 18, 2017
@Toxicable
Copy link

This is sort of a tricky question.
Universal can work with socket.io but you have to use it a in a different way.
By default Universal waits for all async operations to complete before rendering to a string, this includes things like http requests, promises, Observables and WebSockets.
Since Web Sockets are long lived they stay open and will actually block the app from rendering on Universal so you need to somehow close this connection so that Universal can render.

Sorry I can't provide specifics on how to do this since i've never used socket.io

@CaerusKaru
Copy link
Member

CaerusKaru commented Nov 18, 2017

By default Universal waits for all async operations to complete before rendering to a string

To follow-up on this comment, renderModuleFactory, the main action in Universal rendering, will wait for the first stable VM tick to emit before rendering. This may or may not include the WebSocket activity from socket.io. As such, the main concern should be whether or not socket.io prevents that tick from completing or not. It's possible that with WebSockets, which are just event producer/consumers, they will just be passively considered like DOM event listeners. Please feel free to test this and report back!

@matheusdavidson socket.io should work assuming the client you're using is Universal compatible (i.e. doesn't try to reference document, or window) and pulls in its own version of WebSocket for Node (I know this is an issue with Apollo atm, but socket.io I think is fine). The only issues you may face come with duplicate requests, since TransferStateHttpModule does not work with wss connections, which may result in flicker (and you will also need to provide full URLs). You can of course disable socket.io on the server by only including it in a browser-specific module or by placing socket calls behind isPlatformBrowser checks.

@Toxicable
Copy link

Toxicable commented Nov 18, 2017

@CaerusKaru This here
https://github.com/angular/angular/blob/d2707f14574bdfd4ea0c0ecf3d85383f6d54186a/packages/platform-server/src/utils.ts#L46
Which means it waits for all Zones caught operations, which should be all async operations

@CaerusKaru
Copy link
Member

CaerusKaru commented Nov 18, 2017

After throwing together a basic WebSocket example, which socket.io will use if available, it looks like WebSockets are tracked by Zones. Here's the example on StackBlitz, running the WebSocket echo test and then closing the socket. I also updated the appRef subscribe to match what renderModuleFactory does. You can see that it is initially shown as stable, and then no other stability checks are rendered, implying that once you invoke a WebSocket, your application is unstable for the duration of its existence. See comment below for resolution.

@Toxicable
Copy link

@CaerusKaru WebSockets are tracked be Zones, you can see that by putting NgZone.assertInAngularZone() inside the handler event. Also you can see the patch here
https://github.com/angular/zone.js/blob/a66595af18d4c42584401dd2df387b02cb01d8d9/lib/browser/websocket.ts

However as I mentioned above, WebSockets are long lived, they stay open untill they are closed. In your example the WebSocket is never closed, that's why you don't see the stable event fire and why Universal wont work with socket.io or firebase without special handling for the websockets

@CaerusKaru
Copy link
Member

CaerusKaru commented Nov 18, 2017

@Toxicable on line 25 the socket is closed once the message is received. I saw that code too and this leads me to believe that closing the socket is not sufficient to restoring stability.

After looking into this a little more, this is apparently a documented and long-lived issue. However, I think the one advantage Universal has in this case is that I think it's almost guaranteed this won't hold up rendering, since the appRef call is almost immediately invoked after the components render (ie the initial view is stable, and then immediately not stable). But if this turns out not to be the case after testing, the recommendation would be (1) move socket.io into the browser build only or (2) run all socket.io-related code outside the Angular Zone, which may result in a performance hit/change-detection issues.

@Toxicable
Copy link

@CaerusKaru Whops, sorry missed that line, in that case I don't know whats going on in that example.

While both of those could be a solution, I personally think comming up with another way / api for handling when to render on the server should be investigated.

@CaerusKaru
Copy link
Member

@Toxicable I think that's a great idea! Some consistency would be a relief. Do you want to open a new issue to track the feature? I have some preliminary ideas that should probably go there.

@Toxicable
Copy link

@CaerusKaru sure, but open it up on the angular repo since that's where this stems from

@CaerusKaru
Copy link
Member

Tracking here: angular/angular#20520

@matheusdavidson
Copy link
Author

I did try but ended up using socket only in the browser build because as you guys pointed out angular doesn't finish rendering the page, probably its waiting for a response from the socket event.

Not sure if closing the connection all the time i receive a response would be very good because we could have other code waiting for socket events.

Maybe an option would be using socket.io with promises, i will try this when i have some time.

@vikerman
Copy link
Contributor

Hi - You can make a task trackable by Zone.js by doing something similar to here

https://github.com/angular/angular/blob/14e8e880222431eeccdacac6540574b49903bd79/packages/platform-server/src/http.ts#L38

Would that satisfy your use case?

@hiepxanh
Copy link
Contributor

hiepxanh commented Dec 24, 2017

any update on this? Can you write some instruction about ZoneMacroTaskWrapper ? My firebase application need to use that, hope you can help. @vikerman

@Ks89
Copy link

Ks89 commented Dec 29, 2017

It is very useful an official example with server + client using socket.io.
I tried to include it in my server code (based on the official node.js example with express) but it isn't working.

> project@1.0.0-beta.1 serve:ssr /Users/ks89/git/project
> node dist/server.js

/Users/ks89/git/project/dist/server.js:160136
    return /*require.resolve*/(!(function webpackMissingModule() { var e = new Error("Cannot find module \".\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()));
                                                                                                                                               ^

Error: Cannot find module "."
    at webpackMissingModule (/Users/ks89/git/project/dist/server.js:160136:76)
....

@DominikTrenz
Copy link

DominikTrenz commented Feb 15, 2018

A workaround in my case was to force close the socket after a certain time. It increases the render time but it works:

this.socket = io(this.wssEndpoint);
setTimeout(() => {
     this.socket.close();
}, 10000);

I can't wait to see a more elegant solution :)

@CaerusKaru CaerusKaru added this to the Backlog milestone Mar 7, 2018
@takahser
Copy link

takahser commented Jul 26, 2018

how about rxjs support? it doesn't work when using rxjs for websocket communication (see: #1042)

@ignazioa
Copy link

ignazioa commented Aug 3, 2018

@Ks89 , We are experiencing the same error with Express + Universal. Were you able to find a solution ?

Error: Cannot find module "."
    at webpackMissingModule (/Users/ks89/git/project/dist/server.js 

@ulrichdohou
Copy link

Check this. an answer on stackoverflow. You need to detect if it is browser of or server side.
https://stackoverflow.com/a/41721980/6261137

@sean-nicholas
Copy link

I found a solution that works for me. I'm using feathers.js and don't handle the socket directy. The second step is therefore a guess how it works with pure socket.io.

There is somewhere a similar issue where I found parts of the solution, but I could not find it anymore - so thanks original author :)

First, the socket creation needs to run outside of angular:

this.ngZone.runOutsideAngular(() => {
  this.socket = io(this.BASE_URL)
})

Second, the on callback needs to run in the zone again (don't ask me why :D)

this.socket.on('message', (data) => {
  ngZone.run(() => {
    doStuff(data)
  })
})

Now it should work, but you just have created a memory leak because the socket does not get closed. You need to add a ngOnDestroy method to your service that created the socket:

ngOnDestroy(): void {
  this.socket.close()
}

For completeness here is my code that works with feathers.js:

data.service.ts:

import { Services } from './../../../../shared/types/services'
import { Injectable, NgZone, OnDestroy } from '@angular/core'
import { environment } from '../../environments/environment'
import feathers, { Application } from '@feathersjs/feathers'
import socketio from '@feathersjs/socketio-client'
import * as rx from 'feathers-reactive'
import * as io from 'socket.io-client'
import { ngFeathersWebsocketSupport } from '../../lib/ng-feathers-websocket-support'

@Injectable()
export class DataService implements OnDestroy {
  constructor(
    public ngZone: NgZone
  ) {
    this.init()
  }

  BASE_URL = environment.dataUrl
  app = feathers() as Application<Services>
  socket: SocketIOClient.Socket = null

  protected init() {
    this.ngZone.runOutsideAngular(() => {
      this.socket = io(this.BASE_URL)
      this.app
        .configure(socketio(this.socket, {
          timeout: 10 * 1000
        }))
        .configure(rx({ idField: '_id' }))
        .configure(ngFeathersWebsocketSupport({ ngZone: this.ngZone }))
    })
  }

  ngOnDestroy(): void {
    this.socket.close()
  }
}

ng-feathers-websocket-support.ts:

export function ngFeathersWebsocketSupport(options: { ngZone: any }) {
  const mixin = function (service) {
    const originalOn = service.on.bind(service)

    const serviceMixin = {
      on(event, cb) {

        return originalOn(event, (...args) => {
          options.ngZone.run(() => {
            cb(...args)
          })
        })
      }
    }

    // get the extended service object
    service.mixin(serviceMixin)
  }

  return function (app) {
    app.mixins.push(mixin)
  }
}

Hope this helps anyone :)

@sanjaynishad
Copy link

Simply we can check for browser and initilize the socket things

import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

constructor(@Inject(PLATFORM_ID) private platformId: Object) {
  if(isPlatformBrowser(this.platformId)) { // check if this is runing in browser
    this.socket = io('http://localhost:3000/'); // call io constructor
    this.init(); // do the all initilization, e.g. this.socket.on('message', msg => {})
  }
}

@hsuanyi-chou
Copy link

hsuanyi-chou commented Oct 5, 2019

@sean-nicholas
thank you so much !! it works !
by the way, using fromEvent in RxJs makes code much more clean.
in Websocket.service.ts

@Injectable({
  providedIn: 'root'
})
export class WebsocketService implements OnDestroy {

    private url = 'ws://localhost:8081';
    private socket: SocketIOClient.Socket;

    constructor(private readonly ngZone: NgZone) {

      this.ngZone.runOutsideAngular(() => {
        this.socket = io(this.url, {
          reconnection: true,
          reconnectionDelay: 5000,
          reconnectionAttempts: 5,
        });
      });

      this.on$('connection').subscribe(console.log);
      this.on$('error').subscribe(console.log);
      this.on$('wsException').subscribe(console.log);
      this.on$('connect_error').subscribe(console.log);
    }

    emit(channel: string, emitValue: any) {
      this.socket.emit(channel, emitValue);
    }
    on$(channel: string) {
      return fromEvent(this.socket, channel);
    }

    off(channel: string) {
      this.socket.off(channel);
    }

    ngOnDestroy() {
      this.socket.close();
    }
}

in Other.Component.ts

// ....

  ngOnInit() {
    this.websocketService.on$('message').subscribe((message:any) => {
      this.ngZone.run(() =>{
            this.message = message;
            this.changeDetectorRef.markForCheck()  // if you need.
    });
}
  sendMessage() {
       this.websocketService.emit('message', 'emit a message to server');
  }

  ngOnDestroy() {
    this.websocket.off('message');
  }

@Twois
Copy link

Twois commented Nov 6, 2019

@sean-nicholas @hsuanyi-chou

I tried these solutions, but what about the acknowledgements (callback) in the emit method? Because in this case we are not able to wait for that.

Actually I implemented a queue system, and wait for the response form the server, and if the queue is get emptied I close the connection.

@hsuanyi-chou
Copy link

@Twois
in WebsocketService, we have on$.

//.... 
   on$(channel: string) {
      return fromEvent(this.socket, channel);
    }

in Other.Component.ts, use on$ to subscribe and will be received the data when socket.io server emitted.

//...
   ngOnInit() {
        this.WebsocketService.on$('message').subscribe(data =>
            this.ngZone.run(() =>{
               this.message = message;
               this.changeDetectorRef.markForCheck()  // if you need.
            }
    });
   }
//...

you can send any keyword (replace 'message') you like to listen to.

remember using ngZone in the subscribe callback function.

@Twois
Copy link

Twois commented Nov 7, 2019

@hsuanyi-chou

I speak about this case:

emit(channel: string, emitValue: any, cb: Function) {
    this.socket.emit(channel, emitValue, cb);
}

In this case the angular won't wait for the response of the server.

@agcarlosandres
Copy link

@ulrichdohou Muchas gracias! la solución que presentaste planteada aqui: https://stackoverflow.com/a/41721980/6261137 me ayudo a resolver el problema... me salvaste!

@agcarlosandres
Copy link

Check this. an answer on stackoverflow. You need to detect if it is browser of or server side.
https://stackoverflow.com/a/41721980/6261137

Esta solución es sencilla y efectiva

@xtianus79
Copy link

Any update or help with this?

@Chef211
Copy link

Chef211 commented Jun 11, 2020

Does universal work with socket.io? and how to?

I’m just a beginner trying to understand the
business, maybe it can help me in my
writing and illustration of children’s books.

@xtianus79
Copy link

@Chef211 wth?

@xtianus79
Copy link

@Twois or @hsuanyi-chou @sanjaynishad @sean-nicholas Would any of you mind helping me out with this. I am a little lost

@sean-nicholas
Copy link

Hey @xtianus79 Whats your problem? Can you give some context? Have you tried the solutions above? What's working / what's not?

@xtianus79
Copy link

xtianus79 commented Jun 14, 2020

@sean-nicholas so I am in the middle of trying what you are speaking of and this other solution that is working per se and seems like a hack.

Let me show you what I have:
this is the SSE service, same would go for websockets.

import { isPlatformBrowser } from '@angular/common';
import { Injectable, NgZone, Inject, PLATFORM_ID } from "@angular/core";
import { Observable } from "rxjs";

@Injectable({
  providedIn: 'root'
})
export class SseService {

  private isBrowser: boolean = isPlatformBrowser(this.platformId);

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    private _zone: NgZone
    ) {
      if (this.isBrowser) {
        console.log('this happened here');
      }
     }

  getServerSentEvent(url: string): Observable<EventSource> {
    return Observable.create(observer => {
      const eventSource: EventSource = this.getEventSource(url);
      eventSource.onmessage = event => {
        this._zone.run(() => {
          observer.next(event);
        });
      };
      eventSource.onerror = error => {
        this._zone.run(() => {
          observer.error(error);
        });
      };
    });
  }
  private getEventSource(url: string): EventSource {
    if (this.isBrowser) {
      console.log('this happened here 2');
      return new EventSource(url);
    }
    
  }
}

this is the implementation.

ngOnInit() {
    this.sseService
      .getServerSentEvent('http://localhost:4201/user/stream')
      .subscribe(data => console.log('data here >>>>> ', data));

I am trying to implement this.ngZone.runOutsideAngular(() => { instead

If I don't use the browser detection hack then I get this error. FYI, I don't know why I am calling it a hack but it something about it smells.

ERROR ReferenceError: EventSource is not defined

@sean-nicholas
Copy link

@xtianus79 Okay, so I'm not quite sure if you need the runOutsideAngular if you are using server sent events. But if you do, I would think that this might work (just a guess, it's long ago that I implemented my solution and I don't know SSE)

getServerSentEvent(url: string): Observable<EventSource> {
    return Observable.create(observer => {
      this._zone.runOutsideAngular(() => {
        const eventSource: EventSource = this.getEventSource(url);
        eventSource.onmessage = event => {
          this._zone.run(() => {
            observer.next(event);
          });
        };
        eventSource.onerror = error => {
          this._zone.run(() => {
            observer.error(error);
          });
        };
      });
    });
  }

The isPlatformBrowser function is not a hack at all. I use it all the time. And especially in your use case it makes sense because EventSource does only exist in the browser and not in node.js. Therefore you need to make that distinction.

But I think you should return an EventSource mock from getEventSource if you are on the server otherwise const eventSource would be undefined and eventSource.onmessage would fail.

@nauraizmushtaq
Copy link

Hi,
I'm using Angular SSR with ASP.Net using SPA Services. Application became irresponsive when i pass 1K concurrent request using apache JMeter or WebSurg. Most of the time connection timeout shows up or application took more then 120 secs to load the home page.

It would be very helpful if someone could suggest a possible solution

@GNP1988
Copy link

GNP1988 commented Dec 16, 2022

Simply we can check for browser and initilize the socket things

import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

constructor(@Inject(PLATFORM_ID) private platformId: Object) {
  if(isPlatformBrowser(this.platformId)) { // check if this is runing in browser
    this.socket = io('http://localhost:3000/'); // call io constructor
    this.init(); // do the all initilization, e.g. this.socket.on('message', msg => {})
  }
}

work fine for me

@mazba07
Copy link

mazba07 commented Dec 23, 2022

Simply call WebSocket only in the browser mood. Follow the answer.
https://stackoverflow.com/a/41721980/5978179

Here is my socket Service

import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { io } from 'socket.io-client';
import { Observable } from 'rxjs';
import { isPlatformBrowser } from '@angular/common';

@Injectable({
  providedIn: 'root'
})
export class SocketServiceService {

  isBrowser: boolean | undefined;
  socket: any;
  // uri: string = "ws://localhost:3000";
  uri: string = "http://localhost:3000";

  constructor(@Inject(PLATFORM_ID) platformId: Object) {
    this.isBrowser = isPlatformBrowser(platformId);
    if (this.isBrowser) {
      this.socket = io(this.uri);
    }
  }

  listen(eventName: string) {
    return new Observable((subscriber) => {
      this.socket.on(eventName, (data: any) => {
        subscriber.next(data);
      });
    });
  }

  emit(eventName: string, data: any) {
    this.socket.emit(eventName, data);
  }

  close() {
    this.socket.close();
  }

}

@alan-agius4
Copy link
Collaborator

It looks like this issue is resolved. Please create a new issue if the problem is still a need. Thanks.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Mar 12, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests