How can I handle RxJs websocket connection closings? Retry on server close, do nothing on client close

Viewed 1428

I have recently made a simple websocket service for my Angular app. It works great but I am having trouble figuring out how to handle the server/client closing the websocket connection. Here is my service:

import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject} from 'rxjs/webSocket';
import {environment} from '../../../environments/environment';

export const WS_ENDPOINT = environment.backendWebsocketEndpoint;
@Injectable({
  providedIn: 'root'
})
export class SimpleWebsocketService {

  private socket$  = webSocket({
    url: WS_ENDPOINT,
    deserializer: msg => {
      // If for some reason you want the whole response from AWS (you'll have to parse .data yourself)
      // return msg;

      // try to parse message as json. If we can't, just return whatever it is (usually bare string)
      try {
        return JSON.parse(msg.data);
      } catch (e) {
        console.warn('Websocket response could not be parsed as JSON. Returning raw value.')
        return msg.data;
      }
    }
  });
  public messages$ = this.socket$.asObservable();

  constructor() { }

  public sendMessage(msg: { action: string; message: string | object; }) {
    this.socket$.next(msg);
  }

  public closeConnection() {
    this.socket$.complete();
  }

}

And here is a simple component I have implemented it in:

import { Component, OnInit } from '@angular/core';
import { SimpleWebsocketService } from '../services/simpleWebsocket/simple-websocket.service'


@Component({
  selector: 'app-websocket',
  templateUrl: './websocket.component.html',
  styleUrls: ['./websocket.component.scss']
})
export class WebsocketComponent implements OnInit {

  messages: any[] = []; // array we will fill with messages from SimpleWebsocketService
  // Model for chat box form
  model = {
    newMessage: ''
  }

  constructor(public service: SimpleWebsocketService) { }

  ngOnInit(): void {
    // Sub to the messages observable
    this.service.messages$.subscribe(
      msg => {
        console.log('Message from server:', msg)
        this.messages.unshift(msg) // Push messages to local array so this component can reference and display them
      },
      error => {
        console.log('Error on socket connection:', error)
      },
      () => {
        console.log('Socket connection closed. By server or client?')
      }
    )
  }

  submit(formData: any) {
    this.service.sendMessage({"action": "whatever", "message": formData.value.message})
  }

}

If you read the service code carefully you might have noticed I am using AWS websockets via api gateway. This backend AWS service has a 10 minute idle timeout and a max session duration of 2 hours. I could send heartbeat requests from the client to keep the connection open every 9m50s but I still may run up against the hard 2 hour socket connection limit. I have noticed my subscription close console.log runs when AWS closes the connection. What's an elegant way to reconnect the websocket automatically when the SERVER closes the connection? I don't want to prevent the client from closing the connection. I would also like to handle the reconnection in the service if possible so I dont have to copy/paste a reconnection strategy in every component I want to use the websocket service in.

2 Answers

Some ideas:

  • Use repeat in conjunction with retry to automatically reconnect, if the socket disconnects
  • If you want to check if the connection was closed by the user, introduce a service property, which flags if you call closeConnection
  • Check this variable as an input for repeat or retry (maybe repeatWhen retryWhen is more suitable here such as repeatWhen(() => of(!this.userTerminated))

You can use reconnect after server close connection using with retryWhen and delayWhen operator.

const source = interval(1000);

const example = source.pipe(
   retryWhen(errors =>
      errors.pipe(
      // restart in 5 seconds
     delayWhen(val => timer(5000))
    )
   )
).subsribe();

And, if the client closes the connection manage that case with takeUntil() operator.

const clicks = fromEvent(document, 'click')

const result = source.pipe(takeUntil(clicks)).subsribe();
Related