@tapsioss/client-socket-manager
Version:
<div align="center">
259 lines (258 loc) • 9.76 kB
JavaScript
import { io } from "socket.io-client";
import { ManagerReservedEvents, SocketReservedEvents } from "./constants.js";
import { assertCallbackType, isBrowser, warnDisposedClient } from "./utils.js";
class ClientSocketManager {
constructor(uri, options) {
var _a;
this._disposed = false;
this._socket = null;
this._inputListeners = {};
const { path = "/socket.io", reconnectionDelay = 500, reconnectionDelayMax = 2000, eventHandlers, ...restOptions } = options !== null && options !== void 0 ? options : {};
try {
this._socket = io(uri, {
...restOptions,
path,
reconnectionDelay,
reconnectionDelayMax,
});
this._inputListeners = eventHandlers !== null && eventHandlers !== void 0 ? eventHandlers : {};
this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
this._attachPageEvents();
this._attachSocketEvents();
this._attachManagerEvents();
(_a = this._inputListeners.onInit) === null || _a === void 0 ? void 0 : _a.call(this);
}
catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to initialize socket connection", {
uri,
path,
err,
});
}
}
_attachPageEvents() {
if (!isBrowser())
return;
document.addEventListener("visibilitychange", this._handleVisibilityChange);
}
_attachSocketEvents() {
if (!this._socket)
return;
const { onSocketConnection, onSocketConnectionError } = this._inputListeners;
if (onSocketConnection) {
this._socket.on(SocketReservedEvents.CONNECTION, onSocketConnection.bind(this));
}
if (onSocketConnectionError) {
this._socket.on(SocketReservedEvents.CONNECTION_ERROR, onSocketConnectionError.bind(this));
}
this._socket.on(SocketReservedEvents.DISCONNECTION, (reason, details) => {
var _a;
(_a = this._inputListeners.onSocketDisconnection) === null || _a === void 0 ? void 0 : _a.call(this, reason, details);
if (!this.autoReconnectable) {
if (reason === "io server disconnect") {
this.connect();
}
}
});
}
_attachManagerEvents() {
var _a;
const manager = (_a = this._socket) === null || _a === void 0 ? void 0 : _a.io;
if (!manager)
return;
const { onServerPing, onConnectionError, onReconnecting, onReconnectingError, onReconnectionFailure, onSuccessfulReconnection, } = this._inputListeners;
if (onConnectionError) {
manager.on(ManagerReservedEvents.CONNECTION_ERROR, onConnectionError.bind(this));
}
if (onServerPing) {
manager.on(ManagerReservedEvents.SERVER_PING, onServerPing.bind(this));
}
if (onReconnecting) {
manager.on(ManagerReservedEvents.RECONNECTING, onReconnecting.bind(this));
}
if (onReconnectingError) {
manager.on(ManagerReservedEvents.RECONNECTING_ERROR, onReconnectingError.bind(this));
}
if (onReconnectionFailure) {
manager.on(ManagerReservedEvents.RECONNECTION_FAILURE, onReconnectionFailure.bind(this));
}
if (onSuccessfulReconnection) {
manager.on(ManagerReservedEvents.SUCCESSFUL_RECONNECTION, onSuccessfulReconnection.bind(this));
}
}
_detachPageEvents() {
if (!isBrowser())
return;
document.removeEventListener("visibilitychange", this._handleVisibilityChange);
}
_detachSocketEvents() {
var _a;
(_a = this._socket) === null || _a === void 0 ? void 0 : _a.off();
}
_detachManagerEvents() {
var _a;
(_a = this._socket) === null || _a === void 0 ? void 0 : _a.io.off();
}
_handleVisibilityChange() {
var _a, _b;
const isPageVisible = document.visibilityState === "visible";
const isPageHidden = document.visibilityState === "hidden";
if (!isPageVisible && !isPageHidden)
return;
if (isPageVisible) {
(_a = this._inputListeners.onVisiblePage) === null || _a === void 0 ? void 0 : _a.call(this);
if (!this.connected)
this.connect();
}
else {
(_b = this._inputListeners.onHiddenPage) === null || _b === void 0 ? void 0 : _b.call(this);
this.disconnect();
}
}
/**
* Whether the client is disposed.
*/
get disposed() {
return this._disposed;
}
/**
* Emits an event to the socket identified by the channel name.
*/
emit(channel, ...args) {
warnDisposedClient(this.disposed);
if (!this._socket)
return;
this._socket.emit(channel, ...args);
}
/**
* A unique identifier for the session.
*
* `null` when the socket is not connected.
*/
get id() {
var _a, _b;
warnDisposedClient(this.disposed);
return (_b = (_a = this._socket) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
}
/**
* Whether the socket is currently connected to the server.
*/
get connected() {
var _a, _b;
warnDisposedClient(this.disposed);
return (_b = (_a = this._socket) === null || _a === void 0 ? void 0 : _a.connected) !== null && _b !== void 0 ? _b : false;
}
/**
* Whether the connection state was recovered after a temporary disconnection.
* In that case, any missed packets will be transmitted by the server.
*/
get recovered() {
var _a, _b;
warnDisposedClient(this.disposed);
return (_b = (_a = this._socket) === null || _a === void 0 ? void 0 : _a.recovered) !== null && _b !== void 0 ? _b : false;
}
/**
* Whether the Socket will try to reconnect when its Manager connects
* or reconnects.
*/
get autoReconnectable() {
var _a, _b;
warnDisposedClient(this.disposed);
return (_b = (_a = this._socket) === null || _a === void 0 ? void 0 : _a.active) !== null && _b !== void 0 ? _b : false;
}
/**
* Subscribes to a specified channel with a callback function.
*/
subscribe(
/**
* The name of the channel to subscribe to.
*/
channel,
/**
* The callback function to invoke when a message is received on the channel.
*/
cb, options) {
warnDisposedClient(this.disposed);
if (!this._socket)
return;
assertCallbackType(cb, `Expected a valid callback function. Received \`${typeof cb}\`.`);
const { onSubscriptionComplete, signal } = options !== null && options !== void 0 ? options : {};
const listener = (...args) => {
var _a;
warnDisposedClient(this.disposed);
if (!this._socket)
return;
(_a = this._inputListeners.onAnySubscribedMessageReceived) === null || _a === void 0 ? void 0 : _a.call(this, channel, args);
cb.apply(this, args);
};
this._socket.on(channel, listener);
const unsubscribe = () => {
this.unsubscribe(channel, listener);
signal === null || signal === void 0 ? void 0 : signal.removeEventListener("abort", unsubscribe);
};
signal === null || signal === void 0 ? void 0 : signal.addEventListener("abort", unsubscribe);
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
unsubscribe();
onSubscriptionComplete === null || onSubscriptionComplete === void 0 ? void 0 : onSubscriptionComplete.call(this, channel);
}
/**
* Removes the listener for the specified channel.
* If no callback is provided, it removes all listeners for that channel.
*/
unsubscribe(
/**
* The name of the channel whose listener should be deleted.
*/
channel,
/**
* The subscriber callback function to remove.
*/
cb) {
warnDisposedClient(this.disposed);
if (!this._socket)
return;
if (cb)
this._socket.off(channel, cb);
else
this._socket.off(channel);
}
/**
* Manually connects/reconnects the socket.
*/
connect() {
var _a;
warnDisposedClient(this.disposed);
(_a = this._socket) === null || _a === void 0 ? void 0 : _a.connect();
}
/**
* Manually disconnects the socket.
* In that case, the socket will not try to reconnect.
*
* If this is the last active Socket instance of the Manager,
* the low-level connection will be closed.
*/
disconnect() {
var _a;
warnDisposedClient(this.disposed);
(_a = this._socket) === null || _a === void 0 ? void 0 : _a.disconnect();
}
/**
* Disposes of the socket, manager, and engine, ensuring all connections are
* closed and cleaned up.
*/
dispose() {
var _a, _b;
warnDisposedClient(this.disposed);
(_a = this._inputListeners.onDispose) === null || _a === void 0 ? void 0 : _a.call(this);
this._detachPageEvents();
this._detachSocketEvents();
this._detachManagerEvents();
this.disconnect();
(_b = this._socket) === null || _b === void 0 ? void 0 : _b.io.engine.close();
this._socket = null;
this._inputListeners = {};
this._disposed = true;
}
}
export default ClientSocketManager;