UNPKG

zero-cluster

Version:

分布式游戏服务框架

1,006 lines (975 loc) 37.3 kB
import ws from "ws"; import { address, address as getLocalIp } from "ip" import ZeroRemote, { GameVisitor } from "zero-remote" import { ZeroRemoteClient } from "zero-remote/ZeroRemoteClient" import ZeroDispatcher from "zero-dispatcher"; import { IChannel, ISCMessage, ReturnReceiver } from "zero-remote/IZeroRomeote"; type ConnectorType = new (...ary: any[]) => Connector type ModuleType = new (...ary: any[]) => Module type Parameters<T> = T extends (...args: infer P) => any ? P : never; type ReturnType<T> = T extends (...args: any) => infer R ? R : any; export type ModuleSender<T> = { [K in keyof T]: ((value: Parameters<T[K]>[0], playerInfo?: PlayerInfo) => Promise<ReturnType<T[K]>>) } export let routeMap: WeakMap<ConnectorType | ModuleType, string> = new WeakMap() export let balancePool: { [key: string]: (player: any, list: any) => number } = {} /** * 装饰器 用于路由接口 * @param key * @returns */ export function route(key: string) { return (type: ConnectorType | ModuleType) => { routeMap.set(type, key) } } /** * 装饰器 用于落点均衡 * @param callback * @returns */ export function balance<T extends PlayerInfo>(callback: (player: T, pool: any) => any) { return (type: ModuleType) => { let route = getRoute(type) if (route) { balancePool[route] = callback } } } /** * 获取装饰器路由名 * @param connectorType * @returns */ export function getRoute(connectorType: new (...args: any[]) => any): string { let route = routeMap.get(connectorType) if (route) { return route } else { throw new Error("这个类没有@route") } } /** * 服务节点类型 */ export enum ServerType { MODULE, // 模块(面向后端的服务) CONNECT, // 连接(面向前端用户) SPECIAL, // 其他 } /** * 服务节点共享信息通信协议 节点到主服务 */ interface IZeroServerCS { init: (value: { url: string, name: string, key: string, modules: string[], type: ServerType, pid: number, label?: string, info: { [key: string]: any } }) => void getPid: () => number upInfo: (value: { key: string, value: any }) => void getAllPingTime(): { [sid: number]: number } } /** * 服务节点共享信息通信协议 主服务到节点 */ interface IZeroServerSC { clustered: (value: number) => void add: (value: IZeroServerInfo) => void upInfo: (value: { sid: number, key: string, value: any }) => void remove: (value: number) => void } /** * 服务节点共享信息接口 */ export interface IZeroServerInfo { key: string; name: string; label?: string; url: string, modules: string[], type: ServerType, sid: number info: { [key: string]: any } } /** * 一个服务节点基类 * 一个服务节点实例拥有一个独立进程 内存 访问地址 * 服务节点实例之间使用长连接通信 * 服务节点可以有多个实例 * 服务节点的子类应该全部支持动态扩容 * 所有的服务节点都会向主服务进行连接并注册自己 */ export class ZeroServer extends ZeroRemoteClient { serverType: ServerType = ServerType.MODULE static mainUrl: string static modulePool: { [key: string]: Module } = {} modules: any = [] clusterSender = this.getSender<IZeroServerCS>("") clusterReceiver = this.getReceiver<IZeroServerSC>("") static pid: number serverPool: { [key: string]: IZeroServerInfo } = {} name: string key: string = "" serverId!: number; isClustered: boolean = false relinkMaxCount = 1800 label: string = ""; info: { [key: string]: any } = {} constructor(public port: number) { super(ZeroServer.mainUrl) this.name = this.constructor.name if (ZeroServer.mainUrl == null) { throw "请配置主服务器地址ZeroServer.mainUrl=\"XXX\"" } /** * 保存所有服务节点以方便扩展 */ this.clusterReceiver.add((info) => { this.serverPool[info.sid] = info }) this.clusterReceiver.remove((sid) => { delete this.serverPool[sid] }) this.clusterReceiver.clustered((sid: number) => { if (this.serverId == null) { this.isClustered = true this.serverId = sid this.clustered() } else { this.serverPool = {} console.log("重连集群") } }) this.clusterReceiver.upInfo((value) => { if (this.serverPool[value.sid] != null) { this.serverPool[value.sid].info[value.key] = value.value } }) this.link() this.on("linked", () => { if (ZeroServer.pid == null) { this.clusterSender.getPid().then((pid) => { if (ZeroServer.pid == null) { ZeroServer.pid = pid } this.sendInit() }) } else { this.sendInit() } }) /** * 生命不息 重连不止 */ this.on("popup", () => { console.log("集群失败") }) } clustered(): void { } private sendInit() { this.clusterSender.init({ url: getLocalIp() + ":" + this.port, name: this.name, label: this.label, key: this.key, type: this.serverType, modules: this.modules, pid: ZeroServer.pid, info: this.info }).catch((e) => { console.log("注册服务节点出错", e) } ) } upInfo(key: string, value: any) { this.clusterSender.upInfo({ key, value }).then(() => { }).catch((e) => { console.log("同步服务节点信息出错upInfo", e) }) } addModule<T extends Module<any, any>>(moduleType: ModuleType): T | undefined { let route = getRoute(moduleType) if (route) { let module = new moduleType(route) module.server = this module.start() ZeroServer.modulePool[route] = module this.modules.push(route) return module as any } } } /** * 启动在连接服务节点上 * 用来连接模块的线路 */ export class ClientFotModule extends ZeroRemoteClient { relinkMaxCount = 100 popupReLinkTime: number = 1000 popupReLinkTimeIndex!: NodeJS.Timeout; label?: string constructor(public url: string) { super(url) } initConnector(sid: number) { this.send("connector", "init", [sid], (ok, err) => { }) } } /** * 用户 * 一个用户一个实例 * 在连接服务节点上的一个用户实例 */ export class ZeroServerPlayer<T extends PlayerInfo = PlayerInfo> extends GameVisitor { static pool: { [key: string]: ZeroServerPlayer } = {} static index: number = 0 static count: number = 0 static getAtuoId() { return this.index++ } hook: ZeroDispatcher<any> = new ZeroDispatcher() connectorPool: { [key: string]: Connector } = {} clear(): void { super.clear() if (ZeroServerPlayer.pool[this.info.vid] != null) { delete ZeroServerPlayer.pool[this.info.vid] ZeroServerPlayer.count-- for (const key in this.connectorPool) { const item = this.connectorPool[key]; item.isClear = true item.clear() } } this.hook.emit("clear") } constructor(channel: IChannel<ISCMessage>, server: ZeroRemote, public info: T) { super(channel, server) ZeroServerPlayer.pool[info.vid] = this ZeroServerPlayer.count++ } } /** * 连接服务节点 * 连接服务节点面对用户 * 连接服务节点 可注册 连接器 和模块 * 连接服务对用户提供长连接服务 */ export class ConnectorServer<T extends PlayerInfo> extends ZeroServer { connectorTypePool: { [key: string]: new (...args: any[]) => Connector } = {}; serverType = ServerType.CONNECT modulesChannels: { [key: string]: ClientFotModule[] & { [key: string]: ClientFotModule } } = {}; clients: { [key: string]: ClientFotModule } = {}; bridges: Set<ClientFotModule> = new Set() bridgesEntries: IterableIterator<[ClientFotModule, ClientFotModule]>; lineSet: Set<number> = new Set() constructor(port: number, private playerInfoType: new (sid: number, uid: number) => T) { super(port) this.bridgesEntries = this.bridges.entries() } getBridge(): ClientFotModule { let nextInfo = this.bridgesEntries.next() if (nextInfo.done == true) { this.bridgesEntries = this.bridges.entries() nextInfo = this.bridgesEntries.next() } return nextInfo.value[0] } userReceive(connector: Connector, route: string, key: string, args: any) { } userReject(connector: Connector, err: Error | string, request: any) { if (err instanceof Error) { console.error(err.stack) } else { console.log(err) } } clustered() { console.log("创建连接服务:", this.serverId, getLocalIp() + ":" + this.port,) new ZeroRemote(new ws.Server({ port: this.port }), (channel, server) => { let user = new ZeroServerPlayer(channel, server, new this.playerInfoType(this.serverId, ZeroServerPlayer.getAtuoId())) for (const key in this.connectorTypePool) { if (Object.prototype.hasOwnProperty.call(this.connectorTypePool, key)) { const element = this.connectorTypePool[key]; let connector = new element(this, user, key); user.connectorPool[key] = connector } } for (const key in user.connectorPool) { if (Object.prototype.hasOwnProperty.call(user.connectorPool, key)) { const element = user.connectorPool[key]; element.start() } } return user }) this.clusterReceiver.add((info) => { if (info.type == ServerType.MODULE) { console.log(`发现服务:${info.name} 地址:${info.url}`) let clientFotModule = this.clients[info.sid] if (clientFotModule == null) { clientFotModule = this.createClient(info) this.clients[info.sid] = clientFotModule if (info.key == "bridge") { this.bridges.add(clientFotModule) } info.modules.forEach((modulesName: string) => { let clinets = this.modulesChannels[modulesName] if (clinets == null) { clinets = [] as any this.modulesChannels[modulesName] = clinets } clinets.push(clientFotModule) if (clientFotModule.label != null && clientFotModule.label != "") { clinets[clientFotModule.label] = clientFotModule } }) } } }) this.clusterReceiver.remove((sid) => { let client = this.clients[sid] if (client != null) { client.close() delete this.clients[sid] if (this.bridges.has(client)) { this.bridges.delete(client) } for (const key in this.modulesChannels) { if (Object.prototype.hasOwnProperty.call(this.modulesChannels, key)) { const element = this.modulesChannels[key]; let newList: any = [] element.forEach((item) => { if (item.url != client.url) { newList.push(item) if (item.label != null && item.label != "") { newList[item.label] = item } } }) this.modulesChannels[key] = newList } } } }) } createClient(info: IZeroServerInfo): ClientFotModule { let clientFotModule = new ClientFotModule("ws://" + info.url) clientFotModule.label = info.label clientFotModule.link(() => { clientFotModule.initConnector(this.serverId) }) clientFotModule.on("suspend", () => { this.lineSet.delete(info.sid) this.upInfo("lines", [...this.lineSet]) console.log("中断连接服务模块") }) clientFotModule.on("linked", () => { this.lineSet.add(info.sid) this.upInfo("lines", [...this.lineSet]) console.log("连接服务模块成功") }) clientFotModule.on("stop", () => { console.log("中止连接服务模块") }) clientFotModule.receive("module", "", this.moduleMessage.bind(this)) clientFotModule.receive("module", "kick", this.moduleKickMessage.bind(this)) clientFotModule.receive("router", "", this.serverMessage.bind(this)) return clientFotModule } /** * 转发到链接路由 * @param data */ private serverMessage(data: { key: string, data: any }) { this.router.emit("router." + data.key, data.data) } /** * 转发到前端 * @param data */ private moduleMessage(data: { key: string, data: any, vid: number, route: string }) { let user = ZeroServerPlayer.pool[data.vid] if (user) { user.channel.send({ route: data.route, key: data.key, data: data.data }) } } /** * 转发到前端 * @param data */ private moduleKickMessage(data: { key: string, data: any, vid: number, route: string }) { let user = ZeroServerPlayer.pool[data.vid] if (user) { user.kick(data.data) } } addConnector(connectorType: (new (...args: any[]) => Connector)) { let route = getRoute(connectorType) if (route) { this.connectorTypePool[route] = connectorType } } } /** * 远程的连接服务节点访客 * 启动在模块服务节点上,用于处理 模块服务节点 到 连接服务节点 的逻辑 * 有个连接服务点节作为客户端连了上来 */ export class ConnectorVisitor extends GameVisitor { serverId!: number; constructor(channel: IChannel<ISCMessage>, public moduleServer: ModuleServer) { super(channel, moduleServer.connectorServer) this.getReceiver<{ init: (serverId: number) => void }>("connector").init(async (serverId) => { this.serverId = serverId this.moduleServer.connectorVisitorPool[serverId] = this this.moduleServer.upLine() console.log("游戏前台连接初始化", this.serverId) }) } clear() { super.clear() if (this.serverId != null) { delete this.moduleServer.connectorVisitorPool[this.serverId] } console.log("游戏前台断开", Object.keys(this.moduleServer.connectorVisitorPool)) this.moduleServer.upLine() } sendRouterInfo(key: string, data: any) { let sendInfo = { route: "router", key: "", data: { key: key, data: data, } } this.channel.send(sendInfo) } handle(route: string, key: string, data: any, back: (Function | undefined)) { if (route == "module") { let module: any = ZeroServer.modulePool[data.r] let method: Function = module[data.k] if (method) { module.playerInfo = data.p module.isRemote = true let p = method.apply(module, data.a) if (back != null) { if (p instanceof Promise) { p.then((valueThen) => { back!(true, valueThen) }).catch((error: any) => { error.message if (typeof error == "string") { back!(false, error) } if (error instanceof Error) { if (error.message == null) { back!(false, JSON.parse(JSON.stringify(error))) } else { console.error(error) back!(false, error.message) } } else { back!(false, error) } }) } else { back(true, p) } } } } else { super.handle(route, key, data, back) } } } /** * 模块服务节点 * 模块服务节点只能注册模块 * 模块服务节点 接受来自所有的 连接服务节点 的连接请求 用于对连接服务节点提供服务 * 模块服务节点为被动节点 */ export class ModuleServer extends ZeroServer { serverType = ServerType.MODULE connectorVisitorPool: { [key: string]: ConnectorVisitor } = {} connectorServer: ZeroRemote constructor(port: number) { super(port) this.connectorServer = new ZeroRemote(new ws.Server({ port: this.port }), this.createVisitor.bind(this)) } createVisitor(channel: IChannel<any>, server: ZeroRemote<any>): ConnectorVisitor { return new ConnectorVisitor(channel, this) } clustered() { console.log(`创建服务${this.name} :${this.serverId}`) } getLines() { let out: number[] = [] for (const key in this.connectorVisitorPool) { out.push(key as any) } return out } /**报告给集群 */ upLine() { this.clusterSender.upInfo({ key: "lines", value: this.getLines() }).then(() => { }).catch((e) => { console.log("集群同步个体信息出错upLine", e) }) } } /** * 桥接服务是一种模块服务节点 * 桥接服务是一种已实现逻辑的特殊的模块服务节点 * 启用桥接服务后 可以在模块中给其它服务节点实例的用户发送信息 * 桥接服务 对所有的 连接服务节点 起作点 * 桥接服务启动多个实例后 将轮流处理每个请求 (可优化成按权重分配,需要让桥接服务向主服务发送自己的的权重值) */ export class BridgeServer extends ModuleServer { name = "桥接服务" key = "bridge" createVisitor(channel: IChannel<any>, server: ZeroRemote<any>): ConnectorVisitor { let v = new ConnectorVisitor(channel, this) v.getReceiver<{ bridge: (data: { playerInfo: PlayerInfo, data: any }) => void }>("connector").bridge(async (data) => { let server = this.connectorVisitorPool[data.playerInfo.serverId] if (server != null) { server.channel.send(data.data) } else { console.warn("no server " + data.playerInfo.serverId) } }) return v } } /** * 被序列化的传递的用户信息 */ export class PlayerInfo { name: string /** * * @param serverId * @param vid 用于路由到用户不发给前端 访客ID 内存ID */ constructor(public serverId: number, public vid: number, name?: string) { this.name = name || ("S" + serverId + "U" + vid) } } /** * 模块 * * 模块是单例模块 * 所有用户共用模块 * 模块被调用时含有基础用户信息 用户信息会被序列化和反序列化处理过 * 基础用户信息格式是所有模块统一格式 * 基础用户信息中应包含可用于检索更多用户信息的检索ID * 模块与后端到前端通信接口一个模块组一一对应 * 模块可向同一服务节点实例用户直接返回信息 * 向不同服务节点实例的用户返回信息需要启动桥接服务 * 模块之间无法互相调用 !!! * */ export class Module<SC = any, P extends PlayerInfo = any> { sender: ModuleSender<SC> public playerInfo!: P public isRemote: boolean = false public server!: ZeroServer public moduleKey: string = "module" constructor(public route: string) { this.sender = new Proxy<ModuleSender<SC>>({} as any, { get: (target: any, p: string, receiver: any) => { return (data: any, playerInfo: P | null = null) => { if (playerInfo == null) { playerInfo = this.playerInfo } if (playerInfo) { if (this.isRemote) { let sendInfo = { route: this.moduleKey, key: "", data: { route: this.route, key: p, data: data, vid: playerInfo.vid, time: new Date().getTime() } } if (this.server instanceof ModuleServer) { let visitor = (this.server as ModuleServer).connectorVisitorPool[playerInfo.serverId] if (visitor != null) { visitor.channel.send(sendInfo) } else { console.log(">>>>用户所在服务器可能出现问题", sendInfo) } } else { console.log(">>>>这个情况不明") } } else { if (playerInfo.serverId != this.playerInfo.serverId) { //这里要跨节通信 let sendInfo = { route: this.moduleKey, key: "", data: { route: this.route, key: p, data: data, vid: playerInfo.vid, time: new Date().getTime() } } let bridge = (this.server as ConnectorServer<any>).getBridge() bridge.send("connector", "bridge", { playerInfo: playerInfo, data: sendInfo }, () => { }) } else { let sendInfo = { route: route, key: p, data: data, time: new Date().getTime() } let user = ZeroServerPlayer.pool[playerInfo.vid] if (user != null) { user.channel.send(sendInfo) } else { console.warn("no User") } } } } } } }) } start() { } kick(playerInfo: P | null = null, data: any = 0) { if (playerInfo == null) { playerInfo = this.playerInfo } let sendInfo = { route: this.moduleKey, key: "kick", data: data } let visitor = (this.server as ModuleServer).connectorVisitorPool[playerInfo.serverId] visitor.channel.send(sendInfo) } } /** * 连接器 * 连接器面对用户 * 用户断线则连接器失效 * 连接器与前端到后端通信接口一个模块组一一对应 * 连接器用来处理一个前端通信接口的一个模块组下的所有接口 * 每一个用户每个模块都会产一个有连接器实例 * 连接器可以调用 服务模块 自动识别 服务横块是远程服务还是本机调用 * 调用注册在不同服务节点的模块时识别为远程调用 * 处理玩家的请求做了队列处理 * 可以获取注册在同一服务节点下的其它连接器的实例 */ export class Connector<CS = any, P extends PlayerInfo = any> { player!: ZeroServerPlayer<P> receiver: ReturnReceiver<CS>; maxLink: number = 100 private isFree: boolean = true isClear: boolean = false private requestQueue: any[] = [] public moduleKey: string = "module" constructor(public server: ConnectorServer<any>, player: ZeroServerPlayer<P>, public route: string) { this.player = player; this.route = route; /** * 接收来自己玩家的请求所有做了队例处理 防攻击 */ this.receiver = new Proxy<ReturnReceiver<CS>>({} as any, { get: (target: any, p: string, receiver: any) => { return (callback: (value: any) => Promise<void>) => { let router = (this.player as any).routerPool[route] if (router == null) { router = {}; (this.player as any).routerPool[route] = router } router[p] = (args: any) => { return new Promise((resolve: (value: any) => void, reject: (reason?: any) => void) => { if (this.requestQueue.length >= this.maxLink) { reject("请求数超出数量") } else { this.requestQueue.push({ route, key: p, callback, args, resolve, reject, }) } if (this.isFree) { this.isFree = false this.nextReceiver() } }) } } } }) } start() { } private nextReceiver() { if (!this.isClear) { if (this.requestQueue.length > 0) { let request = this.requestQueue.shift() if (request != null && request.callback != null) { request.callback.apply(null, request.args).then((value: any) => { //TODO 事务成功 request.resolve(value) this.server.userReceive(this, request.route, request.key, request.args) this.nextReceiver() }).catch((err: Error | string) => { //TODO 事务失败 request.reject(err) // 错误事件 this.server.userReject(this, err, request) this.nextReceiver() }) } } else { this.isFree = true } } } getConnector<T extends Connector>(connectorType: new (...args: any[]) => T): T { let routeKey = getRoute(connectorType) let connector = this.player.connectorPool[routeKey] return connector as T } getModule<T extends Module>(moduleType: new (...args: any[]) => T): T { let routeKey = getRoute(moduleType) let module = ZeroServer.modulePool[routeKey] if (module == null) { let remoteModule = this.getRemoteModule(routeKey) return new Proxy<T>({} as any, { get: (target: any, p: string, receiver: any) => { if (typeof moduleType.prototype[p] == "function") { return (...args: any[]) => { return new Promise((resolve: (value: any) => void, reject: (reason?: any) => void) => { if (remoteModule == null || remoteModule.isOpen == false) { remoteModule = this.getRemoteModule(routeKey) } if (remoteModule != null) { if (remoteModule.isOpen) { remoteModule.send(this.moduleKey, "", { r: routeKey, k: p, p: this.player.info, a: args }, (isError: boolean, value: any) => { if (isError) { resolve(value) } else { console.log("========================>远程回调出错", routeKey, p, value) reject(value) } }) } else { reject("服务关闭") } } else { reject("没有这个服务 " + routeKey,) console.log("没有这个服务" + routeKey + " " + p) } }) } } else { //TODO } }, set: (target: T, p: string, value: any, receiver: any) => { console.log(p) return true } }) } else { return new Proxy<T>({} as any, { get: (target: any, p: string, receiver: any) => { if (typeof moduleType.prototype[p] == "function") { return (...args: any[]) => { this.isFree module.playerInfo = this.player.info let _module: any = module return _module[p].apply(module, args) } } else { console.error("不支持属性调用") } }, set: (target: T, p: string, value: any, receiver: any) => { console.error("不支持属性赋值") console.log(p) return true } }) } } getRemoteModule(routeKey: string): ClientFotModule | null { let remoteModules = this.server.modulesChannels[routeKey] if (remoteModules) { if (balancePool[routeKey]) { let pick = balancePool[routeKey](this.player, remoteModules) return remoteModules[pick] } else { console.error("请添加@balance") return null } } else { return null } } clear() { } } /** * 集群管理主服务 * 只用于发现服务节点 * 没有任何服务逻辑 * 责任广播服务节点的注删信息连接信息通信时间等服务节点的基本状态信息 */ export default class ZeroCluster extends ZeroRemote<{}> { constructor(public port: number) { super(new ws.Server({ port: port }), (channel, server) => { return new ClusterVisitor(channel, server) }) } getAddress(value: string): string { return value + address() + ":" + this.port.toString() } } export class ClusterVisitor extends GameVisitor { static serverIndex: number = 0 static serverPid: number = 0 static pool: { [url: string]: ClusterVisitor } = {} static sidPool: { [url: string]: number } = {} receiver = this.getReceiver<IZeroServerCS>("") sender = this.getSender<IZeroServerSC>("") url!: string name!: string modules!: string[] sid!: number//在同一个server有相同的sid pid!: number//在同一内存中有相同的pid key: string = "" type!: ServerType; // lines: Set<number> = new Set() clusterServer!: ZeroCluster label?: string info: { [key: string]: any } = {} start() { this.clusterServer = this.server as any this.receiver.init(async (info) => { //TODO 这里要加密 let server = ClusterVisitor.pool[info.url] if (server != null) { server.clear() delete ClusterVisitor.pool[info.url] } let sid = ClusterVisitor.sidPool[info.url] if (sid != null) { this.sid = sid console.log("加入旧节点") } else { this.sid = ClusterVisitor.serverIndex ClusterVisitor.sidPool[info.url] = this.sid ClusterVisitor.serverIndex++ console.log("加入新节点", info.name, info.url) } this.url = info.url this.modules = info.modules this.type = info.type this.pid = info.pid this.name = info.name this.key = info.key this.label = info.label this.info = info.info /** * 给自己发一个完成的信息 */ this.sender.clustered(this.sid) for (const key in ClusterVisitor.pool) { const element = ClusterVisitor.pool[key]; /** * 向新入加者发送已加入者信息 */ this.sender.add({ url: element.url, modules: element.modules, type: element.type, sid: element.sid, name: element.name, key: element.key, label: element.label, info: element.info }) /** * 向所有已加入者发送新入加者信息 */ element.sender.add({ url: info.url, modules: info.modules, type: info.type, name: info.name, key: info.key, sid: this.sid, label: info.label, info: info.info }) } ClusterVisitor.pool[info.url] = this }) this.receiver.getPid(async () => { return ClusterVisitor.serverPid++ }) this.receiver.upInfo(async (value) => { for (const subKey in ClusterVisitor.pool) { const element = ClusterVisitor.pool[subKey]; element.sender.upInfo({ sid: this.sid, key: value.key, value: value.value }) } }) this.receiver.getAllPingTime(async () => { let value: { [sid: number]: number } = {} for (const key in ClusterVisitor.pool) { const element = ClusterVisitor.pool[key]; value[element.sid] = element.pingTime } return value }) } clear(): void { super.clear() delete ClusterVisitor.pool[this.url] for (const key in ClusterVisitor.pool) { if (Object.prototype.hasOwnProperty.call(ClusterVisitor.pool, key)) { const element = ClusterVisitor.pool[key]; element.sender.remove(this.sid) } } } }