├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── index.ts ├── lib │ ├── components │ │ └── GlobalChannelStatus.ts │ ├── manager │ │ ├── RedisGlobalChannelManager.ts │ │ └── StatusChannelManager.ts │ ├── service │ │ └── GlobalChannelServiceStatus.ts │ └── util │ │ └── Utils.ts └── tests │ ├── config │ └── redisConfig.ts │ └── simple.test.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | # https://github.com/actions/starter-workflows/blob/master/ci/npm-publish.yml 3 | on: 4 | [push,pull_request] 5 | # https://github.com/actions/starter-workflows/issues/158 6 | # redis 7 | # redis 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | env: 12 | NODE_ENV: ci 13 | services: 14 | redis: 15 | image: redis:4.0.0 16 | ports: 17 | - 6379:6379 18 | options: --entrypoint redis-server 19 | steps: 20 | - uses: actions/checkout@v1 21 | - uses: actions/setup-node@v1 22 | with: 23 | node-version: 16 24 | - run: npm install --dev 25 | - run: npm run build 26 | - run: npm run test -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | # https://github.com/actions/starter-workflows/blob/master/ci/npm-publish.yml 3 | on: 4 | # [push] 5 | release: 6 | types: [ created ] 7 | # https://github.com/actions/starter-workflows/issues/158 8 | # redis 9 | # redis 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | env: 14 | NODE_ENV: ci 15 | services: 16 | redis: 17 | image: redis:4.0.0 18 | ports: 19 | - 6379:6379 20 | options: --entrypoint redis-server 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-node@v2 24 | with: 25 | node-version: 16 26 | - run: npm install --dev 27 | - run: npm run build 28 | - run: npm run test 29 | 30 | publish-npm: 31 | needs: build 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: actions/setup-node@v2 36 | with: 37 | node-version: 16 38 | registry-url: 'https://registry.npmjs.org' 39 | - run: npm install 40 | - run: npm run build 41 | - run: npm publish 42 | env: 43 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | .idea 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | dist 41 | .DS_Store 42 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # This file is a template, and might need editing before it works on your project. 2 | # Official framework image. Look for the different tagged releases at: 3 | # https://hub.docker.com/r/library/node/tags/ 4 | image: node:latest 5 | 6 | # Pick zero or more services to be used on all builds. 7 | # Only needed when using a docker container to run your tests in. 8 | # Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service 9 | services: 10 | - redis:latest 11 | 12 | # This folder is cached between builds 13 | # http://docs.gitlab.com/ce/ci/yaml/README.html#cache 14 | cache: 15 | paths: 16 | - node_modules/ 17 | 18 | 19 | 20 | test_async: 21 | variables: 22 | NODE_ENV: gitlab 23 | 24 | script: 25 | - npm install --dev 26 | - rm -rf ./dist 27 | - npm run test 28 | 29 | 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | dist: trusty 4 | node_js: 5 | - "8" 6 | services: 7 | - redis-server 8 | env: 9 | - NODE_ENV=ci 10 | 11 | install: npm install --dev 12 | script: rm -rf ./dist && npm run test 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 frank198 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pinusGlobalChannelStatus 2 | [![Build Status](https://travis-ci.org/whtiehack/pinus-global-channel-status.svg?branch=v8)](https://travis-ci.org/whtiehack/pinus-global-channel-status) 3 | [![Actions Status](https://github.com/whtiehack/pinus-global-channel-status/workflows/ci/badge.svg?branch=v8&event=push)](https://github.com/whtiehack/pinus-global-channel-status/actions) 4 | 5 | pinus 分布式服务器通讯 原址:https://github.com/frank198/pomeloGlobalChannel 6 | 7 | 8 | # 使用说明 9 | 10 | - only node version 8.x or greater support 11 | - TS => es2017 语法 12 | - 兼容 pomelo 13 | - 兼容 pinus 14 | - 内置 redis使用 15 | - 发送消息给某一个或多个玩家 16 | - 发送消息给指定的channelName 17 | - 发送消息给指定 sid 和 channelName 18 | - 发送消息给用户列表中指定sid的用户 19 | 20 | 21 | 22 | 23 | ## Installation 24 | 25 | ``` 26 | npm install https://github.com/whtiehack/pinus-global-channel-status.git#v8 27 | 28 | or 29 | 30 | npm install pinus-global-channel-status 31 | ``` 32 | 33 | ## Usage 34 | 35 | 36 | ### pinus 37 | 38 | ``` 39 | import {createGlobalChannelStatusPlugin} from 'pinus-global-channel-status'; 40 | 41 | app.configure('production|development', 'connector', function () { 42 | ... 43 | app.use(createGlobalChannelStatusPlugin(),{ 44 | family : 4, // 4 (IPv4) or 6 (IPv6) 45 | options : {}, 46 | host : '192.168.99.100', 47 | password : null, 48 | port : 6379, 49 | db : 10, // optinal, from 0 to 15 with default redis configure 50 | // optional 51 | cleanOnStartUp:app.getServerType() == 'connector', 52 | }); 53 | }); 54 | 55 | ``` 56 | 57 | --- 58 | * use 59 | ``` 60 | import { GlobalChannelServiceStatus } from "pinus-global-channel-status"; 61 | const globalChannelStatus:GlobalChannelServiceStatus = app.get(GlobalChannelServiceStatus.PLUGIN_NAME); 62 | 63 | ``` 64 | 65 | 66 | ### pomelo 67 | ``` 68 | var globalChannelStatus = require('pinus-global-channel-status').PomeloExports; 69 | 70 | app.use(globalChannelStatus, {globalChannelStatus: { 71 | host: '127.0.0.1', 72 | port: 6379, 73 | password : null, 74 | db: '0' // optinal, from 0 to 15 with default redis configure 75 | }}); 76 | 77 | ``` 78 | 79 | 80 | --- 81 | * use 82 | ``` 83 | 84 | const globalChannelStatus = app.get('globalChannelServiceStatus'); 85 | 86 | or 87 | const GlobalChannelServiceStatus = require('pinus-global-channel-status').GlobalChannelServiceStatus; 88 | const globalChannelStatus = app.get(GlobalChannelServiceStatus.PLUGIN_NAME); 89 | 90 | ``` 91 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 3 | moduleFileExtensions: [ 4 | 'js', 5 | 'json', 6 | 'jsx', 7 | 'node', 8 | 'ts', 9 | 'tsx', 10 | ], 11 | globals: { 12 | 'ts-jest': { 13 | tsConfig: './tsconfig.json', 14 | }, 15 | }, 16 | restoreMocks: true, 17 | testEnvironment: 'node', 18 | preset: 'ts-jest', 19 | testMatch: null, 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinus-global-channel-status", 3 | "version": "7.0.1", 4 | "private": false, 5 | "main": "./dist/index", 6 | "types": "./src/index", 7 | "dependencies": { 8 | "redis": "^4.2.0" 9 | }, 10 | "devDependencies": { 11 | "@types/jest": "29.1.2", 12 | "@types/node": "16.18.87", 13 | "@types/redis": "4.0.10", 14 | "jest": "29.2.0", 15 | "typescript": "4.4.3", 16 | "ts-jest": "29.0.3", 17 | "ts-node": "10.2.1" 18 | }, 19 | "files": [ 20 | "dist/**/*.js", 21 | "src/**/*.*", 22 | ".gitignore", 23 | "jest.config.js", 24 | "package.json", 25 | "README.md", 26 | "tsconfig.json", 27 | "LICENSE" 28 | ], 29 | "engines": { 30 | "node": ">=8.5" 31 | }, 32 | "scripts": { 33 | "build": "tsc", 34 | "test": "./node_modules/.bin/jest" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "includeForks": true, 3 | "baseBranches": [ 4 | "v8" 5 | ], 6 | "extends": [ 7 | "config:base" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | import {GlobalChannelServiceStatus} from "./lib/service/GlobalChannelServiceStatus"; 5 | 6 | export {PinusGlobalChannelStatusOptions} from "./lib/manager/StatusChannelManager"; 7 | 8 | export {GlobalChannelServiceStatus} from './lib/service/GlobalChannelServiceStatus'; 9 | 10 | /** 11 | * 实现一个基本的插件,插件载入时,会被自动扫描handlerPath和remoterPath指向的目录 12 | */ 13 | export class GlobalChannelStatusPlugin { 14 | name = 'GlobalChannelStatusPlugin'; 15 | components = [GlobalChannelServiceStatus]; 16 | } 17 | 18 | export function createGlobalChannelStatusPlugin() { 19 | return new GlobalChannelStatusPlugin(); 20 | } 21 | 22 | export const PomeloExports = { 23 | components: __dirname + '/lib/components/', 24 | // events:__dirname + '/lib/events/' 25 | }; -------------------------------------------------------------------------------- /src/lib/components/GlobalChannelStatus.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | import {GlobalChannelServiceStatus} from "../service/GlobalChannelServiceStatus"; 5 | import {PinusGlobalChannelStatusOptions} from "../manager/StatusChannelManager"; 6 | 7 | 8 | module.exports = function (app, opts: PinusGlobalChannelStatusOptions) { 9 | return new GlobalChannelServiceStatus(app, opts); 10 | }; -------------------------------------------------------------------------------- /src/lib/manager/RedisGlobalChannelManager.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | import { PinusGlobalChannelStatusOptions, StatusChannelManager } from "./StatusChannelManager"; 5 | 6 | export default class GlobalChannelManager extends StatusChannelManager { 7 | constructor(app, opts: PinusGlobalChannelStatusOptions) { 8 | super(app, opts); 9 | } 10 | 11 | public async add(uid: string, sid: string, channelName: string | string[] = null) { 12 | if (!channelName || !channelName.length) 13 | return await super.add(uid, sid); 14 | if (typeof channelName == 'string') { 15 | const genKey = StatusChannelManager.GenKey(this.prefix, sid, channelName); 16 | return await this.redisClient.sAdd(genKey, uid); 17 | } 18 | let multi = this.redisClient.multi(); 19 | for (const channel of channelName) { 20 | multi.sAdd(StatusChannelManager.GenKey(this.prefix, sid, channel), uid); 21 | } 22 | return await multi.exec() as any; 23 | } 24 | 25 | public async destroyChannel(channelName: string | string[]): Promise { 26 | if (!channelName) { 27 | return null; 28 | } 29 | const servers = this.app.getServers(); 30 | if (typeof channelName == 'string') { 31 | channelName = [channelName]; 32 | } 33 | let multi = this.redisClient.multi(); 34 | for (const serverId of Object.keys(servers)) { 35 | const server = servers[serverId]; 36 | if (this.app.isFrontend(server)) { 37 | for (const channel of channelName) { 38 | if (channel) 39 | multi.del(StatusChannelManager.GenKey(this.prefix, serverId, channel)); 40 | } 41 | } 42 | } 43 | return (await multi.exec()) as any; 44 | } 45 | 46 | /** 47 | * 48 | * @param {string} uid 49 | * @param {string} sid serverid 50 | * @param {string | string[]} channelName 51 | * @returns {Promise} 52 | */ 53 | public async leave(uid: string, sid: string, channelName: string | string[] = null) { 54 | if (!channelName || !channelName.length) 55 | return await super.leave(uid, sid); 56 | if (typeof channelName == 'string') { 57 | const genKey = StatusChannelManager.GenKey(this.prefix, sid, channelName); 58 | return await this.redisClient.sRem(genKey, uid); 59 | } 60 | let multi = this.redisClient.multi(); 61 | for (const channel of channelName) { 62 | multi.sRem(StatusChannelManager.GenKey(this.prefix, sid, channel), uid); 63 | } 64 | return await multi.exec() as any; 65 | 66 | } 67 | 68 | /** 69 | * 70 | * @param {string} channelName 71 | * @param {string} sid server id 72 | * @returns {Promise} 73 | */ 74 | public async getMembersBySid(channelName: string, sid: string): Promise { 75 | const genKey = StatusChannelManager.GenKey(this.prefix, sid, channelName); 76 | return await this.redisClient.sMembers(genKey); 77 | } 78 | 79 | /** 80 | * Get members by channelName and serverType. 81 | * 获取指定 channelName 的成员 82 | * @param {String} serverType frontend server type string 83 | * @param {String|Array} channelName channel name 84 | * @returns {Promise} { connector_1:{ channelName1: [ 'uuid_21', 'uuid_12', 'uuid_24', 'uuid_27' ] }, 85 | connector_2: { channelName1: [ 'uuid_15', 'uuid_9', 'uuid_0', 'uuid_18' ] }, 86 | connector_3: { channelName1: [ 'uuid_6', 'uuid_3' ] } 87 | */ 88 | public async getMembersByChannelName(serverType: string, channelName: string | string[]): Promise<{ [serverid: string]: { [channelName: string]: string[] } }> { 89 | if (!serverType) { 90 | return {}; 91 | } 92 | let servers = this.app.getServersByType(serverType); 93 | if (!servers || servers.length === 0) { 94 | // no frontend server infos 95 | return {}; 96 | } 97 | if (typeof channelName == 'string') { 98 | channelName = [channelName]; 99 | } else { 100 | channelName = Array.from(new Set(channelName)); 101 | } 102 | const cmdArr = {}; 103 | for (const serverObject of servers) { 104 | const sid = serverObject.id; 105 | // 可能有bug? 106 | // let serverIdArr = cmdArr[sid] ? cmdArr[sid] : []; 107 | // serverIdArr = channelName.map(change => { 108 | // return ['smembers', StatusChannelManager.GenKey(this.prefix, sid, change)]; 109 | // }); 110 | // cmdArr[sid] = StatusChannelManager.ExecMultiCommands(this.redisClient, serverIdArr); 111 | let multi = this.redisClient.multi(); 112 | for (const channel of channelName) { 113 | multi.sMembers(StatusChannelManager.GenKey(this.prefix, sid, channel)); 114 | } 115 | cmdArr[sid] = multi.exec(); 116 | } 117 | const channelObjectArr: {}[] = await Promise.all(Object.values(cmdArr)); 118 | const channelObject = {}; 119 | const keys = Object.keys(cmdArr); 120 | for (let i = 0; i < keys.length; i++) { 121 | channelObject[keys[i]] = {}; 122 | for (let idx in channelObjectArr[i]) { 123 | channelObject[keys[i]][channelName[idx]] = channelObjectArr[i][idx]; 124 | } 125 | } 126 | return channelObject; 127 | } 128 | 129 | /** 130 | * 通过 服务器ID(sid) 和 指定的 channel 获取玩家列表 131 | * @param {string} sid 132 | * @param channelName 133 | * @return {Promise.} key是channelid { channelName1: [ 'uuid_18', 'uuid_3' ] } 134 | */ 135 | async getMembersByChannelNameAndSid(sid: string, channelName: string | string[]): Promise<{ [key: string]: string[] }> { 136 | if (!sid) { 137 | return null; 138 | } 139 | if (typeof channelName == 'string') { 140 | channelName = [channelName]; 141 | } 142 | let multi = this.redisClient.multi(); 143 | for (const channel of channelName) { 144 | multi.sMembers(StatusChannelManager.GenKey(this.prefix, sid, channel)); 145 | } 146 | const channelObjectArr = await multi.exec(); 147 | const channelObject = {}; 148 | for (let i = 0; i < channelName.length; i++) { 149 | channelObject[channelName[i]] = channelObjectArr[i]; 150 | } 151 | return channelObject; 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /src/lib/manager/StatusChannelManager.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as redisClass from 'redis'; 3 | 4 | 5 | const DEFAULT_PREFIX = 'PINUS:CHANNEL'; 6 | const STATUS_PREFIX = 'PINUS:STATUS'; 7 | 8 | export interface PinusGlobalChannelStatusOptions { 9 | // channel 前缀 10 | channelPrefix?: string; 11 | // status 前缀 12 | statusPrefix?: string; 13 | auth_pass?: string; 14 | password?: string; 15 | // 启动时候清除 建议对 connector 设置成启动时清除 16 | cleanOnStartUp?: boolean; 17 | 18 | // 兼容老的redis配置 19 | host?: string; 20 | port?: number; 21 | db?: number; 22 | 23 | // 新的redis配置 24 | url?: string 25 | username?: string; 26 | name?: string; 27 | database?: number; 28 | // 其它配置 参考 redis client https://github.com/redis/node-redis 29 | } 30 | 31 | 32 | export abstract class StatusChannelManager { 33 | protected readonly prefix: string; 34 | private readonly statusPrefix: string; 35 | private readonly statusPrefixKeys: string; 36 | protected redisClient: redisClass.RedisClientType = null; 37 | 38 | protected constructor(protected app: any, protected opts: PinusGlobalChannelStatusOptions = {} as any) { 39 | this.prefix = opts.channelPrefix || DEFAULT_PREFIX; 40 | this.statusPrefix = opts.statusPrefix || STATUS_PREFIX; 41 | this.statusPrefixKeys = this.statusPrefix + ":*"; 42 | if (this.opts.auth_pass) { 43 | this.opts.password = this.opts.auth_pass; 44 | delete this.opts.auth_pass; 45 | } 46 | if (this.opts.host && !this.opts.url) { 47 | // 兼容老的redis配置 转换 48 | this.opts.url = `redis://${ this.opts.host }:${ this.opts.port }`; 49 | if(!this.opts.database){ 50 | this.opts.database = this.opts.db; 51 | } 52 | } 53 | } 54 | 55 | public start(): Promise { 56 | return new Promise((resolve, reject) => { 57 | const redisClient = redisClass.createClient(this.opts); 58 | redisClient.connect(); 59 | redisClient.on('error', err => { 60 | console.error('redis error', err); 61 | // throw new Error(`[globalChannel][redis errorEvent]err:${err.stack}`); 62 | return reject(`[globalChannel][redis errorEvent]err:${ err.stack }`); 63 | }); 64 | 65 | redisClient.on('ready', err => { 66 | if (err) { 67 | console.error('redis ready error', err); 68 | return reject(`[globalChannel][redis readyEvents]err:${ err.stack }`); 69 | } 70 | console.log('redis create success'); 71 | this.redisClient = redisClient; 72 | return resolve(); 73 | }); 74 | }); 75 | } 76 | 77 | public async stop(force = true) { 78 | if (this.redisClient) { 79 | // this.redisClient.quit(); 80 | await this.redisClient.quit(); 81 | this.redisClient = null; 82 | return true; 83 | } 84 | return true; 85 | } 86 | 87 | public async clean() { 88 | let cleanKey = StatusChannelManager.GenCleanKey(this.prefix); 89 | let result = await this.redisClient.keys(cleanKey); 90 | let multi = this.redisClient.multi(); 91 | if (Array.isArray(result) && result.length > 0) { 92 | console.log("clean channel", result) 93 | for (const value of result) { 94 | multi.del(value); 95 | } 96 | } 97 | cleanKey = StatusChannelManager.GenCleanKey(this.statusPrefix); 98 | result = await this.redisClient.keys(cleanKey); 99 | if (Array.isArray(result) && result.length > 0) { 100 | console.log("clean status", result) 101 | for (const value of result) { 102 | multi.del(value); 103 | } 104 | } 105 | return multi.exec(); 106 | } 107 | 108 | public async flushall(): Promise { 109 | return await this.redisClient.flushAll(); 110 | } 111 | 112 | public async statusCount(): Promise { 113 | let result = await this.redisClient.eval(`local ks = redis.call('keys',KEYS[1]);return #ks;`,{keys: [this.statusPrefixKeys]}); 114 | return Number(result); 115 | } 116 | 117 | public async add(uid: string, sid: string): Promise { 118 | const genKey = StatusChannelManager.GenKey(this.statusPrefix, uid); 119 | return this.redisClient.sAdd(genKey, sid); 120 | } 121 | 122 | public async leave(uid: string, sid: string): Promise { 123 | const genKey = StatusChannelManager.GenKey(this.statusPrefix, uid); 124 | return await this.redisClient.sRem(genKey, sid); 125 | } 126 | 127 | public async getSidsByUid(uid: string): Promise { 128 | const genKey = StatusChannelManager.GenKey(this.statusPrefix, uid); 129 | return await this.redisClient.sMembers(genKey); 130 | } 131 | 132 | public async getSidsByUidArr(uidArr: string[]): Promise<{ [uid: string]: string[] }> { 133 | if (!uidArr || !uidArr.length) { 134 | return null; 135 | } 136 | let multi = this.redisClient.multi(); 137 | // uidArr = [...new Set(uidArr)]; 138 | for (const uid of uidArr) { 139 | multi.sMembers(StatusChannelManager.GenKey(this.statusPrefix, uid)); 140 | } 141 | const uidObjectArr = await multi.exec(); 142 | const uidObject = {}; 143 | for (let i = 0; i < uidArr.length; i++) { 144 | uidObject[uidArr[i]] = uidObjectArr[i]; 145 | } 146 | return uidObject; 147 | } 148 | 149 | protected static GenKey(prefix: string, id: string, channelName: string = null) { 150 | let genKey = ''; 151 | if (channelName === null) 152 | genKey = `${ prefix }:${ id }`; 153 | else 154 | genKey = `${ prefix }:${ channelName }:${ id }`; 155 | return genKey; 156 | } 157 | 158 | protected static GenCleanKey(prefix) { 159 | return `${ prefix }*`; 160 | } 161 | } 162 | 163 | -------------------------------------------------------------------------------- /src/lib/service/GlobalChannelServiceStatus.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import utils from '../util/Utils'; 3 | import * as util from 'util'; 4 | 5 | import DefaultChannelManager from '../manager/RedisGlobalChannelManager'; 6 | import { PinusGlobalChannelStatusOptions } from "../manager/StatusChannelManager"; 7 | 8 | const ST_INITED = 0; 9 | const ST_STARTED = 1; 10 | const ST_CLOSED = 2; 11 | 12 | /** 13 | * Global channel service. 14 | * GlobalChannelService is created by globalChannel component which is a default 15 | * component of pomelo enabled by `app.set('globalChannelConfig', {...})` 16 | * and global channel service would be accessed by 17 | * `app.get('globalChannelService')`. 18 | * @class 19 | */ 20 | export class GlobalChannelServiceStatus { 21 | public static PLUGIN_NAME = 'globalChannelServiceStatus'; 22 | private manager: DefaultChannelManager; 23 | private cleanOnStartUp: boolean; 24 | private state: number = ST_INITED; 25 | public name: string = '__globalChannelStatus__'; 26 | private RpcInvokePromise = null; 27 | 28 | /** 29 | * 构造函数 30 | * @param {*} app pomelo instance 31 | * @param {Object} opts 参数列表 32 | */ 33 | constructor(private app, private opts: PinusGlobalChannelStatusOptions = {}) { 34 | if (app.getServerType() == 'master') { 35 | return 36 | } 37 | this.manager = GetChannelManager(app, opts); 38 | this.cleanOnStartUp = opts.cleanOnStartUp; 39 | // app.rpcInvoke 是 bind了rpcClient this的rpcInvoke 40 | app.set(GlobalChannelServiceStatus.PLUGIN_NAME, this, true); 41 | } 42 | 43 | /** 44 | * Component lifecycle callback 45 | * 46 | * @param {Function} cb 47 | * @return {Void} 48 | */ 49 | afterStart(cb ?: (err?: Error) => void) { 50 | if (this.app.getServerType() == 'master') { 51 | process.nextTick(() => { 52 | utils.InvokeCallback(cb); 53 | }); 54 | return 55 | } 56 | if (this.app.components && this.app.components.__proxy__ && 57 | this.app.components.__proxy__.client && this.app.components.__proxy__.client.rpcInvoke) { 58 | this.RpcInvokePromise = util.promisify(this.app.components.__proxy__.client.rpcInvoke.bind(this.app.components.__proxy__.client)); 59 | } 60 | cb(); 61 | }; 62 | 63 | afterStartAll() { 64 | if (this.app.getServerType() == 'master') { 65 | return 66 | } 67 | if (!this.RpcInvokePromise) { 68 | this.RpcInvokePromise = util.promisify(this.app.rpcInvoke); 69 | } 70 | } 71 | 72 | /** 73 | * TODO:发送消息给指定服务器 中的某一些人 74 | * @param {String} route route string 75 | * @param {Array} uids userId array 76 | * @param {Object} msg 消息内容 77 | * @param {String} serverType frontend server type 78 | * @param {String} frontServerId frontend server Id 79 | * @returns {Array} send message fail userList 80 | */ 81 | async pushMessageForUid(route: string, msg: any, uids: string[], serverType: string, frontServerId: string) { 82 | if (this.state !== ST_STARTED) { 83 | throw new Error('invalid state'); 84 | } 85 | if (!uids || uids.length === 0) { 86 | return []; 87 | } 88 | const servers = this.app.getServersByType(serverType); 89 | 90 | if (!servers || servers.length === 0) { 91 | // no frontend server infos 92 | return []; 93 | } 94 | return await RpcInvoke(this.RpcInvokePromise, frontServerId, route, msg, uids); 95 | } 96 | 97 | /** 98 | * 群发消息给指定的玩家 99 | * @param {string[]} uidArr 100 | * @param {string} route 101 | * @param msg 102 | * @returns {Promise} 失败的玩家id数组 103 | */ 104 | async pushMessageByUids(uidArr: string[], route: string, msg: any): Promise { 105 | if (!uidArr || !uidArr.length) throw new Error('userId List is null'); 106 | if (this.state !== ST_STARTED) { 107 | throw new Error('invalid state'); 108 | } 109 | 110 | const uidObject = await this.getSidsByUidArr(uidArr); 111 | const records: { [serverid: string]: string[] } = {}; 112 | const keysArr = Object.keys(uidObject); 113 | for (let i = 0; i < keysArr.length; i++) { 114 | const uid = keysArr[i]; 115 | const sids = uidObject[uid]; 116 | let uidSet: string[] = null; 117 | if (sids && sids.length) { 118 | for (const serverId of sids) { 119 | if (records[serverId]) { 120 | uidSet = records[serverId]; 121 | } else { 122 | uidSet = []; 123 | records[serverId] = uidSet; 124 | } 125 | uidSet.push(uid); 126 | } 127 | } 128 | } 129 | 130 | const sendMessageArr = []; 131 | for (const sid in records) { 132 | const uidSet = records[sid]; 133 | if (uidSet.length) { 134 | sendMessageArr.push(RpcInvoke(this.RpcInvokePromise, sid, route, msg, uidSet)); 135 | } 136 | } 137 | if (sendMessageArr.length > 0) { 138 | const results = await Promise.all(sendMessageArr); 139 | return [].concat(...results); 140 | } 141 | return null; 142 | } 143 | 144 | /** 145 | * 发送消息给指定 channelName 的所有玩家 146 | * @param {string} serverType 一般是 ‘connector’ frontendServer 147 | * @param {string} route push 路由 148 | * @param msg 149 | * @param {string | string[]} channelName 150 | * @returns {Promise} 返回失败的 id玩家id数组 151 | */ 152 | public async pushMessageByChannelName(serverType: string, route: string, msg: any, channelName: string | string[]): Promise { 153 | if (this.state !== ST_STARTED) { 154 | throw new Error('invalid state'); 155 | } 156 | const servers = this.app.getServersByType(serverType); 157 | if (!servers || servers.length === 0) { 158 | return []; 159 | } 160 | const uidObject = await this.getMembersByChannelName(serverType, channelName); 161 | const sendMessageArr = []; 162 | for (let sid in uidObject) { 163 | const channels = uidObject[sid]; 164 | let uids: string[] = []; 165 | for (let channelName in channels) { 166 | let cUids = channels[channelName]; 167 | uids = uids.concat(cUids); 168 | } 169 | if (uids && uids.length) { 170 | sendMessageArr.push(RpcInvoke(this.RpcInvokePromise, sid, route, msg, uids)); 171 | } 172 | } 173 | if (sendMessageArr.length > 0) { 174 | let failIds = await Promise.all(sendMessageArr); 175 | return [].concat(...failIds); 176 | } 177 | return []; 178 | } 179 | 180 | /** 181 | * 获取指定 channelName 和 服务器类型的成员 182 | * @param {string} serverType 183 | * @param {string | string[]} channelName 184 | * @returns {Promise<{[p: string]: {[p: string]: string[]}}>} 185 | */ 186 | public async getMembersByChannelName(serverType: string, channelName: string | string[]): Promise<{ [serverid: string]: { [channelName: string]: string[] } }> { 187 | if (this.state !== ST_STARTED) { 188 | throw new Error('invalid state'); 189 | } 190 | return await this.manager.getMembersByChannelName(serverType, channelName); 191 | } 192 | 193 | /** 194 | * 获取指定服务器和channelName 的玩家列表 195 | * @param channelName 196 | * @param sid 197 | * @returns {Promise} 198 | */ 199 | public async getMembersBySid(channelName: string, sid: string): Promise { 200 | if (this.state !== ST_STARTED) { 201 | throw new Error('invalid state'); 202 | } 203 | return await this.manager.getMembersBySid(channelName, sid); 204 | } 205 | 206 | /** 207 | * 获得指定玩家在所在的服务器 208 | * @param uid 209 | * @returns {Promise} 210 | */ 211 | public async getSidsByUid(uid: string): Promise { 212 | if (this.state !== ST_STARTED) { 213 | throw new Error('invalid state'); 214 | } 215 | return await this.manager.getSidsByUid(uid); 216 | } 217 | 218 | /** 219 | * 获取指定玩家 addStatus的服务器 220 | * @param uidArr 221 | * @returns {Promise<{[uid: string]: string[]}>} 222 | */ 223 | public async getSidsByUidArr(uidArr: string[]): Promise<{ [uid: string]: string[] }> { 224 | if (this.state !== ST_STARTED) { 225 | throw new Error('invalid state'); 226 | } 227 | return await this.manager.getSidsByUidArr(uidArr); 228 | } 229 | 230 | start(cb) { 231 | if (this.app.getServerType() == 'master') { 232 | process.nextTick(() => { 233 | utils.InvokeCallback(cb); 234 | }); 235 | return 236 | } 237 | if (process.env.NODE_ENV == 'ci') { 238 | this.afterStartAll(); 239 | } 240 | if (this.state !== ST_INITED) { 241 | utils.InvokeCallback(cb, new Error('invalid state')); 242 | return; 243 | } 244 | if (typeof this.manager.start === 'function') { 245 | this.manager.start() 246 | .then(result => { 247 | this.state = ST_STARTED; 248 | if (this.cleanOnStartUp) { 249 | this.manager.clean() 250 | .then(result => { 251 | utils.InvokeCallback(cb, null); 252 | }) 253 | .catch(err => { 254 | utils.InvokeCallback(cb, err); 255 | }); 256 | } else { 257 | utils.InvokeCallback(cb, null); 258 | } 259 | return null; 260 | }) 261 | .catch(err => { 262 | utils.InvokeCallback(cb, err); 263 | }); 264 | } else { 265 | process.nextTick(() => { 266 | utils.InvokeCallback(cb); 267 | }); 268 | } 269 | } 270 | 271 | stop(force, cb) { 272 | if (this.app.getServerType() == 'master') { 273 | process.nextTick(() => { 274 | utils.InvokeCallback(cb); 275 | }); 276 | return 277 | } 278 | this.state = ST_CLOSED; 279 | if (typeof this.manager.stop === 'function') { 280 | this.manager.stop(force) 281 | .then(result => { 282 | utils.InvokeCallback(cb, result); 283 | }) 284 | .catch(err => { 285 | utils.InvokeCallback(cb, err); 286 | }); 287 | } else { 288 | process.nextTick(() => { 289 | utils.InvokeCallback(cb); 290 | }); 291 | } 292 | } 293 | 294 | /** 295 | * Destroy global channel or channels. 296 | */ 297 | public async destroyChannel(channelName: string | string[]): Promise { 298 | if (!channelName || channelName.length <= 0) return; 299 | if (this.state !== ST_STARTED) { 300 | throw new Error('invalid state'); 301 | } 302 | return this.manager.destroyChannel(channelName); 303 | } 304 | 305 | /** 306 | * TODOx:添加一个玩家 到指定channelName 307 | * Add a member into channel. 308 | * @param {String} uid user id 309 | * @param {String} sid frontend server id 310 | * @param {String | Array} channelName 指定的 channelName 311 | * @returns {number} is add: 1 add success, 0 fail 312 | */ 313 | public async add(uid: string, sid: string, channelName: string | string[]): Promise { 314 | if (this.state !== ST_STARTED) { 315 | throw new Error('invalid state'); 316 | } 317 | return this.manager.add(uid, sid, channelName); 318 | } 319 | 320 | /** 321 | * 获取 status 数量 322 | */ 323 | public async getCountStatus() { 324 | return this.manager.statusCount() 325 | } 326 | 327 | /** 328 | * 加入 status 329 | * @param {string} uid 330 | * @param {string} sid 331 | * @returns {Promise} 332 | */ 333 | public async addStatus(uid: string, sid: string): Promise { 334 | if (this.state !== ST_STARTED) { 335 | throw new Error('invalid state'); 336 | } 337 | return this.manager.add(uid, sid); 338 | } 339 | 340 | /** 341 | * 指定channel 移除一个玩家 342 | * @param uid 343 | * @param sid 344 | * @param {string | string[]} channelName 345 | * @returns {Promise} 346 | */ 347 | public async leave(uid, sid, channelName: string | string[]): Promise { 348 | if (this.state !== ST_STARTED) { 349 | throw new Error('invalid state'); 350 | } 351 | return this.manager.leave(uid, sid, channelName); 352 | } 353 | 354 | /** 355 | * 离开 status 一般下线调用 356 | * @param {string} uid 357 | * @param {string} sid 358 | * @returns {Promise} 359 | */ 360 | public async leaveStatus(uid: string, sid: string): Promise { 361 | if (this.state !== ST_STARTED) { 362 | throw new Error('invalid state'); 363 | } 364 | return this.manager.leave(uid, sid); 365 | } 366 | 367 | /** 368 | * 通过 服务器ID(sid) 和 指定的 channel 获取玩家列表 369 | * @param {string} sid 370 | * @param {string | string[]} channelName 371 | * @returns {Promise<{[channelName: string]: string[]}>} key是 channelname example: { channelName1: [ 'uuid_18', 'uuid_3' ] } 372 | */ 373 | public async getMembersByChannelNameAndSid(sid: string, channelName: string | string[]): Promise<{ [channelName: string]: string[] }> { 374 | if (this.state !== ST_STARTED) { 375 | throw new Error('invalid state'); 376 | } 377 | return this.manager.getMembersByChannelNameAndSid(sid, channelName); 378 | } 379 | } 380 | 381 | /** 382 | * 383 | * @param app 384 | * @param opts 385 | * @returns {*} 386 | */ 387 | function GetChannelManager(app, opts) { 388 | let channelManager; 389 | if (typeof opts.channelManager === 'function') { 390 | try { 391 | channelManager = opts.channelManager(app, opts); 392 | } catch (err) { 393 | channelManager = new opts.channelManager(app, opts); 394 | } 395 | } else { 396 | channelManager = opts.channelManager; 397 | } 398 | 399 | if (!channelManager) { 400 | channelManager = new DefaultChannelManager(app, opts); 401 | } 402 | return channelManager; 403 | } 404 | 405 | async function RpcInvoke(RpcInvokePromise, sid: string, route: string, msg: any, sendUids: string[]) { 406 | return await RpcInvokePromise(sid, 407 | { 408 | namespace: 'sys', 409 | service: 'channelRemote', 410 | method: 'pushMessage', 411 | args: [route, msg, sendUids, { isPush: true }] 412 | }); 413 | } -------------------------------------------------------------------------------- /src/lib/util/Utils.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | export default class Utils 3 | { 4 | /** 5 | * Invoke callback with check 6 | * @param cb 7 | * @param args 8 | * @private 9 | */ 10 | static InvokeCallback(cb, ...args) 11 | { 12 | if (cb && typeof cb === 'function') 13 | { 14 | const arg = Array.from ? Array.from(args) : [].slice.call(args); 15 | cb(...arg); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/tests/config/redisConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by frank on 16-11-4. 3 | */ 4 | 5 | module.exports.redisChannel = 6 | { 7 | family: 4, // 4 (IPv4) or 6 (IPv6) 8 | options: {}, 9 | host: '192.168.1.10', 10 | port: 6379, 11 | db: 9 // optinal, from 0 to 15 with default redis configure 12 | }; -------------------------------------------------------------------------------- /src/tests/simple.test.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import { default as GlobalChannelManager } from '../lib/manager/RedisGlobalChannelManager'; 3 | import { GlobalChannelServiceStatus } from "../lib/service/GlobalChannelServiceStatus"; 4 | 5 | const config = require('./config/redisConfig').redisChannel; 6 | 7 | // const test = require('ava').test; 8 | 9 | 10 | const serverType = 'connector'; 11 | const serverId = ['connector_1', 'connector_2', 'connector_3']; 12 | const serverData = [{ id: 'connector_1' }, { id: 'connector_2' }, { id: 'connector_3' }]; 13 | const channelName = ['channelName1', 'channelName2', 'channelName3']; 14 | const serversValue = { 'connector_1': serverData[0], 'connector_2': serverData[1], 'connector_3': serverData[2] }; 15 | 16 | const app: any = { 17 | getServersByType(serverType) { 18 | return serverData; 19 | }, 20 | getServerType() { 21 | return ""; 22 | }, 23 | rpcInvoke(serverId: string, msg: object, cb: Function) { 24 | console.log('app.rpcInvoke', serverId, msg); 25 | cb(null, []) 26 | }, 27 | isFrontend(server) { 28 | return true; 29 | }, 30 | getServers() { 31 | return serversValue; 32 | }, 33 | set() { 34 | } 35 | }; 36 | config.channelPrefix = 'TEST:CHANNEL'; 37 | config.statusPrefix = 'TEST:STATUS'; 38 | config.cleanOnStartUp = true; 39 | if (process.env.NODE_ENV == 'ci') { 40 | config.host = '127.0.0.1'; 41 | config.port = 6379; 42 | } else if (process.env.NODE_ENV == 'gitlab') { 43 | config.host = 'redis'; 44 | config.port = 6379; 45 | } 46 | console.log('test service config', config); 47 | const globalChannel = new GlobalChannelServiceStatus(app, config); 48 | const redisManager: GlobalChannelManager = (globalChannel as any).manager; 49 | 50 | class Test { 51 | 52 | static async before() { 53 | await new Promise(resolve => { 54 | globalChannel.start(resolve); 55 | }); 56 | await redisManager.clean(); 57 | } 58 | 59 | static async after() { 60 | // await redisManager.clean(); 61 | // await redisManager.stop(); 62 | await new Promise(resolve => { 63 | globalChannel.stop(true, resolve); 64 | }); 65 | } 66 | 67 | static async add() { 68 | const coArr = []; 69 | for (let i = 0; i < 50; i++) { 70 | const index = Test.random(0, serverId.length - 1); 71 | coArr.push(redisManager.add(`uuid_${i}`, serverId[index], channelName[i % 3])); 72 | } 73 | const result = await Promise.all(coArr); 74 | console.info('test.add', JSON.stringify(result)); 75 | } 76 | 77 | 78 | static random(min, max) { 79 | return min + Math.floor(Math.random() * (max - min + 1)); 80 | } 81 | 82 | static async leave() { 83 | const coArr = []; 84 | for (const id of serverId) { 85 | coArr.push(redisManager.leave('uuid_1', id, channelName)); 86 | } 87 | const result = await Promise.all(coArr); 88 | console.info('test.leave', result); 89 | } 90 | 91 | 92 | static async addNoChannel() { 93 | const coArr = []; 94 | for (let i = 0; i < 10; i++) { 95 | const index = Test.random(0, serverId.length - 1); 96 | coArr.push(redisManager.add(`uuid_${i}`, serverId[index])); 97 | } 98 | const result = await Promise.all(coArr); 99 | console.info('test.addNoChannel', result); 100 | } 101 | 102 | 103 | static async leaveNoChannel() { 104 | const coArr = []; 105 | for (const id of serverId) { 106 | coArr.push(redisManager.leave('uuid_3', id)); 107 | } 108 | const result = await Promise.all(coArr); 109 | console.info('leaveNoChannel', result); 110 | } 111 | 112 | } 113 | 114 | //Test.test(); 115 | // Test.globalService(); 116 | 117 | describe('test channel', () => { 118 | beforeAll((done) => { 119 | let f = async () => { 120 | await Test.before(); 121 | globalChannel.afterStartAll(); 122 | await Test.add(); 123 | console.log('before all success'); 124 | done() 125 | }; 126 | f() 127 | }, 3000); 128 | 129 | afterAll(async () => { 130 | console.log('clean test data'); 131 | await Test.after(); 132 | }); 133 | 134 | test('test globalService', async () => { 135 | let count = await globalChannel.getCountStatus(); 136 | console.log(' status count:', count); 137 | expect(count).toBe(0); 138 | await Test.addNoChannel(); 139 | count = await globalChannel.getCountStatus(); 140 | console.log(' status count:', count); 141 | expect(count).toBe(10); 142 | const members = await redisManager.getSidsByUid('uuid_3'); 143 | console.info('test.getSidsByUid', members); 144 | 145 | const c1members = await redisManager.getSidsByUidArr(['uuid_10', 'uuid_3', 'uuid_0']); 146 | console.info('test.getSidsByUidArr c1members', c1members); 147 | expect(c1members['uuid_3'][0]).toBe(members[0]); 148 | 149 | await Test.leaveNoChannel(); 150 | 151 | const c2members = await redisManager.getSidsByUidArr(['uuid_10', 'uuid_3', 'uuid_0']); 152 | console.info('test.getSidsByUidArr c2members', c2members); 153 | expect(!!c2members['uuid_3'][0]).toBeFalsy(); 154 | expect(c2members['uuid_0'][0]).toBe(c1members['uuid_0'][0]); 155 | 156 | }); 157 | 158 | test('getMembersBySid', async () => { 159 | const index = Test.random(0, serverId.length - 1); 160 | const members = await redisManager.getMembersBySid(channelName[0], serverId[index]); 161 | console.info('getMembersBySid', members); 162 | }); 163 | 164 | test('getMembersByChannelNameAndSid', async () => { 165 | const members = await redisManager.getMembersByChannelNameAndSid('connector_1', channelName[0]); 166 | console.info('test.getMembersByChannelNameAndSid', members); 167 | const c2members = await redisManager.getMembersByChannelNameAndSid('connector_1', channelName); 168 | console.info('test.getMembersByChannelNameAndSid c2members', c2members); 169 | expect(c2members[channelName[0]]).toMatchObject(members[channelName[0]]); 170 | }); 171 | 172 | test('getMembersByChannel and leave uuid_1', async () => { 173 | const mock = jest.spyOn(app as any, 'getServersByType').mockImplementation((val) => val); 174 | const members = await redisManager.getMembersByChannelName(serverData as any, channelName); 175 | console.info('test.getMembersByChannel', members); 176 | const c1members = await redisManager.getMembersByChannelName(serverData as any, channelName[0]); 177 | console.info('test.getMembersByChannel c1members', c1members); 178 | expect(c1members[serverId[0]][channelName[0]]).toMatchObject(members[serverId[0]][channelName[0]]); 179 | 180 | const c2members = await redisManager.getMembersByChannelName(serverData as any, channelName[1]); 181 | expect(c2members[serverId[0]][channelName[1]]).toMatchObject(members[serverId[0]][channelName[1]]); 182 | 183 | await Test.leave(); 184 | const c3members = await redisManager.getMembersByChannelName(serverData as any, channelName); 185 | console.info('test.getMembersByChannel c3members', c3members); 186 | expect(JSON.stringify(c3members).indexOf('"uuid_1"') == -1).toBeTruthy(); 187 | expect(JSON.stringify(c3members).indexOf('"uuid_2"') == -1).toBeFalsy(); 188 | }); 189 | 190 | // uuid_3 uuid_1 不要用来测试 191 | describe('test global channel service', () => { 192 | 193 | it('test global service', async () => { 194 | console.log('!! test global service'); 195 | let val = await globalChannel.leaveStatus('vvvv', 'whatthefuck'); 196 | expect(val).toBe(0); 197 | val = await globalChannel.addStatus('vvvv', 'whatthefuck'); 198 | expect(val).toBe(1); 199 | val = await globalChannel.addStatus('vvvv', 'whatthefuck'); 200 | expect(val).toBe(0); 201 | val = await globalChannel.leaveStatus('vvvv', 'whatthefuck'); 202 | expect(val).toBe(1); 203 | }); 204 | 205 | 206 | it('test getMembersByChannelNameAndSid', async () => { 207 | let val = await globalChannel.getMembersByChannelNameAndSid(serverId[0], channelName); 208 | expect(Object.keys(val)).toMatchObject(channelName); 209 | expect(val[channelName[0]].length).toBeGreaterThan(0); 210 | 211 | val = await globalChannel.getMembersByChannelNameAndSid('noid', channelName); 212 | console.log('getMembersByChannelNameAndSid val:', val); 213 | expect(Object.keys(val)).toMatchObject(channelName); 214 | expect([]).toMatchObject(val[channelName[0]]); 215 | }); 216 | 217 | it('test destroyChannel', async () => { 218 | let val = await globalChannel.destroyChannel(channelName[0]); 219 | console.log('destroy val:', val); 220 | expect(val.length).toBe(3); 221 | expect(val).toMatchObject([1, 1, 1]); 222 | val = await globalChannel.destroyChannel(channelName); 223 | console.log('destroy val22:', val); 224 | await Test.add(); 225 | }); 226 | 227 | it('test add and leave channel', async () => { 228 | let val = await globalChannel.add('myid', serverId[0], channelName[0]); 229 | console.log('!!! myid val:', val); 230 | expect(val).toBe(1); 231 | val = await globalChannel.add('myid', serverId[0], channelName[0]); 232 | 233 | expect(val).toBe(0); 234 | val = await globalChannel.leave('myid1', serverId[0], channelName[0]); 235 | expect(val).toBe(0); 236 | val = await globalChannel.leave('myid', serverId[0], channelName[0]); 237 | expect(val).toBe(1); 238 | 239 | val = await globalChannel.add('myid', serverId[0], channelName); 240 | console.log('!!! myid add channels val:', val); 241 | expect(val).toMatchObject([1, 1, 1]); 242 | val = await globalChannel.add('myid', serverId[0], channelName); 243 | console.log('!!! myid add channels val again:', val); 244 | expect(val).toMatchObject([0, 0, 0]); 245 | 246 | val = await globalChannel.leave('myid', serverId[0], channelName); 247 | console.log('!!! myid leave channels val:', val); 248 | expect(val).toMatchObject([1, 1, 1]); 249 | }); 250 | 251 | it('test service getSidsByUidArr', async () => { 252 | let val = await globalChannel.getSidsByUidArr(['?id']); 253 | 254 | expect(!!val).toBeTruthy(); 255 | expect(val['?id'].length).toBe(0); 256 | 257 | let addVal = await globalChannel.addStatus('?id', 'testserver'); 258 | expect(addVal).toBe(1); 259 | 260 | val = await globalChannel.getSidsByUidArr(['?id']); 261 | expect(val['?id'].length).toBe(1); 262 | expect(val['?id'][0]).toBe('testserver'); 263 | console.log('testservice getSidsByUidArr', val); 264 | addVal = await globalChannel.leaveStatus('?id', 'testserver'); 265 | expect(addVal).toBe(1); 266 | 267 | }); 268 | 269 | 270 | it('test service getSidsByUid', async () => { 271 | let val = await globalChannel.getSidsByUid('!!id'); 272 | expect(val).toMatchObject([]); 273 | let nVal = await globalChannel.addStatus('!!id', 'ss1'); 274 | expect(nVal).toBe(1); 275 | 276 | val = await globalChannel.getSidsByUid('!!id'); 277 | expect(val).toMatchObject(['ss1']); 278 | 279 | nVal = await globalChannel.addStatus('!!id', 'ss2'); 280 | expect(nVal).toBe(1); 281 | 282 | val = await globalChannel.getSidsByUid('!!id'); 283 | expect(val.sort()).toMatchObject(['ss2', 'ss1'].sort()); 284 | 285 | nVal = await globalChannel.leaveStatus('!!id', 'ss1'); 286 | expect(nVal).toBe(1); 287 | 288 | 289 | }); 290 | 291 | 292 | it('test service getMembersBySid ,add channel ,leave channel', async () => { 293 | let val = await globalChannel.getMembersBySid(channelName[0], 'none'); 294 | expect(val).toMatchObject([]); 295 | let nVal = await globalChannel.add('__testGetSid', 'none', channelName[0]); 296 | expect(nVal).toBe(1); 297 | val = await globalChannel.getMembersBySid(channelName[0], 'none'); 298 | expect(val).toMatchObject(['__testGetSid']); 299 | 300 | nVal = await globalChannel.leave('__testGetSid', 'none', channelName[0]); 301 | expect(nVal).toBe(1); 302 | 303 | }); 304 | 305 | it('test service getMembersByChannelName', async () => { 306 | let val = await globalChannel.getMembersByChannelName('unused', 'failedChannel'); 307 | console.log('!! getMembersByChannelName val', val); 308 | expect(Object.keys(val)).toMatchObject(serverId); 309 | expect(val).toMatchObject({ 310 | connector_1: { failedChannel: [] }, 311 | connector_2: { failedChannel: [] }, 312 | connector_3: { failedChannel: [] } 313 | }); 314 | 315 | await globalChannel.add('@2uid', serverId[0], '@2channel'); 316 | val = await globalChannel.getMembersByChannelName('unused', ['@2channel', '@222channel']); 317 | console.log('@2!! getMembersByChannelName val', val); 318 | expect(Object.keys(val)).toMatchObject(serverId); 319 | 320 | expect(val).toMatchObject({ 321 | connector_1: { '@2channel': ['@2uid'], '@222channel': [] }, 322 | connector_2: { '@2channel': [], '@222channel': [] }, 323 | connector_3: { '@2channel': [], '@222channel': [] } 324 | }); 325 | 326 | 327 | }); 328 | 329 | it('test service pushMessageByChannelName', async () => { 330 | const mockRpc = jest.spyOn(globalChannel as any, 'RpcInvokePromise',); 331 | let failedArr = []; 332 | (mockRpc as any).mockImplementation(async (serverId: string, msg: object) => { 333 | console.log('mock app.rpcInvoke', serverId, msg); 334 | if (serverId == 'connector_1') { 335 | return failedArr = msg['args'][2]; 336 | } 337 | return [] 338 | }); 339 | let val = await globalChannel.pushMessageByChannelName('unused', 'testRoute', 'testMsg', channelName[2]); 340 | console.log('test pushMessageByChannelName val:', val); 341 | expect(!!val).toBeTruthy(); 342 | expect(val.length).toBeGreaterThan(0); 343 | let members = await globalChannel.getMembersByChannelName('unused', channelName[2]); 344 | let strVal = JSON.stringify(members); 345 | for (let uid of val) { 346 | expect(strVal.indexOf(uid)).toBeGreaterThan(0); 347 | } 348 | 349 | val = await globalChannel.pushMessageByChannelName('unused', 'testRoute', 'testMsg', channelName); 350 | console.log('@@2test pushMessageByChannelName val:', val); 351 | expect(!!val).toBeTruthy(); 352 | expect(val.length).toBeGreaterThan(0); 353 | expect(val).toMatchObject(failedArr); 354 | }); 355 | 356 | it('test service pushMessageByUids', async () => { 357 | await redisManager.clean(); 358 | await Test.add(); 359 | let val = await globalChannel.pushMessageByUids(['sss', 'ggg'], 'route pushMessageByUids', 'msg pushMessageByUids'); 360 | expect(val).toBe(null); 361 | 362 | let members = await globalChannel.getMembersBySid(channelName[0], serverId[0]); 363 | for (const uuid of members) { 364 | const ret = await globalChannel.addStatus(uuid, serverId[0]); 365 | if (ret != 1) { 366 | console.error('pushMessageByUids err:', ret, uuid, 'members:', members); 367 | } 368 | expect(ret).toBe(1); 369 | } 370 | val = await globalChannel.pushMessageByUids(members, 'route pushMessageByUids', 'msg pushMessageByUids'); 371 | console.log('!! pushMessageByUids val', val, members); 372 | expect(val).toMatchObject([]); 373 | 374 | let failedIds = []; 375 | const mockRpc = jest.spyOn(globalChannel as any, 'RpcInvokePromise',); 376 | (mockRpc as any).mockImplementation(async (serverId: string, msg: object) => { 377 | console.log('mock app.rpcInvoke', serverId, msg); 378 | if (serverId == 'connector_1') { 379 | return failedIds = msg['args'][2]; 380 | } 381 | return [] 382 | }); 383 | 384 | val = await globalChannel.pushMessageByUids(members, 'route pushMessageByUids', 'msg pushMessageByUids'); 385 | console.log('@3!! pushMessageByUids val', val, members); 386 | expect(val).toMatchObject(failedIds); 387 | }); 388 | }); 389 | 390 | 391 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // types option has been previously configured 4 | "types": [ 5 | // add node as an option 6 | "node", "jest" 7 | ], 8 | "module": "commonjs", //指定生成哪个模块系统代码 9 | "target": "es2017", 10 | "lib": [ 11 | "es2015", 12 | "es2016", 13 | "es2017", 14 | "esnext.asynciterable" 15 | ], 16 | "noImplicitAny": false, //在表达式和声明上有隐含的'any'类型时报错。 17 | "noImplicitThis": false, 18 | "inlineSourceMap": true, 19 | "skipLibCheck": true, 20 | 21 | 22 | "rootDirs": ["."], //仅用来控制输出的目录结构--outDir。 23 | "outDir":"./dist", //重定向输出目录。 24 | "experimentalDecorators":true, 25 | "emitDecoratorMetadata": true, 26 | "moduleResolution": "node", 27 | "watch":false //在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。 28 | }, 29 | "include":[ 30 | "./src/lib/**/*.ts", 31 | "./src/index.ts" 32 | ], 33 | "exclude": [ 34 | "./dist/**/*.*", 35 | "node_modules", 36 | "./src/tests/**/*.*" 37 | ] 38 | } --------------------------------------------------------------------------------