Skip to content

Instantly share code, notes, and snippets.

@jonstuebe
Created December 22, 2023 15:33
Show Gist options
  • Save jonstuebe/6b17109649a19ffd722d0ec7121d2080 to your computer and use it in GitHub Desktop.
Save jonstuebe/6b17109649a19ffd722d0ec7121d2080 to your computer and use it in GitHub Desktop.
A WebSocket class with reconnection support
class ReSocket {
private token: string | undefined;
private socket: WebSocket | undefined;
private listeners: boolean = false;
private currentAttempt: number = 0;
private backoffTime: number = 1000;
private maxAttempts: number = 30;
private timer: NodeJS.Timeout | undefined;
private messageFn: (data: any) => void = (data) => {
//
};
constructor(messageFn: (data: any) => void) {
this.messageFn = messageFn;
}
public updateToken(token: string) {
this.token = token;
if (this.socket) {
this.removeListeners();
this.socket.close();
}
this.init();
}
public destroy() {
if (this.socket) {
this.removeListeners();
this.socket.close();
this.socket = undefined;
}
}
private onOpen() {
console.log("websocket opened");
this.currentAttempt = 0;
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
}
private onError(event: Event) {
console.log("on error");
}
private onClose(event: any) {
if (this.listeners) {
this.removeListeners();
}
this.socket?.close();
this.socket = undefined;
this.currentAttempt++;
console.log(
`WebSocket connection failed. Trying to reconnect. This is attempt ${this.currentAttempt} of ${this.maxAttempts}`
);
if (this.currentAttempt >= this.maxAttempts) {
console.error("Failed to establish WebSocket connection.");
return;
}
const backoffTimeMilliseconds =
this.backoffTime * Math.pow(1.5, this.currentAttempt);
this.timer = setTimeout(this.init.bind(this), backoffTimeMilliseconds);
console.log(`Next reconnection attempt in ${backoffTimeMilliseconds}ms.`);
}
private addListeners() {
if (this.socket) {
this.socket.addEventListener("open", this.onOpen.bind(this));
this.socket.addEventListener("close", this.onClose.bind(this));
this.socket.addEventListener("error", this.onError.bind(this));
this.socket.addEventListener("message", this.onMessage.bind(this));
this.listeners = true;
}
}
private removeListeners() {
if (this.socket && this.listeners) {
this.socket.removeEventListener("open", this.onOpen.bind(this));
this.socket.removeEventListener("close", this.onClose.bind(this));
this.socket.removeEventListener("error", this.onError.bind(this));
this.socket.removeEventListener("message", this.onMessage.bind(this));
this.listeners = false;
}
}
private onMessage(event: MessageEvent<any>) {
this.messageFn(event.data);
}
private init() {
if (this.token === undefined) {
throw new Error("no token found");
}
this.socket = new WebSocket(`${WS_API_URL}?token=${this.token}`);
this.addListeners();
}
}
@WebReflection
Copy link

WebReflection commented Jun 7, 2024

these lines do nothing: https://gist.github.com/jonstuebe/6b17109649a19ffd722d0ec7121d2080#file-resocket-ts-L87-L90

proof

function showType({ type }) {
  this.alert(type);
}

document.addEventListener('click', showType.bind(globalThis));
document.removeEventListener('click', showType.bind(globalThis));

click anywhere on the page and see the alerted click.

bind creates a new reference every single time but it's also not needed at all most of the cases where you can use handleEvent instead.

@WebReflection
Copy link

WebReflection commented Jun 7, 2024

suggested changes:

class ReSocket {
  private token: string | undefined;
  private socket: WebSocket | undefined;
  private listeners: boolean = false;
  private currentAttempt: number = 0;
  private backoffTime: number = 1000;
  private maxAttempts: number = 30;
  private timer: NodeJS.Timeout | undefined;
  private messageFn: (data: any) => void;

  constructor(messageFn: (data: any) => void) {
    this.messageFn = messageFn;
  }

  public updateToken(token: string) {
    this.token = token;

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

    this.init();
  }

  public destroy() {
    if (this.socket) {
      this.removeListeners();
      this.socket.close();
      this.socket = undefined;
    }
  }

  // handle them all
  public handleEvent(event: Event) {
    this[`on${event.type}`](event);
  }

  private onopen() {
    console.log("websocket opened");

    this.currentAttempt = 0;
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = undefined;
    }
  }

  private onerror(event: Event) {
    console.log("on error");
  }

  private onclose(event: any) {
    if (this.listeners) {
      this.removeListeners();
    }

    this.socket?.close();
    this.socket = undefined;

    this.currentAttempt++;
    console.log(
      `WebSocket connection failed. Trying to reconnect. This is attempt ${this.currentAttempt} of ${this.maxAttempts}`
    );

    if (this.currentAttempt >= this.maxAttempts) {
      console.error("Failed to establish WebSocket connection.");
      return;
    }

    const backoffTimeMilliseconds =
      this.backoffTime * Math.pow(1.5, this.currentAttempt);

    this.timer = setTimeout(this.init.bind(this), backoffTimeMilliseconds);
    console.log(`Next reconnection attempt in ${backoffTimeMilliseconds}ms.`);
  }


  private onmessage(event: MessageEvent<any>) {
    this.messageFn(event.data);
  }

  // add them all
  private addListeners() {
    if (this.socket) {
      for (const type of ["open", "close", "error", "message"])
        this.socket.addEventListener(type, this);
      this.listeners = true;
    }
  }

  // remove them all
  private removeListeners() {
    if (this.socket && this.listeners) {
      for (const type of ["open", "close", "error", "message"])
        this.socket.removeEventListener(type, this);
      this.listeners = false;
    }
  }

  private init() {
    if (this.token === undefined) {
      throw new Error("no token found");
    }

    this.socket = new WebSocket(`${WS_API_URL}?token=${this.token}`);
    this.addListeners();
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment