websocketer-cluster
Version:
Cluster manager using WebSocket PubSub
241 lines (232 loc) • 6.86 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const EventEmitter = require('eventemitter3');
const redis = require('redis');
const websocketer = require('websocketer');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
const EventEmitter__default = /*#__PURE__*/_interopDefaultLegacy(EventEmitter);
const rxSpace = / /ig;
class RedisClusterClient extends EventEmitter__default {
constructor(options) {
super();
this._channel = "websocketer";
options = options || {};
options.id = options.id || websocketer.generateId(24);
options.timeout = options.timeout || 60;
options.host = options.host || "127.0.0.1:6379";
this._id = options.id;
this._options = options;
const parts = options.host.split(":");
this._publisher = options.client || redis.createClient({
host: parts[0],
port: parts[1] && parseInt(parts[1], 10),
user: options.username || "default",
password: options.password || "",
retry_strategy: (r) => Math.min(r.attempt * 100, 3e3)
});
this._subscriber = redis.createClient(this._publisher.options);
this._subscriber.subscribe(this._channel);
this._subscriber.on(
"message",
async (channel, message) => {
try {
if (channel !== this._channel)
return;
const data = JSON.parse(message);
if (data.ns !== this._channel)
return;
this.emit("message", data);
} catch (error) {
}
}
);
this._publisher.on("ready", () => this.emit("ready"));
this._publisher.on("connect", () => this.emit("connect"));
this._publisher.on("error", () => this.emit("error"));
this._publisher.on("end", () => this.emit("end"));
this.setId(this._id);
}
get options() {
return this._options;
}
get id() {
return this._id;
}
get channel() {
return this._channel;
}
get client() {
return this._publisher;
}
get subscriber() {
return this._subscriber;
}
get redisOptions() {
return this._options.client.options;
}
destroy() {
this.removeAllListeners();
this._publisher.quit();
this._subscriber.quit();
}
setId(id) {
this._id = id;
this._publisher.client("setname", `${this._channel}:${this._id}`);
}
async clients() {
return new Promise((resolve, reject) => {
this._publisher.client("list", (err, data) => {
if (err)
return reject(err);
const _clients = data.split("\n").map((c) => {
return Object.fromEntries(new URLSearchParams(c.replace(rxSpace, "&")));
}).filter((c) => {
return c.name?.startsWith(this._channel + ":");
});
resolve(_clients);
});
});
}
async clientIds() {
const clients = await this.clients();
if (!clients)
return [];
return clients.map((c) => c.name.split(":")[1]);
}
sendRequest(request) {
this._publisher.publish(this._channel, JSON.stringify(request));
}
}
class RedisCluster extends EventEmitter__default {
constructor(options) {
super();
this._clients = /* @__PURE__ */ new Set();
this._requests = /* @__PURE__ */ new Map();
this._clusterClient = new RedisClusterClient({
id: options.id,
client: options.client,
host: options.host,
username: options.username,
password: options.password,
debug: options.debug
});
this._timeout = options.timeout || 60;
this._handleEvents();
}
get cluster() {
return this._clusterClient;
}
get socketers() {
return this._clients;
}
get clients() {
return this._clients;
}
get requests() {
return this._requests;
}
destroy() {
this.removeAllListeners();
this._clients.clear();
this._clusterClient.destroy();
}
register(client) {
this._clients.add(client);
}
unregister(client) {
this._clients.delete(client);
}
async handleRequest(request) {
const _request = { ...request };
if (_request.rq) {
const clients = await this._clusterClient.clientIds();
this._requests.set(_request.id, {
clients: new Set(clients),
timeout: setTimeout(() => {
this._requests.delete(_request.id);
}, 1e3 * this._timeout)
});
}
this._clusterClient.sendRequest(_request);
}
_handleEvents() {
this._clusterClient.on(
"ready",
() => this.emit("ready")
);
this._clusterClient.on(
"message",
async (data) => {
let reply;
let targetClient;
let targetIsHere;
for (const client of this._clients) {
if (client.id === data.to) {
targetClient = client;
targetIsHere = true;
break;
} else if (data.to && client.remotes.has(data.to)) {
targetClient = client;
targetIsHere = false;
break;
}
}
try {
const request = this._requests.get(data.id);
if (request && !data.rq) {
if (data.er?.code === "ERR_WSR_NO_DESTINATION") {
request.clients.delete(data.er.payload);
if (request.clients.size)
return;
}
clearTimeout(request.timeout);
this._requests.delete(data.id);
}
if (targetIsHere === true) {
reply = await targetClient?.handleMessage(data);
} else if (targetIsHere === false) {
reply = await targetClient?.request("_request_", data);
} else if (data.rq) {
const error = new websocketer.WebSocketerError(
"No destination in cluster",
"ERR_WSR_NO_DESTINATION",
this._clusterClient.id
);
reply = websocketer.endRequestData(data, {
error: {
name: error.name,
code: error.code || "ERR_WSR_INTERNAL",
message: error.message,
payload: error.payload
}
});
}
} catch (error) {
reply = websocketer.endRequestData(data, {
error: {
name: error.name,
code: error.code || "ERR_WSR_INTERNAL",
message: error.message,
payload: error.payload
}
});
}
if (reply && data.rq)
this.handleRequest(reply);
}
);
}
}
class Client extends websocketer.Client {
constructor(options) {
super(void 0, options);
if (this._cluster)
this._cluster.cluster.setId(this._options.id);
}
_send(data) {
this._cluster?.handleRequest(data);
}
}
exports.Client = Client;
exports.RedisCluster = RedisCluster;
exports.RedisClusterClient = RedisClusterClient;