├── .editorconfig ├── .gitignore ├── .npmignore ├── .nycrc ├── LICENSE ├── README.md ├── configs ├── base.tsconfig.json ├── build.tslint.json ├── errors.tslint.json ├── mocha.opts └── warnings.tslint.json ├── dev-packages └── cli │ ├── package.json │ └── regax ├── docs └── RoadMap.md ├── example-packages └── chat │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ ├── app.ts │ ├── app │ ├── client │ │ └── client.ts │ ├── component │ │ └── my.ts │ ├── filter │ │ └── routerLog.ts │ └── server │ │ ├── chat │ │ ├── controller │ │ │ └── chat.ts │ │ └── rpc │ │ │ └── room.ts │ │ └── connector │ │ └── controller │ │ └── user.ts │ └── config │ ├── config.default.ts │ └── router.ts ├── lerna.json ├── package.json ├── packages ├── client-udpsocket │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── client.ts │ │ ├── index.ts │ │ ├── udpSocket.spec.ts │ │ └── udpSocket.ts ├── client-websocket │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── client.spec.ts │ │ ├── client.ts │ │ ├── index.ts │ │ └── socket.ts ├── common │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── error.ts │ │ ├── event.spec.ts │ │ ├── event.ts │ │ ├── index.ts │ │ ├── promise.ts │ │ ├── tick.ts │ │ ├── types.ts │ │ └── util.ts ├── egg-regax │ ├── agent.js │ ├── app.js │ ├── compile.tsconfig.json │ ├── config │ │ └── config.default.js │ ├── package.json │ └── src │ │ ├── agent.ts │ │ ├── app.spec.ts │ │ ├── app.ts │ │ ├── common │ │ ├── constant.ts │ │ ├── ipc.ts │ │ ├── messenger.ts │ │ └── remoteCache.ts │ │ ├── component │ │ ├── egg.ts │ │ └── eggMaster.ts │ │ ├── config │ │ └── config.default.ts │ │ ├── index.ts │ │ └── typings │ │ └── egg.d.ts ├── logger │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── logger.spec.ts │ │ ├── logger.ts │ │ ├── regax │ │ ├── consoleLogger.ts │ │ ├── customLogger.ts │ │ ├── errorLogger.ts │ │ ├── regaxLogger.ts │ │ └── regaxLoggerManager.ts │ │ └── typings │ │ └── egg-logger.d.ts ├── logrotator │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── component │ │ └── logrotator.ts │ │ ├── config │ │ └── config.default.ts │ │ ├── index.ts │ │ ├── rotator │ │ ├── dayRotator.ts │ │ ├── hourRotator.ts │ │ ├── rotator.ts │ │ └── sizeRotator.ts │ │ └── util.ts ├── protobuf │ ├── README.md │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── protobuf.spec.ts │ │ └── protobuf.ts ├── protocol │ ├── README.md │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── protocol.spec.ts │ │ └── protocol.ts ├── rpc │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── __tests__ │ │ ├── client.spec.ts │ │ ├── registry.spec.ts │ │ ├── server.spec.ts │ │ └── testUtil.ts │ │ ├── client │ │ ├── client.ts │ │ ├── mailboxes │ │ │ ├── index.ts │ │ │ ├── mqttMailbox.ts │ │ │ └── wsMailbox.ts │ │ ├── mailstation.ts │ │ └── router.ts │ │ ├── index.ts │ │ ├── registry │ │ ├── registries │ │ │ ├── localRegistry.ts │ │ │ ├── remoteCacheRegistry.ts │ │ │ └── zookeeperRegistry.ts │ │ └── registry.ts │ │ ├── server │ │ ├── acceptors │ │ │ ├── index.ts │ │ │ ├── mqttAcceptor.ts │ │ │ └── wsAcceptor.ts │ │ └── server.ts │ │ ├── typings │ │ └── mqtt-connection.d.ts │ │ └── util │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── remoteCache.ts │ │ └── tracer.ts ├── scheduler │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ │ ├── component │ │ └── scheduler.ts │ │ ├── config │ │ └── config.default.ts │ │ └── index.ts └── server │ ├── bin │ └── regax │ ├── compile.tsconfig.json │ ├── package.json │ └── src │ ├── __tests__ │ ├── application.spec.ts │ ├── connector │ │ ├── udpConnector.spec.ts │ │ └── udpStickyServer.spec.ts │ ├── filter.spec.ts │ ├── master.spec.ts │ ├── monitor.spec.ts │ ├── plugin.spec.ts │ ├── rpc.spec.ts │ ├── service │ │ └── connection.spec.ts │ ├── template │ │ ├── config │ │ │ ├── config.default.ts │ │ │ └── router.ts │ │ ├── index.ts │ │ └── server │ │ │ ├── chat │ │ │ ├── controller │ │ │ │ └── chat.ts │ │ │ └── rpc │ │ │ │ └── room.ts │ │ │ └── connector │ │ │ └── controller │ │ │ └── user.ts │ └── testUtil.ts │ ├── api │ ├── application.ts │ ├── component.ts │ ├── connector.ts │ ├── controller.ts │ ├── filter.ts │ ├── index.ts │ ├── plugin.ts │ ├── router.ts │ ├── rpc.ts │ ├── session.ts │ └── socket.ts │ ├── application.ts │ ├── component │ ├── agent.ts │ ├── backendSession.ts │ ├── channel.ts │ ├── connection.ts │ ├── connector.ts │ ├── globalChannel.ts │ ├── master.ts │ ├── messenger.ts │ ├── pushScheduler.ts │ ├── router.ts │ ├── rpc.ts │ ├── session.ts │ └── taskManager.ts │ ├── config │ ├── config.default.ts │ ├── config.local.ts │ ├── config.prod.ts │ ├── config.unittest.ts │ └── plugin.ts │ ├── connector │ ├── commands │ │ ├── handshake.ts │ │ ├── heartbeat.ts │ │ ├── index.ts │ │ └── kick.ts │ ├── message.ts │ ├── tcpStickyServer.ts │ ├── udpConnector.ts │ ├── udpSocket.ts │ ├── udpStickyServer.ts │ ├── wsConnector.ts │ └── wsSocket.ts │ ├── filter │ ├── index.ts │ └── timeout.ts │ ├── index.ts │ ├── loader │ ├── index.ts │ ├── loader.ts │ ├── loaders │ │ ├── component.ts │ │ ├── config.ts │ │ ├── context.ts │ │ └── plugin.ts │ └── timing.ts │ ├── master │ ├── childProcess.ts │ ├── gracefulExit.ts │ ├── terminate.ts │ └── workerRegistry.ts │ ├── monitor │ └── monitor.ts │ ├── remote │ ├── backend │ │ └── routeRemote.ts │ ├── frontend │ │ ├── channelRemote.ts │ │ └── sessionRemote.ts │ └── index.ts │ ├── service │ ├── backendSessionService.ts │ ├── channelService.ts │ ├── connectionService.ts │ ├── globalChannelService.ts │ ├── index.ts │ ├── messengerService.ts │ ├── rpcService.ts │ └── sessionService.ts │ └── util │ ├── compose.ts │ ├── fs.ts │ ├── logger.ts │ ├── logo.ts │ ├── proxy.spec.ts │ ├── proxy.ts │ ├── queue.ts │ ├── readFiles.ts │ └── routeUtil.ts ├── tsconfig.json ├── tsfmt.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | end_of_line = lf 6 | indent_style = space 7 | 8 | [*.{js,ts,md}] 9 | indent_size = 2 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | lib 5 | *.log 6 | .idea 7 | .metadata 8 | *.iml 9 | jdt.ls-java-project 10 | lerna-debug.log 11 | .nyc_output 12 | coverage 13 | errorShots 14 | examples/*/src-gen 15 | examples/*/webpack.config.js 16 | .browser_modules 17 | **/docs/api 18 | package-backup.json 19 | .history 20 | .Trash-* 21 | packages/plugin/typedoc 22 | plugins 23 | gh-pages 24 | .vscode/ipch 25 | dev-packages/electron/compile_commands.json 26 | test.ts 27 | test.js 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | *.log 5 | .idea 6 | .metadata 7 | jdt.ls-java-project 8 | lerna-debug.log 9 | .nyc_output 10 | coverage 11 | .browser_modules 12 | download 13 | *ui-spec.ts 14 | *slow-spec.ts 15 | test-resources 16 | __tests__ 17 | *.spec.js 18 | *.spec.ts 19 | *.spec.d.ts 20 | *.spec.js.map 21 | *.spec.ts.map 22 | *.spec.d.ts.map 23 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "include": [ 4 | "src/**/*.ts" 5 | ], 6 | "reporter": [ 7 | "html", 8 | "lcov" 9 | ], 10 | "all": true 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 regax 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 | # Regax 2 | 3 | Regax is a Multiplayer Game Engine for Node.js. 4 | 5 | ## Install 6 | 7 | ```shell 8 | $ tnpm i yarn -g 9 | $ yarn 10 | $ yarn build 11 | $ yarn watch # watch files changed 12 | $ yarn test # run tests 13 | ``` 14 | 15 | ## About 16 | 17 | - [RoadMap](docs/RoadMap.md) 18 | - [Demo Chat](https://github.com/regaxjs/demo-chat) 19 | -------------------------------------------------------------------------------- /configs/base.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "noImplicitAny": true, 8 | "noEmitOnError": false, 9 | "noImplicitThis": true, 10 | "noUnusedLocals": true, 11 | "strictNullChecks": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "downlevelIteration": true, 15 | "resolveJsonModule": true, 16 | "module": "commonjs", 17 | "moduleResolution": "node", 18 | "target": "es2017", 19 | "jsx": "react", 20 | "lib": [ 21 | "es6", 22 | "dom" 23 | ], 24 | "sourceMap": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /configs/build.tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": [ 3 | "Lint rules to use when building. We don't include warnings to avoid polluting the build output." 4 | ], 5 | "extends": [ 6 | "./errors.tslint.json" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /configs/errors.tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "rules": { 4 | "arrow-parens": [ 5 | true, 6 | "ban-single-arg-parens" 7 | ], 8 | "arrow-return-shorthand": [ 9 | false, 10 | "multiline" 11 | ], 12 | "class-name": true, 13 | "comment-format": [ 14 | true, 15 | "check-space" 16 | ], 17 | "curly": false, 18 | "eofline": true, 19 | "file-header": [ 20 | false, 21 | "MIT LICENSE" 22 | ], 23 | "forin": false, 24 | "indent": [ 25 | true, 26 | "spaces", 27 | 2 28 | ], 29 | "interface-over-type-literal": true, 30 | "jsdoc-format": [ 31 | true, 32 | "check-multiline-start" 33 | ], 34 | "max-line-length": [ 35 | true, 36 | 180 37 | ], 38 | "no-any": true, 39 | "no-consecutive-blank-lines": true, 40 | "no-construct": true, 41 | "no-implicit-dependencies": { 42 | "options": [ 43 | true, 44 | "dev", 45 | [ 46 | "expect", 47 | "sinon", 48 | "react", 49 | "react-dom" 50 | ] 51 | ] 52 | }, 53 | "no-magic-numbers": false, 54 | "no-null-keyword": true, 55 | "no-shadowed-variable": true, 56 | "no-string-throw": true, 57 | "no-trailing-whitespace": true, 58 | "no-unused-expression": true, 59 | "no-var-keyword": true, 60 | "no-void-expression": { 61 | "options": [ 62 | "ignore-arrow-function-shorthand" 63 | ] 64 | }, 65 | "one-line": [ 66 | true, 67 | "check-open-brace", 68 | "check-catch", 69 | "check-else", 70 | "check-whitespace" 71 | ], 72 | "one-variable-per-declaration": false, 73 | "prefer-const": [ 74 | true, 75 | { 76 | "destructuring": "all" 77 | } 78 | ], 79 | "quotemark": [ 80 | true, 81 | "single", 82 | "jsx-single", 83 | "avoid-escape", 84 | "avoid-template" 85 | ], 86 | "radix": true, 87 | "semicolon": [ 88 | true, 89 | "never", 90 | "ignore-interfaces" 91 | ], 92 | "space-before-function-paren": [ 93 | true, 94 | { 95 | "anonymous": "always" 96 | } 97 | ], 98 | "trailing-comma": true, 99 | "triple-equals": [ 100 | true, 101 | "allow-null-check" 102 | ], 103 | "typedef": [ 104 | true, 105 | "call-signature", 106 | "property-declaration" 107 | ], 108 | "typedef-whitespace": [ 109 | true, 110 | { 111 | "call-signature": "nospace", 112 | "index-signature": "nospace", 113 | "parameter": "nospace", 114 | "property-declaration": "nospace", 115 | "variable-declaration": "nospace" 116 | } 117 | ], 118 | "variable-name": false, 119 | "whitespace": [ 120 | true, 121 | "check-branch", 122 | "check-decl", 123 | "check-operator", 124 | "check-separator", 125 | "check-type" 126 | ] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /configs/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --require source-map-support/register 3 | --require ts-node/register 4 | --recursive 5 | --watch-extensions ts 6 | --timeout 5000 7 | -------------------------------------------------------------------------------- /configs/warnings.tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "await-promise": { 4 | "severity": "warning", 5 | "options": [ 6 | "Thenable", 7 | "PromiseLike" 8 | ] 9 | }, 10 | "deprecation": true, 11 | "no-return-await": { 12 | "severity": "warning" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dev-packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@regax/cli", 4 | "version": "0.1.4", 5 | "license": "MIT", 6 | "description": "NPM scripts for Regax packages.", 7 | "files": [ 8 | "" 9 | ], 10 | "bin": { 11 | "regax": "regax" 12 | }, 13 | "scripts": { 14 | "ext:clean": "regax compile:clean && regax test:clean", 15 | "ext:build": "concurrently -n compile,lint -c blue,green \"regax compile\" \"regax lint\"", 16 | "ext:compile": "tsc -p compile.tsconfig.json", 17 | "ext:compile:clean": "rimraf lib", 18 | "ext:lint": "tslint --fix -c ../../configs/build.tslint.json --project compile.tsconfig.json", 19 | "ext:watch": "tsc -w -p compile.tsconfig.json", 20 | "ext:test": "cross-env NODE_ENV=unittest nyc mocha --opts ../../configs/mocha.opts \"./src/**/*.*spec.ts\"", 21 | "ext:test:watch": "cross-env NODE_ENV=unittest mocha -w --opts ../../configs/mocha.opts \"./src/**/*.*spec.ts\"", 22 | "ext:test:clean": "rimraf .nyc_output && rimraf coverage" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dev-packages/cli/regax: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | const path = require('path'); 4 | const cp = require('child_process'); 5 | 6 | const extScriptsPck = require(path.resolve(__dirname, 'package.json')); 7 | 8 | function getExtScript() { 9 | const commandIndex = process.argv.findIndex(arg => arg.endsWith('regax')) + 1; 10 | const args = process.argv.slice(commandIndex); 11 | if (!args[0]) { 12 | throw new Error('Please specify the script that runs with regax command.'); 13 | } 14 | const script = 'ext:' + args[0]; 15 | if (!(script in extScriptsPck.scripts)) { 16 | throw new Error('The ext script does not exist: ' + script); 17 | } 18 | return [extScriptsPck.scripts[script], ...args.slice(1, args.length)].join(' '); 19 | } 20 | 21 | function run(script) { 22 | return new Promise((resolve, reject) => { 23 | const env = Object.assign({}, process.env); 24 | const scriptProcess = cp.exec(script, { 25 | env, 26 | cwd: process.cwd() 27 | }); 28 | scriptProcess.stdout.pipe(process.stdout); 29 | scriptProcess.stderr.pipe(process.stderr); 30 | scriptProcess.on('error', reject); 31 | scriptProcess.on('close', resolve); 32 | }); 33 | } 34 | 35 | (async () => { 36 | let exitCode = 0; 37 | let extScript = undefined; 38 | try { 39 | extScript = getExtScript(); 40 | exitCode = await run(extScript); 41 | } catch (err) { 42 | if (extScript) { 43 | console.error(`Error occurred in regax when executing: '${extScript}'`, err); 44 | } else { 45 | console.error('Error occurred in regax', err); 46 | } 47 | console.log(`${err.name}: ${err.message}`); 48 | exitCode = 1; 49 | } 50 | process.exit(exitCode); 51 | })(); 52 | -------------------------------------------------------------------------------- /docs/RoadMap.md: -------------------------------------------------------------------------------- 1 | # Regax实时游戏框架设计 2 | 3 | ## 需求 4 | 5 | 服务于 “多人” “实时” 场景或对战类游戏 6 | 7 | ## 框架设计 8 | 9 | 框架设计参照 [pomelo](https://github.com/NetEase/pomelo),如下图,其中蓝色优先支持: 10 | 11 | ![](https://gw.alipayobjects.com/mdn/rms_1898de/afts/img/A*1NgWSazayioAAAAAAAAAAABkARQnAQ) 12 | 13 | 14 | - 运行架构说明: 15 | 16 | - 客户端通过 Websocket 长连接连到 Connector 服务器群。 17 | - Connector负责承载连接,并把请求转发到后端的服务器群。 18 | - 后端的服务器群负责各自的业务逻辑,真实的案例中会有各种类型的服务器,如自动寻路服务器、AI服务器。 19 | - 后端服务器处理完逻辑后把结果返回给Connector,再由connector广播回给客户端。 20 | - Master负责统一管理这些服务器,包括各服务器的启动、监控和关闭等功能。 21 | 22 | 这个运行架构符合几个伸缩性原则: 23 | 24 | - 前后端进程分离,把承载连接和广播的压力尽量分出去, 且连接服务器群是无状态的, 因为游戏场景中连接服务器往往会成为性能的热点。 25 | - 进程的粒度尽量小,把功能细分到各个服务器 26 | - 连接服务器和业务逻辑服务器做负载均衡,并支持弹性扩容 27 | 28 | ## 游戏服务器和Web服务器 29 | 30 | 在游戏场景中,连接服务器往往会成为性能的热点,这就是是由于巨大的广播消息所导致的。由于广播消息的数量和玩家数量相比是n^2的关系,这就会导致广播消息数量会随着玩家数量急剧增长 31 | 32 | 最初的网络服务器是单进程的架构,所有的逻辑都在单台服务器内完成, 这对于同时在线要求不高的游戏是可以这么做的。由于同时在线人数的上升,单服务器的可伸缩性必然受到挑战。 33 | 34 | 随着网络游戏对可伸缩性要求的增加,分布式是必然的趋势的。 35 | 36 | 游戏服务器的分布式架构与Web服务器是不同的,以下是web服务器与游戏服务器架构的区别: 37 | 38 | - 长连接与短连接。web应用使用基于http的短连接以达到最大的可扩展性,游戏应用采用基于socket(websocket)的长连接,以达到最大的实时性。 39 | - 分区策略不同。web应用的分区可以根据负载均衡自由决定, 而游戏则是基于场景(area)的分区模式, 这使同场景的玩家跑在一个进程内, 以达到最少的跨进程调用。 40 | - 有状态和无状态。web应用是无状态的, 可以达到无限的扩展。 而游戏应用则是有状态的, 由于基于场景的分区策略,它的请求必须路由到指定的服务器, 这也使游戏达不到web应用同样的可扩展性。 41 | 42 | - 广播模式和request/response模式。web应用采用了基于request/response的请求响应模式。而游戏应用则更频繁地使用广播, 由于玩家在游戏里的行动要实时地通知场景中的其它玩家, 必须通过广播的模式实时发送。这也使游戏在网络通信上的要求高于web应用。 43 | 因此, Web应用与游戏应用的运行架构也完全不同, 下图是通常web应用与游戏应用的不同: 44 | 45 | 可以看到由于web服务器的无状态性,只需要通过前端的负载均衡器可以导向任意一个进程。 而游戏服务器是蜘蛛网式的架构,每个进程都有各自的职责,这些进程的交织在一起共同完成一件任务。这就是一个标准的分布式开发架构 46 | 47 | 48 | ## Roadmap 49 | 50 | 51 | ### 一期:完成基础分层架构及通信设计 52 | 53 | - 基础建设 54 | 55 | - [ ] 完成基础分层架构 56 | - [ ] 通信协议:完成客户端/服务端websocket协议支持及Session、Channel基础能力建设 57 | - [ ] 数据服务:内存缓存(Tair),持久化存储(OB) 58 | - [ ] 工具:集成到chair插件(基于node-cluster做负载均衡) 59 | 60 | - 案例 61 | 62 | - [ ] 聊天室案例 63 | - [ ] 贪吃蛇多人对战服务端设计 64 | 65 | ### 二期: 完善基础建设 66 | 67 | - 基础建设 68 | 69 | - [ ] 多进程负载均衡 70 | - [ ] 服务监控 71 | - [ ] 工具完善 72 | 73 | - 业务服务沉淀 74 | 75 | - [ ] AI 服务 76 | - [ ] 房间自动匹配服务 77 | - [ ] 自动寻路服务等 78 | 79 | ### 三期: 平台建设 80 | 81 | - [ ] 业务服务抽象沉淀,建立实时游戏SaaS服务平台 82 | 83 | ## 参考 84 | 85 | - [pomelo架构概览](https://github.com/NetEase/pomelo/wiki/pomelo%E6%9E%B6%E6%9E%84%E6%A6%82%E8%A7%88) 86 | 87 | 88 | -------------------------------------------------------------------------------- /example-packages/chat/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": "." 7 | }, 8 | "include": [ 9 | "src" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /example-packages/chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@regax/example-chat", 4 | "version": "0.1.25", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@regax/client-websocket": "^0.1.16", 8 | "@regax/server": "^0.1.31", 9 | "crc": "^3.8.0", 10 | "node-uuid": "^1.4.8", 11 | "utility": "^1.16.3", 12 | "ws": "^7.2.0" 13 | }, 14 | "scripts": { 15 | "start": "node ./lib/app.js", 16 | "prepare": "yarn run clean && yarn run build", 17 | "clean": "regax clean", 18 | "build": "regax build", 19 | "watch": "regax watch", 20 | "test": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example-packages/chat/src/app.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '@regax/server' 2 | import { client } from './app/client/client' 3 | 4 | async function server(): Promise { 5 | const gameServer = new Application( 6 | __dirname, 7 | ) 8 | await gameServer.start() 9 | } 10 | 11 | async function main(): Promise { 12 | await server() 13 | await client() 14 | } 15 | 16 | main() 17 | -------------------------------------------------------------------------------- /example-packages/chat/src/app/client/client.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as ws from 'ws' 3 | import { Client } from '@regax/client-websocket' 4 | 5 | export async function client(): Promise { 6 | const c1 = new Client({ url: 'ws://127.0.0.1:8089', reconnect: true, WebSocket: ws }) 7 | const c2 = new Client({ url: 'ws://127.0.0.1:8089', reconnect: true, WebSocket: ws }) 8 | await c1.connect() 9 | await c2.connect() 10 | // 监听房间变化 11 | c1.on('onChat', (d: any) => { 12 | console.log('>>>> client1 get message: ', d) 13 | }) 14 | c2.on('onChat', (d: any) => { 15 | console.log('>>>> client2 get message: ', d) 16 | }) 17 | // c1进入房间1 18 | await c1.request('connector.user.enter', { rid: 'room1', username: 'client1' }) 19 | console.log('client1 enter the room') 20 | await c2.request('connector.user.enter', { rid: 'room1', username: 'client2' }) 21 | console.log('client2 enter the room') 22 | // 发送消息 23 | c1.request('chat.chat.send', { target: '*', content: 'hello world' }) 24 | console.log('client1 send message to all: hello world') 25 | c2.request('chat.chat.send', { target: '*', content: 'hello world' }) 26 | console.log('client2 send message to all: hello world') 27 | c2.request('chat.chat.send', { target: 'client1', content: 'hi, client1' }) 28 | console.log('client2 send message to client1: hi, client1') 29 | } 30 | -------------------------------------------------------------------------------- /example-packages/chat/src/app/component/my.ts: -------------------------------------------------------------------------------- 1 | import { Component, injectable, inject, Application } from '@regax/server' 2 | 3 | @injectable() 4 | export default class MyComponent implements Component { 5 | constructor( 6 | @inject(Application) protected readonly app: Application 7 | ) { 8 | } 9 | onStart(): void { 10 | // console.log('on plugin start ') 11 | } 12 | /** 13 | * Use "this.app.service.my.method" to invoke 14 | */ 15 | onServiceRegistry(): { method: () => void } { 16 | return { 17 | method(): void { 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example-packages/chat/src/app/filter/routerLog.ts: -------------------------------------------------------------------------------- 1 | import { Filter, Application } from '@regax/server' 2 | const utility = require('utility') 3 | 4 | export default function Filter(app: Application): Filter { 5 | const traceLogger = app.getLogger('traceLogger') 6 | return async (ctx, next) => { 7 | const startTime = Date.now() 8 | await next() 9 | traceLogger.write(`${utility.logDate('.')},${ctx.traceId},${ctx.routeType},${ctx.route},${ctx.error ? 'N' : 'Y'},${Date.now() - startTime}ms,${ctx.session.uid}`) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example-packages/chat/src/app/server/chat/controller/chat.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Controller } from '@regax/server' 3 | 4 | export default class ChatController extends Controller { 5 | /** 6 | * Send messages to users 7 | * @param msg 8 | */ 9 | send(msg: any): void { 10 | const { session } = this.ctx 11 | const rid = session.get('rid') // room id 12 | const username = (session.uid as string).split('*')[0] 13 | const param = { 14 | msg: msg.content, 15 | from: username, 16 | target: msg.target 17 | } 18 | const channel = this.service.channel.getChannel(rid as string, true)! 19 | if (msg.target === '*') { 20 | // the target is all users 21 | channel.pushMessage('onChat', param) 22 | } else { 23 | // the target is specific user 24 | const tuid = msg.target + '*' + rid 25 | const data = channel.getValueByUid(tuid) // serverId 26 | if (!data) { 27 | this.fail('User %s is not in the room %s', msg.target, rid) 28 | return 29 | } 30 | const uids = [{ 31 | uid: tuid, 32 | sid: data.sid, 33 | }] 34 | channel.pushMessageByUids('onChat', param, uids) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example-packages/chat/src/app/server/chat/rpc/room.ts: -------------------------------------------------------------------------------- 1 | import { RPC } from '@regax/server' 2 | 3 | export default class ChatRPC extends RPC { 4 | add(uid: string, serverId: string, rid: string): void { 5 | const channel = this.service.channel.createChannel(rid) 6 | channel.add(uid, serverId) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example-packages/chat/src/app/server/connector/controller/user.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@regax/server' 2 | 3 | export default class EntryController extends Controller { 4 | /** 5 | * New client entry chat server. 6 | */ 7 | async enter(msg: { rid: number, username: string }): Promise { 8 | const rid = msg.rid // room id 9 | const uid = msg.username + '*' + rid 10 | const { session } = this.ctx 11 | // bind to uid 12 | session.bind(uid) 13 | session.set('rid', rid) 14 | session.push('rid') 15 | session.on(session.event.CLOSED, () => { 16 | console.log('>>>>>>>>>> session closed') 17 | }) 18 | await this.rpc.chat.room.add(uid, this.app.serverId, rid) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example-packages/chat/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationOpts } from '@regax/server' 2 | import * as uuid from 'node-uuid' 3 | 4 | export default function configDefault(): ApplicationOpts { 5 | return { 6 | component: { 7 | all: ['my'] 8 | }, 9 | connector: { 10 | createTraceId: () => uuid.v1().replace(/-/g, ''), 11 | }, 12 | customLogger: { 13 | traceLogger: { 14 | file: 'trace-digest.log', 15 | consoleLevel: 'NONE', 16 | }, 17 | }, 18 | master: { 19 | servers: [ 20 | { serverType: 'connector', sticky: true, clientPort: 8089 }, 21 | { serverType: 'connector', sticky: true, clientPort: 8089 }, 22 | { serverType: 'chat' }, 23 | { serverType: 'chat' }, 24 | ], 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example-packages/chat/src/config/router.ts: -------------------------------------------------------------------------------- 1 | import { Session } from '@regax/server' 2 | const crc = require('crc') 3 | 4 | export function chat(servers: string[], session: Session): string { 5 | if (!session.get('rid')) throw new Error('RoomId miss') 6 | const index = Math.abs(crc.crc32(session.get('rid'))) % servers.length 7 | return servers[index] 8 | } 9 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/*", 7 | "dev-packages/*", 8 | "example-packages/*" 9 | ], 10 | "command": { 11 | "run": { 12 | "stream": true 13 | }, 14 | "publish": { 15 | "registry": "https://registry.npmjs.org/" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regax", 3 | "private": true, 4 | "description": "a Multiplayer Game Engine for Node.js", 5 | "scripts": { 6 | "prepare": "yarn build:clean", 7 | "clean": "lerna run clean", 8 | "build": "lerna run build", 9 | "build:clean": "lerna run prepare", 10 | "watch": "lerna run watch --parallel", 11 | "test": "lerna run test --scope \"@regax/!(example-)*\" --stream --concurrency=1", 12 | "test:watch": "lerna run test:watch --scope \"@regax/!(example-)*\" --stream --concurrency=1", 13 | "publish": "lerna publish", 14 | "len": "wc -l `find . -regex '.*packages.*src.*.ts'`" 15 | }, 16 | "workspaces": [ 17 | "packages/*", 18 | "dev-packages/*", 19 | "example-packages/*" 20 | ], 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "yarn prepare" 24 | } 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/regaxjs/regax.git" 29 | }, 30 | "homepage": "https://github.com/regaxjs/regax", 31 | "author": "xiamidaxia ", 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@istanbuljs/nyc-config-typescript": "^0.1.3", 35 | "@types/expect": "^24.3.0", 36 | "@types/mocha": "^5.2.7", 37 | "@types/node": "^12.12.3", 38 | "@types/sinon": "^7.5.0", 39 | "concurrently": "^5.0.0", 40 | "expect": "^24.9.0", 41 | "husky": "^3.0.9", 42 | "lerna": "^3.19.0", 43 | "mocha": "^6.2.2", 44 | "nyc": "^15.0.0", 45 | "rimraf": "^3.0.0", 46 | "sinon": "^7.5.0", 47 | "ts-node": "^8.5.4", 48 | "tslint": "^5.20.0", 49 | "typescript": "^3.6.4" 50 | }, 51 | "dependencies": { 52 | "cross-env": "^6.0.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/client-udpsocket/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es5" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/client-udpsocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/client-udpsocket", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "main": "lib/index", 6 | "typings": "lib/index.d.ts", 7 | "dependencies": { 8 | "@regax/common": "^0.1.11", 9 | "@regax/protocol": "^0.1.5" 10 | }, 11 | "files": [ 12 | "lib" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/regaxjs/regax.git" 17 | }, 18 | "homepage": "https://github.com/regaxjs/regax", 19 | "scripts": { 20 | "prepare": "yarn run clean && yarn run build", 21 | "clean": "regax clean", 22 | "build": "regax build", 23 | "watch": "regax watch", 24 | "test": "regax test" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/client-udpsocket/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './udpSocket' 2 | export * from './client' 3 | -------------------------------------------------------------------------------- /packages/client-websocket/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es5" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/client-websocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/client-websocket", 3 | "version": "0.1.16", 4 | "license": "MIT", 5 | "main": "lib/index", 6 | "typings": "lib/index.d.ts", 7 | "dependencies": { 8 | "@regax/common": "^0.1.11", 9 | "@regax/protocol": "^0.1.5" 10 | }, 11 | "files": [ 12 | "lib" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/regaxjs/regax.git" 17 | }, 18 | "homepage": "https://github.com/regaxjs/regax", 19 | "scripts": { 20 | "prepare": "yarn run clean && yarn run build", 21 | "clean": "regax clean", 22 | "build": "regax build", 23 | "watch": "regax watch", 24 | "test": "regax test" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/client-websocket/src/client.spec.ts: -------------------------------------------------------------------------------- 1 | // import * as http from 'http' 2 | 3 | describe('client-websocket', () => { 4 | before(() => { 5 | // const server = http.createServer() 6 | }) 7 | it('connect', () => { 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/client-websocket/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './socket' 2 | export * from './client' 3 | -------------------------------------------------------------------------------- /packages/common/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es5" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/common", 3 | "version": "0.1.11", 4 | "license": "MIT", 5 | "main": "lib/index", 6 | "typings": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/regaxjs/regax.git" 13 | }, 14 | "homepage": "https://github.com/regaxjs/regax", 15 | "scripts": { 16 | "prepare": "yarn run clean && yarn run build", 17 | "clean": "regax clean", 18 | "build": "regax build", 19 | "watch": "regax watch", 20 | "test": "regax test" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/common/src/error.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | export class RegaxError extends Error { 4 | code?: string | number 5 | static create(e: string | Error, code?: string | number): RegaxError { 6 | let error: RegaxError 7 | if (typeof e === 'string') { 8 | error = new RegaxError(e) 9 | } else { 10 | error = new RegaxError(e.message) 11 | error.stack = e.stack 12 | Object.assign(error, e) 13 | } 14 | error.code = code 15 | return error 16 | } 17 | static toJSON(e: Error): { message: string, stack?: string, code?: string | number } { 18 | return { 19 | message: e.message, 20 | stack: e.stack, 21 | code: (e as any).code 22 | } 23 | } 24 | } 25 | 26 | export enum ErrorCode { 27 | CONTROLLER_FAIL = 'CONTROLLER_FAIL', 28 | RPC_FAIL = 'RPC_FAIL', 29 | TIMEOUT = 'TIMEOUT', 30 | } 31 | -------------------------------------------------------------------------------- /packages/common/src/event.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regaxjs/regax/9acb14810a90648bd41f45fe49a858a918fe53ea/packages/common/src/event.spec.ts -------------------------------------------------------------------------------- /packages/common/src/event.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | export interface EventListener { 3 | (...args: any[]): any, 4 | fn?: (...args: any[]) => any, 5 | } 6 | 7 | interface EventRemove { 8 | (): void 9 | } 10 | 11 | export interface EventEmitter { 12 | readonly event?: any, 13 | on(event: T & string, fn: EventListener): EventRemove 14 | once(event: T & string, fn: EventListener): EventRemove 15 | off(event: T & string, fn?: EventListener): void 16 | emit(event: T & string, ...args: any): void 17 | } 18 | 19 | export class EventEmitter { 20 | protected listeners?: { [key: string]: EventListener[] } 21 | on(event: T & string, fn: EventListener): EventRemove { 22 | if (!this.listeners) this.listeners = {}; 23 | (this.listeners[event] = this.listeners[event] || []).push(fn) 24 | return () => this.off(event, fn) 25 | } 26 | once(event: T & string, fn: EventListener): EventRemove { 27 | const listener: EventListener = (...args) => { 28 | this.off(event, listener) 29 | fn.apply(this, args) 30 | } 31 | listener.fn = fn 32 | return this.on(event, fn) 33 | } 34 | off(event?: T & string, fn?: EventListener): void { 35 | // remove all listeners 36 | if (!event || !this.listeners) { 37 | this.listeners = undefined 38 | return 39 | } 40 | const currentListeners = this.listeners[event] 41 | if (!currentListeners) return 42 | if (!fn) { 43 | delete this.listeners[event] 44 | return 45 | } 46 | for (let i = 0; i < currentListeners.length; i++) { 47 | const cb = currentListeners[i] 48 | if (cb === fn || cb.fn === fn) { 49 | currentListeners.splice(i, 1) 50 | break 51 | } 52 | } 53 | } 54 | emit(event: T & string, ...args: any[]): void { 55 | const currentListeners = (this.listeners || {})[event] 56 | if (!currentListeners || currentListeners.length === 0) return 57 | for (let i = 0; i < currentListeners.length; i ++) { 58 | currentListeners[i].apply(this, args) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event' 2 | export * from './util' 3 | export * from './types' 4 | export * from './tick' 5 | export * from './promise' 6 | export * from './error' 7 | -------------------------------------------------------------------------------- /packages/common/src/promise.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | // Fork from: https://github.com/phosphorjs/phosphor/blob/master/packages/coreutils/src/promise.ts 3 | /** 4 | * A class which wraps a promise into a delegate object. 5 | * 6 | * #### Notes 7 | * This class is useful when the logic to resolve or reject a promise 8 | * cannot be defined at the point where the promise is created. 9 | */ 10 | export 11 | class PromiseDelegate { 12 | /** 13 | * Construct a new promise delegate. 14 | */ 15 | constructor() { 16 | this.promise = new Promise((resolve, reject) => { 17 | this._resolve = resolve 18 | this._reject = reject 19 | }) 20 | } 21 | 22 | /** 23 | * The promise wrapped by the delegate. 24 | */ 25 | readonly promise: Promise 26 | 27 | /** 28 | * Resolve the wrapped promise with the given value. 29 | * 30 | * @param value - The value to use for resolving the promise. 31 | */ 32 | resolve(value?: T | PromiseLike): void { 33 | const resolve = this._resolve 34 | resolve(value) 35 | } 36 | 37 | /** 38 | * Reject the wrapped promise with the given value. 39 | * 40 | * @reason - The reason for rejecting the promise. 41 | */ 42 | reject(reason?: any): void { 43 | const reject = this._reject 44 | reject(reason) 45 | } 46 | 47 | private _resolve: (value?: T | PromiseLike) => void 48 | private _reject: (reason?: any) => void 49 | } 50 | -------------------------------------------------------------------------------- /packages/common/src/tick.ts: -------------------------------------------------------------------------------- 1 | const gapThreshold = 100 // tick gap threashold 2 | 3 | export class Tick { 4 | protected tickId?: number 5 | protected tickTimeoutId?: number 6 | protected nextTickTimeout: number = 0 7 | constructor( 8 | protected tickInterval = 0, 9 | protected tickTimeout = 0 10 | ) {} 11 | isRunning(): boolean { 12 | return !!this.tickId 13 | } 14 | next(onTick: () => void, onTimeout?: () => void): void { 15 | if (!this.tickInterval) return 16 | if (this.tickTimeoutId) { 17 | clearTimeout(this.tickTimeoutId) 18 | this.tickTimeoutId = undefined 19 | } 20 | if (this.tickId) { 21 | // already in a tick interval 22 | return 23 | } 24 | const tickTimeoutCb = () => { 25 | const gap = this.nextTickTimeout - Date.now() 26 | if (gap > gapThreshold) { 27 | // @ts-ignore 28 | this.tickTimeoutId = setTimeout(tickTimeoutCb, gap) 29 | } else { 30 | if (onTimeout) onTimeout() 31 | } 32 | } 33 | // @ts-ignore 34 | this.tickId = setTimeout(() => { 35 | this.tickId = undefined 36 | onTick() 37 | if (!onTimeout) return 38 | this.nextTickTimeout = Date.now() + this.tickTimeout 39 | // @ts-ignore 40 | this.tickTimeoutId = setTimeout(tickTimeoutCb, this.tickTimeout) 41 | }, this.tickInterval) 42 | } 43 | refreshNextTickTimeout(): void { 44 | if (this.nextTickTimeout) { 45 | this.nextTickTimeout = Date.now() + this.tickTimeout 46 | } 47 | } 48 | setTick(tickInterval: number, tickTimeout: number = 0): void { 49 | this.tickInterval = tickInterval 50 | this.tickTimeout = tickTimeout 51 | } 52 | stop(): void { 53 | if (this.tickId) { 54 | clearTimeout(this.tickId) 55 | this.tickId = undefined 56 | } 57 | if (this.tickTimeoutId) { 58 | clearTimeout(this.tickTimeoutId) 59 | this.tickTimeoutId = undefined 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/common/src/types.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | export type MaybePromise = T | Promise | PromiseLike 4 | 5 | export type MaybeArray = T | T[] 6 | 7 | export type Deferred = { 8 | [P in keyof T]: Promise 9 | } 10 | 11 | export type Mutable = { -readonly [P in keyof T]: T[P] } 12 | 13 | export interface PlainObject { [key: string]: PlainObject | PlainData | undefined } 14 | 15 | export type PlainData = string | number | boolean | ObjectOf | any[] 16 | 17 | export interface ObjectOf { [key: string]: T } 18 | 19 | export type Fn = (arg1?: arg1, arg2?: arg1, arg3?: arg3, ...args: args[]) => T 20 | -------------------------------------------------------------------------------- /packages/common/src/util.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | const keys = Object.keys 3 | import { Fn } from './types' 4 | 5 | export const partial = (fn: Fn, ...args: any[]) => (...rest: any[]) => fn(...args, ...rest) 6 | 7 | export const partialRight = (fn: Fn, ...args: any[]) => (...rest: any[]) => fn(...rest, ...args) 8 | 9 | export const each = (obj: any, fn: (value: any, key: string) => void) => keys(obj).forEach(key => fn(obj[key], key)) 10 | 11 | export const values = (obj: any) => Object.values ? Object.values(obj) : keys(obj).map(k => obj[k]) 12 | 13 | export const filter = (obj: any, fn: (value: any, key: string) => boolean, dest?: any) => 14 | keys(obj).reduce((output, key) => fn(obj[key], key) ? Object.assign(output, { [key]: obj[key]}) : output, dest || {}) 15 | 16 | export const pick = (obj: any, fields: string[], dest?: any) => filter(obj, (n, k) => fields.indexOf(k) !== -1, dest) 17 | 18 | export const omit = (obj: any, fields: string[], dest?: any) => filter(obj, (n, k) => fields.indexOf(k) === -1, dest) 19 | 20 | export const reduce = (obj: any, fn: (res: any, value: any, key: string) => T, res: T) => keys(obj).reduce((r, k) => fn(r, obj[k], k), res) 21 | 22 | export const mapValues = (obj: any, fn: (value: any, key: string) => any) => reduce(obj, (res, value, key) => Object.assign(res, { [key]: fn(value, key) }), {}) 23 | 24 | export const mapKeys = (obj: any, fn: (value: any, key: string) => any) => reduce(obj, (res, value, key) => Object.assign(res, { [fn(value, key)]: value }), {}) 25 | 26 | export const delay = (ms: number) => new Promise((res: any) => setTimeout(res, ms)) 27 | -------------------------------------------------------------------------------- /packages/egg-regax/agent.js: -------------------------------------------------------------------------------- 1 | const { createAgent } = require('./lib') 2 | module.exports = function (app) { 3 | return createAgent(app) 4 | } 5 | -------------------------------------------------------------------------------- /packages/egg-regax/app.js: -------------------------------------------------------------------------------- 1 | const { createApp } = require('./lib') 2 | module.exports = function (app) { 3 | return createApp(app) 4 | } 5 | -------------------------------------------------------------------------------- /packages/egg-regax/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es2017" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/egg-regax/config/config.default.js: -------------------------------------------------------------------------------- 1 | // Load by egg 2 | module.exports = () => { 3 | const config = {}; 4 | 5 | // This will trigger the egg-logroator 6 | // TODO test 7 | config.customLogger = { 8 | regaxAppLogger: { 9 | consoleLevel: 'NONE', 10 | file: 'regax-app.log', 11 | }, 12 | regaxAgentLogger: { 13 | consoleLevel: 'NONE', 14 | file: 'regax-agent.log', 15 | }, 16 | regaxCoreLogger: { 17 | consoleLevel: 'NONE', 18 | file: 'regax-core.log', 19 | }, 20 | }; 21 | 22 | config.regax = { 23 | agentRPCPort: 4333 24 | } 25 | 26 | return config; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/egg-regax/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/egg-regax", 3 | "version": "0.1.27", 4 | "description": "regax for egg plugin", 5 | "license": "MIT", 6 | "main": "lib/index", 7 | "typings": "lib/index.d.ts", 8 | "eggPlugin": { 9 | "name": "regax", 10 | "dependencies": [] 11 | }, 12 | "regaxPlugin": { 13 | "dir": "lib" 14 | }, 15 | "files": [ 16 | "config", 17 | "lib", 18 | "agent.js", 19 | "app.js" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/regaxjs/regax.git" 24 | }, 25 | "homepage": "https://github.com/regaxjs/regax", 26 | "dependencies": { 27 | "@regax/common": "^0.1.11", 28 | "@regax/rpc": "^0.1.19", 29 | "@regax/server": "^0.1.31" 30 | }, 31 | "scripts": { 32 | "prepare": "yarn run clean && yarn run build", 33 | "clean": "regax clean", 34 | "build": "regax build", 35 | "watch": "regax watch", 36 | "test": "regax test" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/egg-regax/src/agent.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as path from 'path' 3 | const childProcess = require('child_process') 4 | import { RegaxIPC } from './common/ipc' 5 | import { createServer, ServerInfo } from '@regax/rpc' 6 | import { createProxy } from '@regax/server/lib/util/proxy' 7 | 8 | const toCammandOpts = (opts: any, arr: string[] = []) => Object.keys(opts).reduce((res: any, k: string) => { 9 | if (opts[k] === undefined) return res 10 | return res.concat(`--${k}`, opts[k]) 11 | }, arr) 12 | 13 | async function createAgentRPCServer(agent: any, services: any): Promise { 14 | const server = createServer({ 15 | serverType: 'egg-agent', 16 | services, 17 | logger: agent.logger, 18 | port: agent.config.regax.agentRPCPort, 19 | }) 20 | await server.start() 21 | agent.logger.info('[egg-regax] egg-agent rpc server started on port: ' + server.serverInfo.port) 22 | return server.serverInfo 23 | } 24 | 25 | export function createAgent(agent: any): void { 26 | const ipc = new RegaxIPC(agent.messenger, 'agent') 27 | 28 | agent.messenger.once('egg-ready', async () => { 29 | const regaxBin = path.join(agent.baseDir, './node_modules/@regax/server/bin/regax') 30 | ipc.ready() 31 | // Create agent rpc server 32 | const __eggAgentServerInfo = await createAgentRPCServer(agent, createProxy((service: string) => (data: any) => ipc.invoke(service, data))) 33 | const options = { 34 | directory: path.join(agent.baseDir, './app/regax'), 35 | env: agent.config.env, 36 | type: 'master', 37 | configs: JSON.stringify({ 38 | loader: { 39 | plugins: { 40 | egg: { enable: true, path: path.join(__dirname, '..') }, 41 | logrotator: false, // use chair logroator instead 42 | }, 43 | }, 44 | __loggerDir: agent.config.logger.dir, 45 | __eggConfig: agent.config, 46 | __eggAgentServerInfo, 47 | }), 48 | } 49 | const cmdOpts = toCammandOpts(options, ['start']) 50 | childProcess.fork(regaxBin, cmdOpts, { 51 | detached: false, 52 | stdio: 'inherit', 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /packages/egg-regax/src/app.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regaxjs/regax/9acb14810a90648bd41f45fe49a858a918fe53ea/packages/egg-regax/src/app.spec.ts -------------------------------------------------------------------------------- /packages/egg-regax/src/app.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | import { RegaxIPC } from './common/ipc' 4 | 5 | export function createApp(app: any): void { 6 | app.messenger.once('egg-ready', async () => { 7 | const ipc = new RegaxIPC(app.messenger, 'worker', { 8 | eggContext({ args, key }: { args: any[], key: string }): any { 9 | const eggCtx = app.createAnonymousContext() 10 | let res 11 | let lastCtx = eggCtx 12 | try { 13 | res = key.split('.').reduce((ctx: any, k: string) => { 14 | lastCtx = ctx 15 | return ctx[k] 16 | }, eggCtx) 17 | } catch (e) { 18 | throw new Error('Context ' + key + ' is undefined.') 19 | } 20 | if (typeof res === 'function') return res.call(lastCtx, ...args) 21 | return res 22 | } 23 | }) 24 | ipc.ready() 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /packages/egg-regax/src/common/constant.ts: -------------------------------------------------------------------------------- 1 | export const EGG_REGISTRY_ROOT_PATH = '/egg-regax/rpc' 2 | -------------------------------------------------------------------------------- /packages/egg-regax/src/common/ipc.ts: -------------------------------------------------------------------------------- 1 | import { Fn, PromiseDelegate, RegaxError } from '@regax/common' 2 | 3 | const INVOKE_TIMEOUT = 5000 4 | let _id = 0 5 | export enum RegaxIPCType { 6 | REQUEST = 'regax_ipc_req', 7 | RESPONSE = 'regax_ipc_res' 8 | } 9 | // tslint:disable:no-any 10 | export class RegaxIPC { 11 | protected reqs: { [reqId: string]: PromiseDelegate } = {} 12 | constructor( 13 | protected messenger: any, 14 | protected type: 'agent' | 'worker', 15 | protected services: { [serviceName: string]: Fn } = {} 16 | ) {} 17 | ready(): void { 18 | this.messenger.on(RegaxIPCType.REQUEST, async ({ action, data, id, pid }: any) => { 19 | if (this.services[action]) { 20 | try { 21 | const res = await this.services[action](data) 22 | this.messenger.sendTo(pid, RegaxIPCType.RESPONSE, { data: res, id }) 23 | } catch (e) { 24 | this.messenger.sendTo(pid, RegaxIPCType.RESPONSE, { error: RegaxError.toJSON(e), id }) 25 | } 26 | } else { 27 | this.messenger.sendTo(pid, RegaxIPCType.RESPONSE, { error: RegaxError.toJSON(new Error('Unknown action' + action)), id }) 28 | } 29 | }) 30 | this.messenger.on(RegaxIPCType.RESPONSE, ({ data, error, id }: any) => { 31 | if (this.reqs[id]) { 32 | const p = this.reqs[id] 33 | delete this.reqs[id] 34 | if (error) { 35 | p.reject(RegaxError.create(error)) 36 | } else { 37 | p.resolve(data) 38 | } 39 | } 40 | }) 41 | } 42 | invoke(action: string, data?: any): Promise { 43 | const p = new PromiseDelegate() 44 | const reqId = _id++ 45 | this.reqs[reqId] = p 46 | setTimeout(() => { 47 | delete this.reqs[reqId] 48 | p.reject(new Error(`inovke ${action} timeout`)) 49 | }, INVOKE_TIMEOUT) 50 | if (this.type === 'agent') { 51 | this.messenger.sendRandom(RegaxIPCType.REQUEST, { action, data, id: reqId, pid: process.pid }) 52 | } else { 53 | this.messenger.sendToAgent(RegaxIPCType.REQUEST, { action, data, id: reqId, pid: process.pid }) 54 | } 55 | return p.promise 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/egg-regax/src/common/messenger.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Fn, EventEmitter } from '@regax/common' 3 | import { RegaxIPCType } from './ipc' 4 | 5 | function sendMessage(msg: any): void { 6 | const connected = process.connected && process.send 7 | if (connected) { 8 | process.send!(msg) 9 | } 10 | } 11 | 12 | export class Messenger { 13 | protected emitter = new EventEmitter() 14 | constructor() { 15 | process.on('message', (data: any) => { 16 | this.emitter.emit(RegaxIPCType.RESPONSE, data.data) 17 | }) 18 | } 19 | on(action: string, fn: Fn): void { 20 | this.emitter.on(action, fn) 21 | } 22 | sendToAgent(action: string, data: any): void { 23 | sendMessage({ action, data }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/egg-regax/src/common/remoteCache.ts: -------------------------------------------------------------------------------- 1 | import { SimpleRemoteCache } from '@regax/rpc' 2 | 3 | // tslint:disable:no-any 4 | export class TairRemoteCache implements SimpleRemoteCache { 5 | constructor( 6 | protected readonly tairAPI: any 7 | ) {} 8 | async set(key: string, value: any, exSeconds?: number): Promise { 9 | await this.tairAPI.put(key, value, { 10 | expired: exSeconds, 11 | }) 12 | } 13 | async get(key: string, exSeconds?: number): Promise { 14 | const data = await this.tairAPI.get(key, { 15 | expired: exSeconds, 16 | }) 17 | return data.data 18 | } 19 | del(key: string): Promise { 20 | return this.tairAPI.invalid(key) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/egg-regax/src/component/egg.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { inject, Component, injectable, Application } from '@regax/server' 3 | import RPC from '@regax/server/lib/component/rpc' 4 | // import { createProxy } from '@regax/server/lib/util/proxy' 5 | 6 | @injectable() 7 | export default class RegaxEggComponent implements Component { 8 | constructor( 9 | @inject('app') protected readonly app: Application, 10 | @inject('rpc') protected readonly rpc: RPC, 11 | ) { 12 | } 13 | onServiceRegistry(): any { 14 | const self = this 15 | const eggConfig = self.app.customConfigs.__eggConfig 16 | return { 17 | get(key: string): Promise { 18 | return self.invokeEggContext(key) 19 | }, 20 | invoke: (key: string, args: any[]) => self.invokeEggContext(key, args), 21 | getService: (key: string) => (...args: any) => this.invokeEggContext(`service.${key}`, args), 22 | config: eggConfig 23 | } 24 | } 25 | invokeEggContext(key: string, args: any[] = []): Promise { 26 | const serverId = this.rpc.rpcClient.getServersByType('egg-agent')[0] 27 | return this.rpc.rpcClient.rpcInvoke(serverId, 'eggContext', [{ key, args }]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/egg-regax/src/component/eggMaster.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { inject, Component, injectable, Application } from '@regax/server' 3 | import RPC from '@regax/server/lib/component/rpc' 4 | 5 | @injectable() 6 | export default class RegaxEggMasterComponent implements Component { 7 | constructor( 8 | @inject('app') protected readonly app: Application, 9 | @inject('rpc') protected readonly rpc: RPC, 10 | ) { 11 | } 12 | onServiceRegistry(): any { 13 | return { 14 | } 15 | } 16 | onStart(): void { 17 | // register the egg agent rpc server 18 | this.rpc.rpcRegistry.register(this.app.customConfigs.__eggAgentServerInfo) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/egg-regax/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { Application, ApplicationOpts } from '@regax/server' 2 | 3 | export default function configDefault(app: Application): ApplicationOpts { 4 | return { 5 | component: { 6 | master: ['egg', 'eggMaster'], 7 | frontend: ['egg'], 8 | backend: ['egg'] 9 | }, 10 | logger: { 11 | dir: app.customConfigs.__loggerDir 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/egg-regax/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './agent' 2 | export * from './app' 3 | -------------------------------------------------------------------------------- /packages/egg-regax/src/typings/egg.d.ts: -------------------------------------------------------------------------------- 1 | import { Application as RegaxApplication } from '@regax/server' 2 | 3 | declare module 'egg' { 4 | export interface Application { 5 | regax: RegaxApplication; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/logger/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es2017" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/logger", 3 | "version": "0.1.7", 4 | "license": "MIT", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "scripts": { 8 | "prepare": "yarn run clean && yarn run build", 9 | "clean": "regax clean", 10 | "build": "regax build", 11 | "watch": "regax watch", 12 | "test": "regax test" 13 | }, 14 | "files": [ 15 | "lib" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/regaxjs/regax.git" 20 | }, 21 | "homepage": "https://github.com/regaxjs/regax", 22 | "dependencies": { 23 | "egg-logger": "^2.4.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/logger/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './regax/regaxLogger' 2 | export * from './regax/regaxLoggerManager' 3 | export * from './logger' 4 | export * from './regax/consoleLogger' 5 | export * from './regax/errorLogger' 6 | export * from './regax/customLogger' 7 | 8 | import { ConsoleLogger } from './regax/consoleLogger' 9 | export const defaultLogger = new ConsoleLogger({ level: 'INFO' }) 10 | -------------------------------------------------------------------------------- /packages/logger/src/logger.spec.ts: -------------------------------------------------------------------------------- 1 | import { Logger, ConsoleLogger, RegaxLogger } from './index' 2 | import * as path from 'path' 3 | describe('logger', () => { 4 | it('create', () => { 5 | const a = new Logger({}) 6 | a.info('info output') 7 | }) 8 | it ('consoleLogger', () => { 9 | const a = new ConsoleLogger({ 10 | level: 'INFO' 11 | }) 12 | a.info('info output') 13 | a.warn('warn output') 14 | a.error('erro putput ') 15 | }) 16 | it ('regaxLogger', () => { 17 | const a = new RegaxLogger({ 18 | file: path.join(__dirname, 'test.log'), 19 | level: 'INFO' 20 | }) 21 | a.info('info output') 22 | a.warn('warn output') 23 | a.error('erro putput ') 24 | a.close() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/logger/src/logger.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { LoggerLevel } from 'egg-logger' 3 | import { Logger as EggLogger } from 'egg-logger' 4 | 5 | export * from 'egg-logger' 6 | 7 | export interface LoggerMetaData { 8 | date: number, 9 | level: LoggerLevel, 10 | pid: number, 11 | message: string, 12 | } 13 | 14 | export interface LoggerOpts { 15 | level?: LoggerLevel 16 | encoding?: string 17 | consoleLevel?: LoggerLevel 18 | allowDebugAtProd?: boolean 19 | } 20 | 21 | export class Logger extends EggLogger { 22 | protected options: T 23 | } 24 | -------------------------------------------------------------------------------- /packages/logger/src/regax/consoleLogger.ts: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('egg-logger/lib/utils') 3 | import { LoggerOpts, Logger, ConsoleTransport } from '../logger' 4 | 5 | /** 6 | * Terminal Logger, send log to console. 7 | */ 8 | export class ConsoleLogger extends Logger { 9 | /** 10 | * @constructor 11 | * @param {Object} options 12 | * - {String} [encoding] - log string encoding, default is 'utf8' 13 | */ 14 | constructor(options: LoggerOpts) { 15 | super(options) 16 | 17 | this.set('console', new ConsoleTransport({ 18 | level: this.options.level, 19 | formatter: utils.consoleFormatter, 20 | })) 21 | } 22 | 23 | get defaults(): LoggerOpts { 24 | return { 25 | encoding: 'utf8', 26 | level: process.env.NODE_ENV === 'production' ? 'INFO' : 'WARN', 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/logger/src/regax/customLogger.ts: -------------------------------------------------------------------------------- 1 | import { RegaxLogger } from './regaxLogger' 2 | 3 | /** 4 | * Custom Logger 5 | */ 6 | export class CustomLogger extends RegaxLogger {} 7 | -------------------------------------------------------------------------------- /packages/logger/src/regax/errorLogger.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | const utils = require('egg-logger/lib/utils') 3 | import { LoggerLevel, levels } from '../logger' 4 | import { RegaxLogger, RegaxLoggerOpts } from './regaxLogger' 5 | 6 | /** 7 | * Error Logger, only print `ERROR` level log. 8 | * level and consoleLevel should >= `ERROR` level. 9 | */ 10 | export class ErrorLogger extends RegaxLogger { 11 | constructor(options: RegaxLoggerOpts) { 12 | options = options || {} 13 | options.level = getDefaultLevel(options.level) as any 14 | options.consoleLevel = getDefaultLevel(options.consoleLevel) as any 15 | super(options) 16 | } 17 | } 18 | 19 | function getDefaultLevel(l?: LoggerLevel): number { 20 | const level = utils.normalizeLevel(l) as number 21 | 22 | if (level === undefined) { 23 | return levels.ERROR 24 | } 25 | 26 | return level > levels.ERROR ? level : levels.ERROR 27 | } 28 | -------------------------------------------------------------------------------- /packages/logrotator/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es2017" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/logrotator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/logrotator", 3 | "version": "0.0.5", 4 | "license": "MIT", 5 | "main": "lib/index", 6 | "typings": "lib/index.d.ts", 7 | "regaxPlugin": { 8 | "dir": "lib" 9 | }, 10 | "dependencies": { 11 | "@regax/logger": "^0.1.7", 12 | "debug": "^4.1.1", 13 | "moment": "^2.24.0", 14 | "mz": "^2.7.0" 15 | }, 16 | "peerDependencies": { 17 | "@regax/server": "^0.1.21" 18 | }, 19 | "files": [ 20 | "lib" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/regaxjs/regax.git" 25 | }, 26 | "homepage": "https://github.com/regaxjs/regax", 27 | "scripts": { 28 | "prepare": "yarn run clean && yarn run build", 29 | "clean": "regax clean", 30 | "build": "regax build", 31 | "watch": "regax watch", 32 | "test": "regax test" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/logrotator/src/component/logrotator.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, injectable, Application } from '@regax/server' 2 | 3 | export interface LogrotatorOpts { 4 | filesRotateByHour?: string[], // for rotate_by_hour 5 | filesRotateBySize?: string[], // for rotate_by_size 6 | hourDelimiter?: string, // default '-' 7 | maxFileSize?: number, 8 | maxFiles?: number, 9 | rotateDuration?: number, 10 | maxDays?: number, // for clean_log 11 | } 12 | 13 | @injectable() 14 | export default class LogrotatorComponent implements Component { 15 | protected opts: LogrotatorOpts 16 | constructor( 17 | @inject(Application) protected readonly app: Application, 18 | ) { 19 | this.opts = this.app.getConfig('logrotator') 20 | } 21 | onStart(): void { 22 | // console.log('>>>>>>>>>logrotator: ', this.app.serverType) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/logrotator/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { Application, ApplicationOpts } from '@regax/server' 2 | 3 | export default function configDefault(app: Application): ApplicationOpts { 4 | return { 5 | component: { 6 | agent: ['logrotator'] // exec in agent 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/logrotator/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rotator/rotator' 2 | export * from './component/logrotator' 3 | -------------------------------------------------------------------------------- /packages/logrotator/src/rotator/dayRotator.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const moment = require('moment') 3 | const debug = require('debug')('regax-logrotator:day_rotator') 4 | import { Rotator, RotateFileMap } from './rotator' 5 | import { walkLoggerFile } from '../util' 6 | 7 | // rotate log by day 8 | // rename from foo.log to foo.log.YYYY-MM-DD 9 | export class DayRotator extends Rotator { 10 | async getRotateFiles(): Promise { 11 | const files: RotateFileMap = new Map() 12 | const logDir = this.loggerOpts.dir 13 | const loggerFiles = walkLoggerFile(this.app.loggers) 14 | loggerFiles.forEach(file => { 15 | // support relative path 16 | if (!path.isAbsolute(file)) file = path.join(logDir, file) 17 | this.setFile(file, files) 18 | }) 19 | 20 | // Should rotate agent log, because schedule is running under app worker, 21 | // agent log is the only differece between app worker and agent worker. 22 | // - app worker -> egg-web.log 23 | // - agent worker -> egg-agent.log 24 | // TODO 25 | const agentLogName = this.loggerOpts.agentLogName 26 | this.setFile(path.join(logDir, agentLogName), files) 27 | 28 | return files 29 | } 30 | 31 | protected setFile(srcPath: string, files: RotateFileMap): void { 32 | // don't rotate logPath in filesRotateBySize 33 | if (this.filesRotateBySize.indexOf(srcPath) > -1) { 34 | return 35 | } 36 | 37 | // don't rotate logPath in filesRotateByHour 38 | if (this.filesRotateByHour.indexOf(srcPath) > -1) { 39 | return 40 | } 41 | 42 | if (!files.has(srcPath)) { 43 | // allow 2 minutes deviation 44 | const targetPath = srcPath + moment() 45 | .subtract(23, 'hours') 46 | .subtract(58, 'minutes') 47 | .format('.YYYY-MM-DD') 48 | debug('set file %s => %s', srcPath, targetPath) 49 | files.set(srcPath, { srcPath, targetPath }) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/logrotator/src/rotator/hourRotator.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const moment = require('moment') 3 | const debug = require('debug')('regax-logrotator:hour_rotator') 4 | import { Rotator, RotateFileMap } from './rotator' 5 | const fs = require('mz/fs') 6 | 7 | // rotate log by hour 8 | // rename from foo.log to foo.log.YYYY-MM-DD-HH 9 | export class HourRotator extends Rotator { 10 | async getRotateFiles(): Promise { 11 | const files: RotateFileMap = new Map() 12 | const logDir = this.loggerOpts.dir 13 | const filesRotateByHour = this.filesRotateByHour 14 | 15 | for (let logPath of filesRotateByHour) { 16 | // support relative path 17 | if (!path.isAbsolute(logPath)) logPath = path.join(logDir, logPath) 18 | const exists = await fs.exists(logPath) 19 | if (!exists) { 20 | continue 21 | } 22 | this.setFile(logPath, files) 23 | } 24 | 25 | return files 26 | } 27 | 28 | get hourDelimiter(): string { 29 | return this.opts.hourDelimiter! 30 | } 31 | 32 | protected setFile(srcPath: string, files: RotateFileMap): void { 33 | if (!files.has(srcPath)) { 34 | const targetPath = srcPath + moment().subtract(1, 'hours').format(`.YYYY-MM-DD${this.hourDelimiter}HH`) 35 | debug('set file %s => %s', srcPath, targetPath) 36 | files.set(srcPath, { srcPath, targetPath }) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/logrotator/src/rotator/rotator.ts: -------------------------------------------------------------------------------- 1 | // fork from https://github.com/eggjs/egg-logrotator#readme 2 | 3 | import { Application, ApplicationOpts } from '@regax/server' 4 | import { Logger } from '@regax/logger' 5 | import { LogrotatorOpts } from '../component/logrotator' 6 | const fs = require('mz/fs') 7 | 8 | export type RotateFileMap = Map 9 | 10 | export abstract class Rotator { 11 | protected logger: Logger 12 | protected opts: LogrotatorOpts 13 | protected loggerOpts: ApplicationOpts['logger'] & {} 14 | protected filesRotateBySize: string[] 15 | protected filesRotateByHour: string[] 16 | constructor( 17 | protected readonly app: Application 18 | ) { 19 | this.logger = app.coreLogger 20 | this.opts = app.getConfig('logrotator') 21 | this.loggerOpts = this.app.getConfig('logger') 22 | this.filesRotateBySize = this.opts.filesRotateBySize || [] 23 | this.filesRotateByHour = this.opts.filesRotateByHour || [] 24 | } 25 | abstract getRotateFiles(): Promise 26 | async rotate(): Promise { 27 | const files = await this.getRotateFiles() 28 | const rotatedFile = [] 29 | for (const file of files.values()) { 30 | try { 31 | await renameOrDelete(file.srcPath, file.targetPath) 32 | rotatedFile.push(`${file.srcPath} -> ${file.targetPath}`) 33 | } catch (err) { 34 | err.message = `[regax-logrotator] rename ${file.srcPath}, found exception: ` + err.message 35 | this.logger.error(err) 36 | } 37 | } 38 | 39 | if (rotatedFile.length) { 40 | // tell every one to reload logger 41 | this.logger.info('[regax-logrotator] broadcast log-reload') 42 | // TODO 43 | // this.app.messenger.sendToApp('log-reload') 44 | // this.app.messenger.sendToAgent('log-reload') 45 | } 46 | 47 | this.logger.info('[regax-logrotator] rotate files success by %s, files %j', 48 | this.constructor.name, rotatedFile) 49 | } 50 | } 51 | 52 | // rename from srcPath to targetPath, for example foo.log.1 > foo.log.2 53 | async function renameOrDelete(srcPath: string, targetPath: string): Promise { 54 | if (srcPath === targetPath) { 55 | return 56 | } 57 | const srcExists = await fs.exists(srcPath) 58 | if (!srcExists) { 59 | return 60 | } 61 | const targetExists = await fs.exists(targetPath) 62 | // if target file exists, then throw 63 | // because the target file always be renamed first. 64 | if (targetExists) { 65 | throw new Error(`targetFile ${targetPath} exists!!!`) 66 | } 67 | await fs.rename(srcPath, targetPath) 68 | } 69 | -------------------------------------------------------------------------------- /packages/logrotator/src/rotator/sizeRotator.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const debug = require('debug')('regax-logrotator:size_rotator') 3 | import { Rotator, RotateFileMap } from './rotator' 4 | const fs = require('mz/fs') 5 | 6 | // rotate log by size, if the size of file over maxFileSize, 7 | // it will rename from foo.log to foo.log.1 8 | // if foo.log.1 exists, foo.log.1 will rename to foo.log.2 9 | export class SizeRotator extends Rotator { 10 | 11 | async getRotateFiles(): Promise { 12 | const files: RotateFileMap = new Map() 13 | const logDir = this.loggerOpts.dir 14 | const filesRotateBySize = this.filesRotateBySize 15 | const maxFileSize = this.opts.maxFileSize! 16 | const maxFiles = this.opts.maxFiles! 17 | for (let logPath of filesRotateBySize) { 18 | // support relative path 19 | if (!path.isAbsolute(logPath)) logPath = path.join(logDir, logPath) 20 | 21 | const exists = await fs.exists(logPath) 22 | if (!exists) { 23 | continue 24 | } 25 | try { 26 | const stat = await fs.stat(logPath) 27 | if (stat.size >= maxFileSize) { 28 | this.logger.info(`[regax-logrotator] file ${logPath} reach the maximum file size, current size: ${stat.size}, max size: ${maxFileSize}`) 29 | // delete max log file if exists, otherwise will throw when rename 30 | const maxFileName = `${logPath}.${maxFiles}` 31 | const maxExists = await fs.exists(maxFileName) 32 | if (maxExists) { 33 | await fs.unlink(maxFileName) 34 | } 35 | this.setFile(logPath, files) 36 | } 37 | } catch (err) { 38 | err.message = '[regax-logrotator] ' + err.message 39 | this.logger.error(err) 40 | } 41 | } 42 | return files 43 | } 44 | 45 | setFile(logPath: string, files: RotateFileMap): void { 46 | const maxFiles = this.opts.maxFiles! 47 | if (files.has(logPath)) { 48 | return 49 | } 50 | // foo.log.2 -> foo.log.3 51 | // foo.log.1 -> foo.log.2 52 | for (let i = maxFiles - 1; i >= 1; i--) { 53 | const srcPath = `${logPath}.${i}` 54 | const targetPath = `${logPath}.${i + 1}` 55 | debug('set file %s => %s', srcPath, targetPath) 56 | files.set(srcPath, { srcPath, targetPath }) 57 | } 58 | // foo.log -> foo.log.1 59 | debug('set file %s => %s', logPath, `${logPath}.1`) 60 | files.set(logPath, { srcPath: logPath, targetPath: `${logPath}.1` }) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /packages/logrotator/src/util.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { RegaxLoggerManager } from '@regax/logger' 3 | 4 | export function walkLoggerFile(loggers: RegaxLoggerManager): string[] { 5 | const files = [] 6 | for (const key in loggers) { 7 | if (!loggers.hasOwnProperty(key)) { 8 | continue 9 | } 10 | const registeredLogger = (loggers as any)[key] 11 | for (const transport of registeredLogger.values()) { 12 | const file = transport.options.file 13 | if (file) { 14 | files.push(file) 15 | } 16 | } 17 | } 18 | return files 19 | } 20 | -------------------------------------------------------------------------------- /packages/protobuf/README.md: -------------------------------------------------------------------------------- 1 | # regax-protocol 2 | 3 | ## 参考 4 | 5 | - [Pomelo Protocol](https://github.com/NetEase/pomelo/wiki/Pomelo-%E5%8D%8F%E8%AE%AE) 6 | - [Pomelo data compression](https://github.com/NetEase/pomelo/wiki/Pomelo-%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9%E5%8D%8F%E8%AE%AE) 7 | -------------------------------------------------------------------------------- /packages/protobuf/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es2017" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/protobuf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/protobuf", 3 | "version": "0.1.5", 4 | "license": "MIT", 5 | "main": "lib/protobuf.js", 6 | "typings": "lib/protobuf.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/regaxjs/regax.git" 13 | }, 14 | "homepage": "https://github.com/regaxjs/regax", 15 | "scripts": { 16 | "prepare": "yarn run clean && yarn run build", 17 | "clean": "regax clean", 18 | "build": "regax build", 19 | "watch": "regax watch", 20 | "test": "regax test" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/protobuf/src/protobuf.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regaxjs/regax/9acb14810a90648bd41f45fe49a858a918fe53ea/packages/protobuf/src/protobuf.spec.ts -------------------------------------------------------------------------------- /packages/protobuf/src/protobuf.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | export const Protobuf = undefined 4 | 5 | export interface Protobuf { 6 | encode(...args: any[]): any, 7 | } 8 | -------------------------------------------------------------------------------- /packages/protocol/README.md: -------------------------------------------------------------------------------- 1 | # regax-protocol 2 | 3 | ## 参考 4 | 5 | - [Pomelo Protocol](https://github.com/NetEase/pomelo/wiki/Pomelo-%E5%8D%8F%E8%AE%AE) 6 | - [Pomelo data compression](https://github.com/NetEase/pomelo/wiki/Pomelo-%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9%E5%8D%8F%E8%AE%AE) 7 | -------------------------------------------------------------------------------- /packages/protocol/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es5" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/protocol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/protocol", 3 | "version": "0.1.5", 4 | "license": "MIT", 5 | "main": "lib/protocol.js", 6 | "typings": "lib/protocol.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/regaxjs/regax.git" 13 | }, 14 | "homepage": "https://github.com/regaxjs/regax", 15 | "scripts": { 16 | "prepare": "yarn run clean && yarn run build", 17 | "clean": "regax clean", 18 | "build": "regax build", 19 | "watch": "regax watch", 20 | "test": "regax test" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/rpc/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es2017" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/rpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/rpc", 3 | "version": "0.1.19", 4 | "license": "MIT", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "dependencies": { 8 | "@regax/common": "^0.1.11", 9 | "@regax/logger": "^0.1.7", 10 | "@types/crc": "^3.4.0", 11 | "@types/detect-port": "^1.1.0", 12 | "@types/node-uuid": "^0.0.28", 13 | "@types/node-zookeeper-client": "^0.2.6", 14 | "@types/ws": "^6.0.3", 15 | "address": "^1.1.2", 16 | "crc": "^3.8.0", 17 | "detect-port": "^1.3.0", 18 | "mqtt-connection": "^4.0.0", 19 | "node-uuid": "^1.4.8", 20 | "node-zookeeper-client": "^1.1.0", 21 | "ws": "^7.2.0" 22 | }, 23 | "files": [ 24 | "lib" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/regaxjs/regax.git" 29 | }, 30 | "homepage": "https://github.com/regaxjs/regax", 31 | "scripts": { 32 | "prepare": "yarn run clean && yarn run build", 33 | "clean": "regax clean", 34 | "build": "regax build", 35 | "watch": "regax watch", 36 | "test": "regax test" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/rpc/src/__tests__/client.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as expect from 'expect' 3 | import { expectThrow, TestLogger } from './testUtil' 4 | import { createServer, Server, createClient } from '../index' 5 | 6 | describe('#client', () => { 7 | let server: Server 8 | let logger: TestLogger 9 | beforeEach(async () => { 10 | logger = new TestLogger({}) 11 | server = createServer({ 12 | port: 3333, 13 | // rpcDebugLog: true, 14 | services: { 15 | async plus(a, b): Promise { 16 | return a + b 17 | }, 18 | } 19 | }) 20 | await server.start() 21 | }) 22 | afterEach(() => { 23 | server.stop() 24 | }) 25 | describe('#create', () => { 26 | it('should be ok for creating client with an empty opts', async () => { 27 | const client = createClient() 28 | expect(client).toBeDefined() 29 | await client.start() 30 | client.stop(true) 31 | }) 32 | }) 33 | 34 | describe('#status', () => { 35 | it('should warn if start twice', async () => { 36 | const client = createClient({ logger, rpcDebugLog: true }) 37 | await client.start() 38 | await client.start() 39 | expect(logger.match('has started', 'warn')).toEqual(1) 40 | }) 41 | 42 | it('should ignore the later operation if stop twice', async () => { 43 | const client = createClient({ logger, rpcDebugLog: true }) 44 | client.stop() 45 | client.stop() 46 | expect(logger.match('not running', 'warn')) 47 | }) 48 | 49 | it('should throw an error if try to do rpc invoke when the client not start', async () => { 50 | const client = createClient({ logger, rpcDebugLog: true }) 51 | await expectThrow(() => client.rpcInvoke('server1', 'service1'), 'not running') 52 | }) 53 | 54 | it('should throw an error if try to do rpc invoke after the client stop', async () => { 55 | const client = createClient({ logger, rpcDebugLog: true }) 56 | await client.start() 57 | expect(await client.rpcInvoke(server.serverId, 'plus', [1, 1])).toEqual(2) 58 | client.stop() 59 | await expectThrow(() => client.rpcInvoke(server.serverId, 'plus', [1, 1]), 'not running') 60 | }) 61 | }) 62 | 63 | }) 64 | -------------------------------------------------------------------------------- /packages/rpc/src/__tests__/registry.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { LocalRegistry, ZookeeperRegistry, RemoteCacheRegistry, Registry } from '../' 3 | import * as expect from 'expect' 4 | import { expectThrow } from './testUtil' 5 | import { delay } from '@regax/common' 6 | 7 | function baseRegistryTest(desc: string, RegistryClass: any, opts: any = {}): void { 8 | describe(desc, () => { 9 | let zk: Registry 10 | let zk2: Registry 11 | beforeEach(() => { 12 | zk = new RegistryClass({ 13 | rootPath: '/regax-rpc-test', 14 | ...opts, 15 | }) 16 | zk2 = new RegistryClass({ 17 | rootPath: '/regax-rpc-test', 18 | ...opts, 19 | }) 20 | zk.start() 21 | zk2.start() 22 | }) 23 | afterEach(() => { 24 | zk.stop() 25 | zk2.stop() 26 | }) 27 | it('server registry', async () => { 28 | let info = { 29 | host: 'localhost', 30 | serverType: 'any', 31 | serverId: 'server1', 32 | port: 3333 33 | } 34 | await zk.register(info) 35 | expect(await zk.getAllServers()).toEqual({ [info.serverId]: info }) 36 | expect(await zk.getServerInfo('server1')).toEqual(info) 37 | info = Object.assign(info, { serverType: 'any2' }) 38 | await zk.register(info) 39 | expect(await zk.getAllServers()).toEqual({ [info.serverId]: info }) 40 | expect(await zk2.getAllServers()).toEqual({ [info.serverId]: info }) 41 | expect(await zk.getServerInfo('server1')).toEqual(info) 42 | expect(await zk2.getServerInfo('server1')).toEqual(info) 43 | zk.stop() 44 | await delay(20) 45 | // session disconnect 46 | expect(await zk2.getAllServers()).toEqual({}) 47 | await expectThrow(() => zk.getAllServers(), 'CONNECTION_LOSS') 48 | }) 49 | it('server subscribe and unsubscribe', async () => { 50 | let serverList1: any 51 | let serverList2: any 52 | let c1 = 0 53 | let c2 = 0 54 | function fn1(serverList: any): void { 55 | c1++ 56 | serverList1 = serverList 57 | } 58 | function fn2(serverList: any): void { 59 | c2++ 60 | serverList2 = serverList 61 | } 62 | const unSub1 = zk.subscribe(fn1) 63 | zk2.subscribe(fn2) 64 | await zk.register({ host: 'localhost', serverType: '1', serverId: 'server1', port: 3333 }) 65 | await zk2.register({ host: 'localhost', serverType: '2', serverId: 'server2', port: 4444 }) 66 | await zk.register({ host: 'localhost', serverType: '2', serverId: 'server2', port: 5555 }) 67 | await zk.unRegister('server1') 68 | await delay(100) 69 | expect(serverList1).toEqual({ server2: { host: 'localhost', serverType: '2', serverId: 'server2', port: 5555 }}) 70 | expect(serverList2).toEqual({ server2: { host: 'localhost', serverType: '2', serverId: 'server2', port: 5555 }}) 71 | expect(c1).toEqual(1) 72 | expect(c2).toEqual(1) 73 | unSub1() 74 | await zk2.register({ host: 'localhost', serverType: '2', serverId: 'server3', port: 4444 }) 75 | await delay(100) 76 | expect(serverList1).toEqual({ server2: { host: 'localhost', serverType: '2', serverId: 'server2', port: 5555 }}) // no changed 77 | expect(serverList2).toEqual({ 78 | server2: { host: 'localhost', serverType: '2', serverId: 'server2', port: 5555 }, 79 | server3: { host: 'localhost', serverType: '2', serverId: 'server3', port: 4444 } 80 | }) 81 | expect(c1).toEqual(1) 82 | expect(c2).toEqual(2) 83 | }) 84 | }) 85 | } 86 | 87 | describe('# Registry', () => { 88 | baseRegistryTest('# ZookeeperRegistry', ZookeeperRegistry, { username: 'regax', password: 'regax' }) 89 | baseRegistryTest('# LocalRegistry', LocalRegistry, { changeDelay: 10 }) 90 | baseRegistryTest('# RemoteCacheRegistry', RemoteCacheRegistry, { syncInterval: 10 }) 91 | }) 92 | -------------------------------------------------------------------------------- /packages/rpc/src/__tests__/server.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as expect from 'expect' 3 | import { createServer, Server, createClient, Client, LocalRegistry } from '../index' 4 | import { TestLogger, expectThrow } from './testUtil' 5 | import { delay } from '@regax/common' 6 | const INVOKE_TIMEOUT = 200 7 | 8 | describe('# server', () => { 9 | describe('# server create', () => { 10 | it('should generate an port automatically if no port is provided', async () => { 11 | const server = createServer({ services: {} }) 12 | await server.start() 13 | expect(server.serverInfo).toBeDefined() 14 | const server2 = createServer({ services: {}, ...server.serverInfo, autoport: false }) 15 | await expectThrow(() => server2.start(), 'already in use') // port in use 16 | server.stop() 17 | await server2.start() 18 | server2.stop() 19 | }) 20 | it('should sync to registry when server is created', async () => { 21 | const server = createServer({ services: {} }) 22 | const registry = new LocalRegistry({ }) 23 | registry.start() 24 | await server.start() 25 | let serverMap = await registry.getAllServers() 26 | expect(serverMap[server.serverId]).toEqual(server.serverInfo) 27 | server.stop() 28 | serverMap = await registry.getAllServers() 29 | expect(serverMap[server.serverId]).toEqual(undefined) 30 | registry.stop() 31 | }) 32 | }) 33 | describe('# server rpc invoke', () => { 34 | let server: Server 35 | let client: Client 36 | beforeEach(async () => { 37 | server = createServer({ 38 | // rpcDebugLog: true, 39 | services: { 40 | async plus(a, b): Promise { 41 | return a + b 42 | }, 43 | async invokeError(): Promise { 44 | throw new Error('invoke error message') 45 | }, 46 | async busy(): Promise { 47 | return delay(500) 48 | } 49 | } 50 | }) 51 | client = createClient({ 52 | invokeTimeout: INVOKE_TIMEOUT 53 | // rpcDebugLog: true, 54 | }) 55 | await server.start() 56 | await client.start() 57 | }) 58 | afterEach(async () => { 59 | server.stop() 60 | client.stop(true) 61 | }) 62 | it('should return a value when invoke rpc service', async () => { 63 | const res = await client.rpcInvoke(server.serverId, 'plus', [1, 2]) 64 | expect(res).toEqual(3) 65 | }) 66 | it ('should throw an error invoke rpc when the service invoked with error', async () => { 67 | await expectThrow(() => client.rpcInvoke(server.serverId, 'invokeError'), 'invoke error message') 68 | }) 69 | it('should throw an error when the service is undefined', async () => { 70 | await expectThrow(() => client.rpcInvoke('unknownServer', 'call1'), 'fail to find remote server') 71 | await expectThrow(() => client.rpcInvoke(server.serverId, 'unknownService'), 'no such service') 72 | }) 73 | it ('should invoke timeout when the server is busy', async () => { 74 | await expectThrow(() => client.rpcInvoke(server.serverId, 'busy'), `timeout ${INVOKE_TIMEOUT}`) 75 | }) 76 | it('should invoke pending when the client is connecting', async () => { 77 | const logger = new TestLogger({}) 78 | const client2 = createClient({ 79 | invokeTimeout: INVOKE_TIMEOUT, 80 | logger, 81 | rpcDebugLog: true, 82 | }) 83 | await client2.start() 84 | const call = () => client2.rpcInvoke(server.serverId, 'plus', [1, 2]) 85 | expect(await Promise.all(Array(5).fill(call).map(fn => fn()))) 86 | .toEqual(Array(5).fill(3)) 87 | // console.log(logger.msgs) 88 | expect(logger.match('addToPending')).toEqual(4) 89 | client2.stop(true) 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /packages/rpc/src/__tests__/testUtil.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as util from 'util' 3 | import { Logger } from '@regax/logger' 4 | import { Fn } from '@regax/common' 5 | 6 | export class TestLogger extends Logger { 7 | public msgs: { msg: string, type: string }[] = [] 8 | log(str: string, ...args: any[]): void { 9 | const msg = util.format(str, ...args) 10 | this.msgs.push({ msg, type: 'log' }) 11 | // console.log(msg) 12 | } 13 | info(str: string, ...args: any[]): void { 14 | const msg = util.format(str, ...args) 15 | this.msgs.push({ msg, type: 'info' }) 16 | // console.info(msg) 17 | } 18 | warn(str: string, ...args: any[]): void { 19 | const msg = util.format(str, ...args) 20 | this.msgs.push({ msg, type: 'warn' }) 21 | // console.warn(msg) 22 | } 23 | error(str: string, ...args: any[]): void { 24 | const msg = util.format(str, ...args) 25 | this.msgs.push({ msg, type: 'error' }) 26 | // console.error(...args) 27 | } 28 | debug(str: string, ...args: any[]): void { 29 | const msg = util.format(str, ...args) 30 | this.msgs.push({ msg, type: 'debug' }) 31 | // console.debug(msg) 32 | } 33 | match(str: string | RegExp, type?: string): number { 34 | return this.msgs.filter(m => { 35 | if (type && m.type !== type) return false 36 | return m.msg.match(str) 37 | }).length 38 | } 39 | } 40 | 41 | export async function expectThrow(fn: Fn, msg: string | RegExp): Promise { 42 | let error: Error 43 | try { 44 | await fn() 45 | } catch (e) { 46 | error = e 47 | } 48 | if (!error! || !error!.message.match(msg)) { 49 | throw new Error(`Expect throw message "${msg}" but get: ${error! && error!.message}`) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/rpc/src/client/client.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { MailboxOpts, Mailbox } from './mailboxes' 3 | import { EventEmitter } from '@regax/common' 4 | import { Mailstation, MailstationEvent } from './mailstation' 5 | import { Tracer } from '../util/tracer' 6 | import { Logger, defaultLogger } from '@regax/logger' 7 | import { Registry, ServerInfo } from '../registry/registry' 8 | import { Route, defRoute, Routes } from './router' 9 | 10 | export { 11 | Route, 12 | Routes, 13 | } 14 | 15 | export interface ClientOpts extends MailboxOpts { 16 | createMailbox?: (server: ServerInfo, opts: MailboxOpts) => Mailbox 17 | registry?: Registry 18 | registryRootPath?: string 19 | route?: Route 20 | } 21 | 22 | export enum ClientState { 23 | INITED, 24 | STARTED, 25 | CLOSED 26 | } 27 | 28 | export enum ClientEvent { 29 | ERROR = 'error', 30 | CLOSE = 'close' 31 | } 32 | 33 | export class Client extends EventEmitter { 34 | public state = ClientState.INITED 35 | public station: Mailstation 36 | public readonly clientId: string 37 | protected route: Route = defRoute 38 | protected logger: Logger = defaultLogger 39 | routeCache: { [key: string]: any } = {} 40 | constructor( 41 | readonly opts: ClientOpts 42 | ) { 43 | super() 44 | this.station = new Mailstation(opts) 45 | this.clientId = this.opts.clientId || '' 46 | if (opts.logger) this.logger = opts.logger 47 | if (opts.route) this.route = opts.route 48 | this.station.on(MailstationEvent.CLOSE, this.emit.bind(this, ClientEvent.CLOSE)) 49 | this.station.on(MailstationEvent.ERROR, this.emit.bind(this, ClientEvent.ERROR)) 50 | } 51 | async start(): Promise { 52 | if (this.state > ClientState.INITED) { 53 | this.logger.warn('[regax-rpc] has started.') 54 | return 55 | } 56 | await this.station.start() 57 | this.state = ClientState.STARTED 58 | } 59 | stop(force?: boolean): void { 60 | if (this.state !== ClientState.STARTED) { 61 | this.logger.warn('[regax-rpc] client is not running now.') 62 | return 63 | } 64 | this.state = ClientState.CLOSED 65 | this.station.stop(force) 66 | } 67 | getServersByType(serverType: string): string[] { 68 | return this.station.serversMap[serverType] || [] 69 | } 70 | async rpcInvokeByRoute(serverType: string, serviceName: string, args: any[], searchSeed?: string): Promise { 71 | const servers = serverType === '*' ? this.getServersByType('*') : this.getServersByType(serverType).concat(this.getServersByType('*')) 72 | if (servers.length === 0) { 73 | throw new Error('[regax-rpc] cannot find server info by type:' + serverType) 74 | } 75 | const serverId = this.route(servers, { serverType, serviceName, args }, searchSeed, this) 76 | return this.rpcInvoke(serverId, serviceName, args) 77 | } 78 | /** 79 | * Do the rpc invoke directly. 80 | * 81 | * @param serverId - remote server id 82 | * @param serviceName - service name 83 | * @param args - service args 84 | */ 85 | async rpcInvoke(serverId: string, serviceName: string, args: any[] = []): Promise { 86 | let tracer: Tracer | undefined 87 | 88 | if (this.opts.rpcDebugLog) { 89 | tracer = new Tracer(this.logger, this.clientId, serverId, { service: serviceName, args }) 90 | tracer.info('client', 'rpcInvoke', 'the entrance of rpc invoke') 91 | } 92 | 93 | if (this.state !== ClientState.STARTED) { 94 | if (tracer) tracer.error('client', 'rpcInvoke', 'fail to do rpc invoke for client is not running') 95 | this.logger.error('[regax-rpc] fail to do rpc invoke for client is not running') 96 | throw new Error('[regax-rpc] fail to do rpc invoke for client is not running') 97 | } 98 | return this.station.dispatch(serverId, { service: serviceName, args }, tracer) 99 | } 100 | } 101 | 102 | export function createClient(opts: ClientOpts = {}): Client { 103 | return new Client(opts) 104 | } 105 | -------------------------------------------------------------------------------- /packages/rpc/src/client/mailboxes/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { EventEmitter } from '@regax/common' 3 | import { Logger } from '@regax/logger' 4 | import { Tracer } from '../../util/tracer' 5 | // import { MqttMailbox } from './mqttMailbox' 6 | import { WSMailbox } from './wsMailbox' 7 | import { ServerInfo } from '../../server/server' 8 | 9 | export const DEFAULT_CONNECT_TIMEOUT = 5000 10 | export const DEFAULT_KEEPALIVE = 10 * 1000 11 | export const DEFAULT_INVOKE_TIMEOUT = 10 * 1000 12 | export const DEFAULT_FLUSH_INTERVAL = 50 13 | 14 | export interface MailboxOpts { 15 | clientId?: string, 16 | logger?: Logger 17 | bufferMsg?: boolean, 18 | keepalive?: number 19 | invokeTimeout?: number 20 | connectTimeout?: number 21 | flushInterval?: number 22 | rpcDebugLog?: boolean 23 | } 24 | 25 | export enum MailboxEvent { 26 | ERROR = 'error', 27 | CLOSE = 'close', 28 | } 29 | 30 | export interface Mailbox extends EventEmitter { 31 | close(): void 32 | connect(tracer?: Tracer): Promise 33 | send(msg: any, tracer?: Tracer): Promise 34 | } 35 | 36 | export function createMailbox(server: ServerInfo, opts: MailboxOpts): Mailbox { 37 | return new WSMailbox(server, opts) 38 | } 39 | -------------------------------------------------------------------------------- /packages/rpc/src/client/router.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | const crc = require('crc') 3 | 4 | export interface Route { 5 | (servers: string[], msg: RouteMsgData, searchSeed?: string, client?: any): string 6 | } 7 | export interface RouteMsgData { 8 | serverType: string, 9 | serviceName?: string, 10 | args?: any[] 11 | } 12 | 13 | /** 14 | * Calculate route info and return an appropriate server id. 15 | */ 16 | export function defRoute(servers: string[], msg: RouteMsgData, searchSeed?: string): string { 17 | const index = Math.abs(crc.crc32(searchSeed || Math.random().toString())) % servers.length 18 | return servers[index] 19 | } 20 | 21 | /** 22 | * Random algorithm for calculating server id. 23 | */ 24 | export function rdRoute(servers: string[]): string { 25 | const index = Math.floor(Math.random() * servers.length) 26 | return servers[index] 27 | } 28 | 29 | /** 30 | * Round-Robin algorithm for calculating server id. 31 | */ 32 | export function rrRoute(servers: string[], msg: RouteMsgData, searchSeed?: string, client?: any): string { 33 | if (!client._rrParam) { 34 | client._rrParam = {} 35 | } 36 | let index 37 | if (client._rrParam[msg.serverType]) { 38 | index = client._rrParam[msg.serverType] 39 | } else { 40 | index = 0 41 | } 42 | const serverId = servers[index % servers.length] 43 | if (index++ === Number.MAX_VALUE) { 44 | index = 0 45 | } 46 | client._rrParam[msg.serverType] = index 47 | return serverId 48 | } 49 | 50 | const Routes: { [routeType: string]: Route } = { 51 | df: defRoute, 52 | rr: rrRoute, 53 | rd: rdRoute, 54 | } 55 | 56 | export { 57 | Routes 58 | } 59 | -------------------------------------------------------------------------------- /packages/rpc/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './registry/registry' 2 | export * from './server/server' 3 | export * from './client/client' 4 | 5 | export * from './util/constants' 6 | export * from './util/remoteCache' 7 | export * from './util/tracer' 8 | -------------------------------------------------------------------------------- /packages/rpc/src/registry/registry.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@regax/common' 2 | import { Logger } from '@regax/logger' 3 | import { ServerInfo, ServerMap } from '../server/server' 4 | 5 | export enum RegistryEvent { 6 | CONNECTION = 'connection', 7 | DISCONNECT = 'disconnect', 8 | CHANGED = 'changed', 9 | ERROR = 'error' 10 | } 11 | 12 | export { 13 | ServerInfo, 14 | ServerMap, 15 | } 16 | 17 | export const REGISTRY_CHANGE_DELAY = 10 18 | 19 | export interface RegistryOpts { 20 | rootPath?: string 21 | logger?: Logger 22 | } 23 | 24 | export interface Registry extends EventEmitter { 25 | start(): void 26 | stop(): void 27 | isConnected(): boolean, 28 | register(serverInfo: ServerInfo): Promise 29 | unRegister(serverId: string): Promise 30 | getServerInfo(serverId: string): Promise 31 | getAllServers(): Promise 32 | subscribe(fn: (servers: ServerMap) => void): () => void 33 | } 34 | 35 | // registries 36 | export * from './registries/localRegistry' 37 | export * from './registries/zookeeperRegistry' 38 | export * from './registries/remoteCacheRegistry' 39 | -------------------------------------------------------------------------------- /packages/rpc/src/server/acceptors/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { EventEmitter } from '@regax/common' 3 | import { Logger } from '@regax/logger' 4 | // import { MqttAcceptor } from './mqttAcceptor' 5 | import { WSAcceptor } from './wsAcceptor' 6 | import { Tracer } from '../../util/tracer' 7 | 8 | export interface AcceptorOpts { 9 | logger?: Logger 10 | bufferMsg?: boolean 11 | rpcDebugLog?: boolean 12 | flushInterval?: number // flush interval in ms 13 | } 14 | 15 | export interface AcceptorDispacher { 16 | (serviceName: string, args: any[], tracer?: Tracer): Promise 17 | } 18 | 19 | export enum AcceptorEvent { 20 | ERROR = 'error', 21 | CLOSE = 'close', 22 | LISTENING = 'listening', 23 | DISCONNECT = 'disconnect', 24 | CONNECTION = 'connection', 25 | } 26 | 27 | export interface Acceptor extends EventEmitter { 28 | listen(port: number): void 29 | close(): void 30 | } 31 | 32 | export function createAcceptor(opts: AcceptorOpts, dispatcher: AcceptorDispacher): Acceptor { 33 | return new WSAcceptor(opts, dispatcher) 34 | } 35 | -------------------------------------------------------------------------------- /packages/rpc/src/typings/mqtt-connection.d.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | declare module 'mqtt-connection' { 3 | function Connection(...args: any): any 4 | export = Connection 5 | } 6 | -------------------------------------------------------------------------------- /packages/rpc/src/util/constants.ts: -------------------------------------------------------------------------------- 1 | export enum RPC_ERROR { 2 | SERVER_NOT_STARTED = 'RPC_SERVER_NOT_STARTED', 3 | SERVER_NOT_FOUND = 'RPC_SERVER_NOT_FOUND', 4 | SERVER_CLOSED = 'RPC_SERVER_CLOSED', 5 | SERVICE_NOT_FOUND = 'RPC_SERVICE_NOT_FOUND', 6 | FAIL_CONNECT_SERVER = 'RPC_FAIL_CONNECT_SERVER', 7 | FAIL_SEND_MESSAGE = 'RPC_FAIL_SEND_MESSAGE', 8 | } 9 | 10 | export const RPC_DEFAULT_ROOT_PATH = '/regax-rpc' 11 | -------------------------------------------------------------------------------- /packages/rpc/src/util/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { delay } from '@regax/common' 3 | 4 | export async function retry(fn: () => Promise, times: 3, delayMs: 100): Promise { 5 | let result: T 6 | while (times > 0) { 7 | times -- 8 | result = await fn() 9 | if (result !== undefined) return result 10 | await delay(delayMs) 11 | } 12 | } 13 | 14 | export function normalizeDirPath(p: string): string { 15 | p = p.endsWith('/') ? p.slice(0, -1) : p 16 | return p.startsWith('/') ? p : `/${p}` 17 | } 18 | -------------------------------------------------------------------------------- /packages/rpc/src/util/tracer.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as uuid from 'node-uuid' 3 | import { Logger } from '@regax/logger' 4 | 5 | export class Tracer { 6 | constructor( 7 | protected readonly logger: Logger, 8 | readonly source: any, 9 | readonly remote: any, 10 | readonly msg: any, 11 | readonly id: string = uuid.v1(), 12 | public seq: number = 1 13 | ) { 14 | } 15 | getLogger(role: string, method: string, desc: string): any { 16 | return { 17 | traceId: this.id, 18 | seq: this.seq++, 19 | role: role, 20 | source: this.source, 21 | remote: this.remote, 22 | method, 23 | msg: this.msg, 24 | timestamp: Date.now(), 25 | description: desc 26 | } 27 | } 28 | info(role: string, method: string, desc: string): void { 29 | this.logger.info(JSON.stringify(this.getLogger(role, method, desc))) 30 | } 31 | debug(role: string, method: string, desc: string): void { 32 | this.logger.debug(JSON.stringify(this.getLogger(role, method, desc))) 33 | } 34 | error(role: string, method: string, desc: string): void { 35 | this.logger.error(JSON.stringify(this.getLogger(role, method, desc))) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/scheduler/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es2017" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/scheduler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/scheduler", 3 | "version": "0.0.5", 4 | "license": "MIT", 5 | "main": "lib/index", 6 | "typings": "lib/index.d.ts", 7 | "regaxPlugin": { 8 | "dir": "lib" 9 | }, 10 | "dependencies": { 11 | "@regax/common": "^0.1.11", 12 | "@regax/logger": "^0.1.7" 13 | }, 14 | "peerDependencies": { 15 | "@regax/server": "^0.1.21" 16 | }, 17 | "files": [ 18 | "lib" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/regaxjs/regax.git" 23 | }, 24 | "homepage": "https://github.com/regaxjs/regax", 25 | "scripts": { 26 | "prepare": "yarn run clean && yarn run build", 27 | "clean": "regax clean", 28 | "build": "regax build", 29 | "watch": "regax watch", 30 | "test": "regax test" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/scheduler/src/component/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { inject, Component, injectable, Application } from '@regax/server' 2 | 3 | @injectable() 4 | export default class SchedulerComponent implements Component { 5 | constructor( 6 | @inject('app') protected readonly app: Application, 7 | ) { 8 | } 9 | onStart(): void { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/scheduler/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { Application, ApplicationOpts } from '@regax/server' 2 | 3 | export default function configDefault(app: Application): ApplicationOpts { 4 | return { 5 | component: { 6 | all: ['scheduler'] 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/scheduler/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component/scheduler' 2 | -------------------------------------------------------------------------------- /packages/server/bin/regax: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/master/childProcess') 4 | -------------------------------------------------------------------------------- /packages/server/compile.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/base.tsconfig", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "baseUrl": ".", 7 | "target": "es2017" 8 | }, 9 | "include": [ 10 | "src" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@regax/server", 3 | "version": "0.1.31", 4 | "license": "MIT", 5 | "main": "lib/index", 6 | "typings": "lib/index.d.ts", 7 | "dependencies": { 8 | "@regax/common": "^0.1.11", 9 | "@regax/logger": "^0.1.7", 10 | "@regax/logrotator": "^0.0.5", 11 | "@regax/protocol": "^0.1.5", 12 | "@regax/rpc": "^0.1.19", 13 | "@regax/scheduler": "^0.0.5", 14 | "@types/commander": "^2.12.2", 15 | "@types/ws": "^6.0.3", 16 | "address": "^1.1.2", 17 | "await-event": "^2.1.0", 18 | "commander": "^4.1.0", 19 | "debug": "^4.1.1", 20 | "inversify": "^5.0.1", 21 | "ps-tree": "^1.2.0", 22 | "reflect-metadata": "^0.1.13", 23 | "ws": "^7.2.0" 24 | }, 25 | "files": [ 26 | "lib", 27 | "bin" 28 | ], 29 | "devDependencies": { 30 | "@regax/client-udpsocket": "^0.0.2", 31 | "@regax/client-websocket": "^0.1.16" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/regaxjs/regax.git" 36 | }, 37 | "homepage": "https://github.com/regaxjs/regax", 38 | "scripts": { 39 | "prepare": "yarn run clean && yarn run build", 40 | "clean": "regax clean", 41 | "build": "regax build", 42 | "watch": "regax watch", 43 | "test": "regax test" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/connector/udpConnector.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Client } from '@regax/client-udpsocket' 3 | import * as expect from 'expect' 4 | import { Application } from '../../' 5 | import { delay } from '@regax/common' 6 | const assert = require('assert') 7 | const dgram = require('dgram') 8 | const path = require('path') 9 | 10 | describe('udpConnector', () => { 11 | let client: Client 12 | let server: Application 13 | function createClient(): Client { 14 | return new Client({ 15 | host: '127.0.0.1', 16 | port: 33333, 17 | creatSocket: t => dgram.createSocket(t) 18 | }) 19 | } 20 | beforeEach(() => { 21 | client = createClient() 22 | server = new Application( 23 | path.join(__dirname, '../../../lib/__tests__/template'), 24 | 'master', 25 | { 26 | master: { 27 | servers: [ 28 | { serverType: 'connector', clientPort: 33333, clientType: 'udp' }, 29 | { serverType: 'chat' }, 30 | { serverType: 'chat' } 31 | ] 32 | }, 33 | udpConnector: { 34 | heartbeatInterval: 100, 35 | } 36 | } 37 | ) 38 | }) 39 | afterEach(async () => { 40 | if (client) client.disconnect() 41 | if (server ) await server.stop() 42 | }) 43 | it('should start with heatbeat', async () => { 44 | let heartbeatTimes = 0 45 | client.on('heartbeat', () => { 46 | heartbeatTimes ++ 47 | }) 48 | await server.start() 49 | await client.connect() 50 | await delay(500) 51 | assert(heartbeatTimes > 3) 52 | }) 53 | it('should send msg success by client', async () => { 54 | await server.start() 55 | const c1Room: any = {} 56 | const c2Room: any = {} 57 | const c1 = createClient() 58 | const c2 = createClient() 59 | c1.on('onChat', (d: any) => { 60 | c1Room[d.from] = d 61 | }) 62 | c2.on('onChat', (d: any) => { 63 | c2Room[d.from] = d 64 | }) 65 | await c1.connect() 66 | await c2.connect() 67 | await c1.request('connector.user.enter', { rid: 'room1', username: 'client1' }) 68 | await c2.request('connector.user.enter', { rid: 'room1', username: 'client2' }) 69 | await c1.request('chat.chat.send', { target: '*', content: 'hello world' }) 70 | await c2.request('chat.chat.send', { target: '*', content: 'hello world' }) 71 | const roomMsgs = { 72 | client1: { msg: 'hello world', from: 'client1', target: '*' }, 73 | client2: { msg: 'hello world', from: 'client2', target: '*' }, 74 | } 75 | await delay(10) 76 | expect(c1Room).toEqual(roomMsgs) 77 | expect(c2Room).toEqual(roomMsgs) 78 | c1.disconnect() 79 | c2.disconnect() 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/connector/udpStickyServer.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Client } from '@regax/client-udpsocket' 3 | import * as expect from 'expect' 4 | import { Application } from '../../' 5 | import { delay } from '@regax/common' 6 | const assert = require('assert') 7 | const dgram = require('dgram') 8 | const path = require('path') 9 | 10 | describe.skip('udpStickyServer', () => { 11 | let client: Client 12 | let server: Application 13 | function createClient(): Client { 14 | return new Client({ 15 | host: '127.0.0.1', 16 | port: 33333, 17 | creatSocket: t => dgram.createSocket(t) 18 | }) 19 | } 20 | beforeEach(() => { 21 | client = createClient() 22 | server = new Application( 23 | path.join(__dirname, '../../../lib/__tests__/template'), 24 | 'master', 25 | { 26 | master: { 27 | servers: [ 28 | { serverType: 'connector', sticky: true, clientPort: 33333, clientType: 'udp' }, 29 | // { serverType: 'connector', sticky: true, clientPort: 33333, clientType: 'udp' }, 30 | { serverType: 'chat' }, 31 | { serverType: 'chat' } 32 | ] 33 | }, 34 | udpConnector: { 35 | heartbeatInterval: 100, 36 | } 37 | } 38 | ) 39 | }) 40 | afterEach(async () => { 41 | if (client) client.disconnect() 42 | if (server ) await server.stop() 43 | }) 44 | it('should start with heatbeat', async () => { 45 | let heartbeatTimes = 0 46 | client.on('heartbeat', () => { 47 | heartbeatTimes ++ 48 | }) 49 | await server.start() 50 | await client.connect() 51 | await delay(500) 52 | assert(heartbeatTimes > 3) 53 | }) 54 | it('should send msg success by client', async () => { 55 | await server.start() 56 | const c1Room: any = {} 57 | const c2Room: any = {} 58 | const c1 = createClient() 59 | const c2 = createClient() 60 | c1.on('onChat', (d: any) => { 61 | c1Room[d.from] = d 62 | }) 63 | c2.on('onChat', (d: any) => { 64 | c2Room[d.from] = d 65 | }) 66 | await c1.connect() 67 | await c2.connect() 68 | await c1.request('connector.user.enter', { rid: 'room1', username: 'client1' }) 69 | await c2.request('connector.user.enter', { rid: 'room1', username: 'client2' }) 70 | await c1.request('chat.chat.send', { target: '*', content: 'hello world' }) 71 | await c2.request('chat.chat.send', { target: '*', content: 'hello world' }) 72 | const roomMsgs = { 73 | client1: { msg: 'hello world', from: 'client1', target: '*' }, 74 | client2: { msg: 'hello world', from: 'client2', target: '*' }, 75 | } 76 | await delay(10) 77 | expect(c1Room).toEqual(roomMsgs) 78 | expect(c2Room).toEqual(roomMsgs) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/filter.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { TimeoutFilter, ApplicationOpts } from '../' 3 | import { createTestApp } from './testUtil' 4 | 5 | // const assert = require('assert') 6 | 7 | describe('filter', () => { 8 | it('add filters', async () => { 9 | const configs: ApplicationOpts = { 10 | filters: { 11 | timeout: TimeoutFilter 12 | }, 13 | filterConfigs: { 14 | timeout: { 15 | maxSize: 10, 16 | } 17 | } 18 | } 19 | const app = createTestApp('connector') 20 | app.setConfig(configs) 21 | await app.start() 22 | // app.get('router').localRouter 23 | await app.stop() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/master.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as path from 'path' 3 | import { Application } from '../application' 4 | import { ServerMap } from '@regax/rpc' 5 | import * as ws from 'ws' 6 | import { Client } from '@regax/client-websocket' 7 | 8 | const assert = require('assert') 9 | 10 | describe('master', () => { 11 | let serverMap: ServerMap = {} 12 | const assertCount = (serverType: string, count: number) => assert.ok(Object.keys(serverMap).filter(w => serverMap[w].serverType === serverType).length === count) 13 | const filter = (serverType: string) => Object.keys(serverMap).filter(k => serverMap[k].serverType === serverType).map(k => serverMap[k]) 14 | beforeEach(() => { 15 | serverMap = {} 16 | }) 17 | it('start master app with multi process', async () => { 18 | const app = new Application( 19 | path.join(__dirname, '../../lib/__tests__/template'), 20 | 'master', 21 | ) 22 | app.setConfig({ 23 | master: { 24 | servers: [ 25 | { serverType: 'gate', clientPort: 3021 }, 26 | { serverType: 'connector', clientPort: 3022 }, 27 | { serverType: 'connector', clientPort: 3023 }, 28 | { serverType: 'chat', port: 3024 }, 29 | { serverType: 'chat', port: 3025 }, 30 | ], 31 | } 32 | }) 33 | await app.start() 34 | serverMap = await app.getAllServers() 35 | assert.ok(Object.keys(serverMap), 6) 36 | assertCount('master', 1) 37 | assertCount('gate', 1) 38 | assertCount('connector', 2) 39 | assertCount('chat', 2) 40 | const c1 = new Client({ url: `ws://127.0.0.1:${3021}`, reconnect: false, WebSocket: ws }) 41 | const c2 = new Client({ url: `ws://127.0.0.1:${3023}`, reconnect: false, WebSocket: ws }) 42 | await c1.connect() 43 | await c2.connect() 44 | await c1.request('connector.user.enter', { rid: 'room1', username: 'client1' }) 45 | await c2.request('connector.user.enter', { rid: 'room1', username: 'client2' }) 46 | c1.disconnect() 47 | c2.disconnect() 48 | await app.stop() 49 | }) 50 | it('start master app by sticky mode', async () => { 51 | const app = new Application( 52 | path.join(__dirname, '../../lib/__tests__/template'), 53 | 'master', 54 | ) 55 | const STICKY_PORT = 3001 56 | app.setConfig({ 57 | master: { 58 | servers: [ 59 | { serverType: 'gate' }, 60 | { serverType: 'connector', sticky: true, clientPort: STICKY_PORT }, 61 | { serverType: 'connector', sticky: true, clientPort: STICKY_PORT }, 62 | { serverType: 'chat' }, 63 | { serverType: 'chat' }, 64 | ], 65 | }, 66 | }) 67 | await app.start() 68 | serverMap = await app.getAllServers() 69 | assert.ok(Object.keys(serverMap), 6) 70 | assertCount('master', 1) 71 | assertCount('gate', 1) 72 | assertCount('connector', 2) 73 | assertCount('chat', 2) 74 | filter('connector').forEach((c: any) => assert.ok(c.clientPort === STICKY_PORT)) 75 | filter('gate').forEach((c: any) => assert.ok(c.clientPort !== STICKY_PORT)) // sticky only support connector server 76 | const c1 = new Client({ url: `ws://127.0.0.1:${STICKY_PORT}`, reconnect: false, WebSocket: ws }) 77 | const c2 = new Client({ url: `ws://127.0.0.1:${STICKY_PORT}`, reconnect: false, WebSocket: ws }) 78 | await c1.connect() 79 | await c2.connect() 80 | await c1.request('connector.user.enter', { rid: 'room1', username: 'client1' }) 81 | await c2.request('connector.user.enter', { rid: 'room1', username: 'client2' }) 82 | c1.disconnect() 83 | c2.disconnect() 84 | await app.stop() 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/monitor.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | import { Application, Monitor } from '../index' 4 | import * as expect from 'expect' 5 | import { createTemplateServers, defaultServerConfigs } from './template' 6 | import { delay } from '@regax/common' 7 | 8 | describe.skip('component.monitor', () => { 9 | let servers: Application[] 10 | let monitor: Monitor 11 | beforeEach(async () => { 12 | servers = await createTemplateServers() 13 | monitor = new Monitor({ 14 | keepalive: 10, 15 | invokeTimeout: 1000, 16 | }) 17 | await monitor.start() 18 | }) 19 | afterEach(async () => { 20 | await Promise.all(servers.map(s => s.stop())) 21 | monitor.stop() 22 | }) 23 | it('api.getAllServers', async () => { 24 | const data = await monitor.getAllServers() 25 | expect(Object.keys(data).length).toEqual(defaultServerConfigs.length) 26 | }) 27 | it('api.checkAllServersAlive', async () => { 28 | const serverIds = Object.keys(await monitor.getAllServers()) 29 | const unAliveServers = await monitor.checkAllServersAlive() 30 | expect(unAliveServers).toEqual([]) 31 | await servers[0].stop() 32 | expect(await monitor.checkAllServersAlive()).toEqual([]) 33 | expect(await monitor.checkAllServersAlive(serverIds)).toEqual([servers[0].serverId]) 34 | }) 35 | it('api.checkServerAlive', async () => { 36 | expect(await monitor.checkServerAlive('UnknownServerId')).toEqual(false) 37 | const serverId = servers[0].serverId 38 | expect(await monitor.checkServerAlive(serverId)).toEqual(true) 39 | await servers[0].stop() 40 | expect(await monitor.checkServerAlive(serverId)).toEqual(false) 41 | }) 42 | it.skip('api.clearDiedServers', async () => { 43 | const serverIds = Object.keys(await monitor.getAllServers()) 44 | await servers[0].stop() 45 | expect(await monitor.clearDiedServers(serverIds)).toEqual([servers[0].serverId]) 46 | }) 47 | it.skip('api.stopServer', async () => { 48 | const serverId = servers[0].serverId 49 | await monitor.stopServer(serverId, 10) 50 | await delay(10) 51 | expect(await monitor.checkServerAlive(serverId)).toEqual(false) 52 | }) 53 | it.skip('api.restartServer', async () => { 54 | const serverId = servers[0].serverId 55 | expect(await monitor.checkServerAlive(serverId)).toEqual(true) 56 | await monitor.restartServer(serverId, 10) 57 | await delay(20) 58 | expect(await monitor.checkServerAlive(serverId)).toEqual(true) 59 | await monitor.stopServer(serverId, 10) 60 | await delay(20) 61 | expect(await monitor.checkServerAlive(serverId)).toEqual(false) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as path from 'path' 3 | import { Application } from '../application' 4 | import * as expect from 'expect' 5 | 6 | const assert = require('assert') 7 | 8 | describe('plugin', () => { 9 | it('start app with plugins', async () => { 10 | const plugins = { 11 | plugin1: { enable: true, path: path.join(__dirname, 'plugins/plugin1') } 12 | } 13 | const app = new Application( 14 | path.join(__dirname, '../../lib/__tests__/template'), 15 | 'master', 16 | { loader: { plugins } }, 17 | ) 18 | await app.start() 19 | assert(app.get('scheduler')) // bultin plugins 20 | assert(app.get('plugin1')) 21 | await app.stop() 22 | }) 23 | it('disable plugin', async () => { 24 | const plugins = { 25 | plugin1: { enable: true, path: path.join(__dirname, 'plugins/plugin1') }, 26 | scheduler: { enable: false } 27 | } 28 | const app = new Application( 29 | path.join(__dirname, '../../lib/__tests__/template'), 30 | 'master', 31 | { loader: { plugins } }, 32 | ) 33 | await app.start() 34 | expect(() => app.get('scheduler')).toThrowError('No matching bindings') 35 | assert(app.get('plugin1')) 36 | await app.stop() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/rpc.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | import * as ws from 'ws' 4 | import * as expect from 'expect' 5 | import { Application, ApplicationOpts } from '../index' 6 | import { Client } from '@regax/client-websocket' 7 | import { createTemplateServers } from './template' 8 | import { delay } from '@regax/common' 9 | 10 | const host = require('address').ip() 11 | 12 | const serverConfigs: ApplicationOpts[] = [ 13 | { serverType: 'gate', connector: { port: 8091, maxConnectionCount: 2 } }, 14 | { serverType: 'connector', connector: { port: 8092, maxConnectionCount: 2 } }, 15 | { serverType: 'connector', connector: { port: 8093, maxConnectionCount: 2 } }, 16 | { serverType: 'chat' }, 17 | { serverType: 'chat' }, 18 | { serverType: 'chat' }, 19 | ] 20 | describe('regax rpc invoke', () => { 21 | let servers: Application[] 22 | let c1: Client 23 | let c2: Client 24 | beforeEach(async () => { 25 | servers = await createTemplateServers(serverConfigs) 26 | c1 = new Client({ url: `ws://${host}:8092`, reconnect: false, WebSocket: ws }) 27 | c2 = new Client({ url: `ws://${host}:8093`, reconnect: false, WebSocket: ws }) 28 | await c1.connect() 29 | await c2.connect() 30 | }) 31 | afterEach(async () => { 32 | await Promise.all(servers.map(s => s.stop())) 33 | c1.disconnect() 34 | c2.disconnect() 35 | }) 36 | it('should send msg success by client', async () => { 37 | const c1Room: any = [] 38 | const c2Room: any = [] 39 | c1.on('onChat', (d: any) => { 40 | c1Room.push(d) 41 | }) 42 | c2.on('onChat', (d: any) => { 43 | c2Room.push(d) 44 | }) 45 | await c1.request('connector.user.enter', { rid: 'room1', username: 'client1' }) 46 | await c2.request('connector.user.enter', { rid: 'room1', username: 'client2' }) 47 | await c1.request('chat.chat.send', { target: '*', content: 'hello world' }) 48 | await c2.request('chat.chat.send', { target: '*', content: 'hello world' }) 49 | const roomMsgs = [ 50 | { msg: 'hello world', from: 'client1', target: '*' }, 51 | { msg: 'hello world', from: 'client2', target: '*' } 52 | ] 53 | await delay(10) 54 | expect(c1Room).toEqual(roomMsgs) 55 | expect(c2Room).toEqual(roomMsgs) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/template/config/config.default.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Application } from '../../../' 3 | 4 | export default function configDefault(app: Application): any { 5 | return { 6 | connector: { 7 | port: 8083, 8 | }, 9 | testConfig: {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/template/config/router.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { RouteData } from '../../../' 3 | import { BackendSession } from '../../../service/backendSessionService' 4 | import { Routes } from '@regax/rpc' 5 | 6 | export function chat(servers: string[], session: BackendSession, msg: any, route: RouteData): string { 7 | const rid: string = session.get('rid') as string 8 | if (!rid) throw new Error('miss chat room') 9 | return Routes.df(servers, route, rid) 10 | } 11 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/template/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { Application, ApplicationOpts } from '../../index' 3 | 4 | export const defaultServerConfigs: ApplicationOpts[] = [ 5 | { serverType: 'gate', connector: { port: 8091 } }, 6 | { serverType: 'connector', connector: { port: 8092 } }, 7 | { serverType: 'connector', connector: { port: 8093 } }, 8 | { serverType: 'chat' }, 9 | { serverType: 'chat' }, 10 | { serverType: 'chat' }, 11 | ] 12 | export async function createTemplateServers(configs = defaultServerConfigs): Promise { 13 | const apps: Application[] = [] 14 | for (let i = 0; i < configs.length; i ++) { 15 | const c = configs[i] 16 | const app = new Application(path.join(__dirname, '../template'), c.serverType) 17 | app.setConfig(Object.assign(c, { 18 | rpc: { 19 | canInvokeLocal: false 20 | } 21 | })) 22 | await app.start() 23 | apps.push(app) 24 | } 25 | return apps 26 | } 27 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/template/server/chat/controller/chat.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Controller } from '../../../../../' 3 | 4 | export default class ChatController extends Controller { 5 | /** 6 | * Send messages to users 7 | * @param msg 8 | */ 9 | send(msg: any): void { 10 | const { session } = this.ctx 11 | const rid = session.get('rid') // room id 12 | const username = (session.uid as string).split('*')[0] 13 | const param = { 14 | msg: msg.content, 15 | from: username, 16 | target: msg.target 17 | } 18 | const channel = this.service.channel.getChannel(rid as string, true) 19 | if (msg.target === '*') { 20 | // the target is all users 21 | channel!.pushMessage('onChat', param) 22 | } else { 23 | // the target is specific user 24 | // const tuid = msg.target + '*' + rid 25 | // const tsid = channel!.getMember(tuid)['sid'] 26 | // channel!.pushMessageByUids('onChat', param, [{ 27 | // uid: tuid, 28 | // sid: tsid 29 | // } ]) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/template/server/chat/rpc/room.ts: -------------------------------------------------------------------------------- 1 | import { RPC } from '../../../../../' 2 | 3 | export default class ChatRPC extends RPC { 4 | add(rid: string, uid: string, serverId: string): boolean { 5 | const channel = this.service.channel.createChannel(rid) 6 | channel.add(uid, serverId) 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/template/server/connector/controller/user.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Controller } from '../../../../../' 3 | 4 | export default class EntryController extends Controller { 5 | /** 6 | * New client entry chat server. 7 | * 8 | * @param {Object} msg request message 9 | */ 10 | async enter(msg: { rid: string, username: string }): Promise { 11 | const rid = msg.rid // room id 12 | const uid = msg.username + '*' + rid 13 | const { session } = this.ctx 14 | // rebind 15 | if (session.uid) { 16 | await session.unbind(session.uid) 17 | } 18 | await session.bind(uid) 19 | session.set('rid', rid) 20 | await session.push('rid') // 同步数据 21 | session.on(session.event.CLOSED, () => { 22 | // TODO 23 | }) 24 | return this.rpc.chat.room.add(rid, uid, this.app.serverId) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/server/src/__tests__/testUtil.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as expect from 'expect' 3 | import * as path from 'path' 4 | import { Fn } from '@regax/common' 5 | import { Application } from '../' 6 | 7 | export async function expectThrow(fn: Fn, msg: string | RegExp): Promise { 8 | let error: Error 9 | try { 10 | await fn() 11 | } catch (e) { 12 | error = e 13 | } 14 | expect(error!).toBeDefined() 15 | expect(error!.message).toMatch(msg) 16 | } 17 | 18 | export function createTestApp(serverType: string = 'master'): Application { 19 | return new Application( 20 | path.join(__dirname, '../../lib/__tests__/template'), 21 | serverType, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/server/src/api/application.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { ServerInfo } from '@regax/rpc' 3 | import { RegaxLoggerManagerOpts, RegaxLoggerOpts } from '@regax/logger' 4 | import { RPCOpts } from '../component/rpc' 5 | import { ConnectorOpts } from '../component/connector' 6 | import { RouterOpts } from '../component/router' 7 | import { SessionOpts } from '../component/session' 8 | import { MasterOpts } from '../component/master' 9 | import { AgentOpts } from '../component/agent' 10 | import { ComponentOpts } from './component' 11 | import { Connector, StickyServer } from './connector' 12 | import { UDPConnectorOpts } from '../connector/udpConnector' 13 | import { WebSocketConnectorOpts } from '../connector/wsConnector' 14 | import { LoaderOpts } from '../loader/loader' 15 | import { Application } from '../application' 16 | import { FilterCreator, FilterConfig } from './filter' 17 | import { ObjectOf } from '@regax/common' 18 | 19 | export enum ApplicationEnv { 20 | local = 'local', 21 | prod = 'prod', 22 | test = 'test', 23 | unittest = 'unittest' 24 | } 25 | 26 | export interface ApplicationOpts { 27 | app?: { 28 | serverVersion?: string, // Application Server version 29 | } 30 | router?: RouterOpts, 31 | connector?: ConnectorOpts, 32 | connectorRegistries?: { [clientType: string]: { new(app: Application): Connector } } 33 | udpConnector?: UDPConnectorOpts 34 | wsConnector?: WebSocketConnectorOpts 35 | stickyServerRegistries?: { [clientType: string]: { new(port: number, app: Application): StickyServer } } 36 | session?: SessionOpts, 37 | filters?: ObjectOf, 38 | filterConfigs?: ObjectOf 39 | rpc?: RPCOpts, 40 | master?: MasterOpts 41 | agent?: AgentOpts 42 | loader?: LoaderOpts, // only use when app preload 43 | logrotator?: { 44 | filesRotateByHour?: string[], // for rotate_by_hour 45 | filesRotateBySize?: string[], // for rotate_by_size 46 | hourDelimiter?: string, // default '-' 47 | maxFileSize?: number, 48 | maxFiles?: number, 49 | rotateDuration?: number, 50 | maxDays?: number, // for clean_log 51 | }, 52 | logger?: RegaxLoggerManagerOpts & { 53 | disableConsoleAfterReady?: boolean 54 | allowDebugAtProd?: boolean 55 | coreLogger?: RegaxLoggerOpts // custom config of coreLogger 56 | } 57 | customLogger?: { 58 | [loggerName: string]: RegaxLoggerOpts, 59 | } 60 | component?: ComponentOpts 61 | [componentName: string]: any 62 | } 63 | 64 | export interface ApplicationServerInfo extends ServerInfo { 65 | clientPort?: number, 66 | clientType?: string, // udp or ws or other customs, default ws 67 | sticky?: boolean, 68 | } 69 | 70 | export enum ApplicationEvent { 71 | BIND_SESSION = 'bind_session', 72 | UNBIND_SESSION = 'unbind_session', 73 | CLOSE_SESSION = 'close_session', 74 | STARTED = 'started', 75 | STOPPED = 'stopped', 76 | READY = 'ready', // all servers started 77 | } 78 | -------------------------------------------------------------------------------- /packages/server/src/api/component.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | import { inject, injectable } from 'inversify' 4 | import { ObjectOf } from '@regax/common' 5 | export const Component = Symbol('Component') 6 | 7 | export { 8 | injectable, 9 | inject, 10 | } 11 | 12 | export interface Component { 13 | onLoad?: (componentConfig: ObjectOf) => void // before app start 14 | onStart?: (componentConfig: ObjectOf) => void | Promise // app start 15 | onStop?: (componentConfig: ObjectOf) => void | Promise // app stop 16 | onServiceRegistry?: (componentConfig: ObjectOf) => any // register the service 17 | } 18 | 19 | type CompConfig = { 20 | name: string, 21 | path: string, 22 | } | string 23 | 24 | export interface ComponentOpts { 25 | master?: T[], 26 | agent?: T[], 27 | frontend?: T[], 28 | backend?: T[], 29 | all?: T[], 30 | [specifiedServerType: string]: T[] | undefined, 31 | } 32 | -------------------------------------------------------------------------------- /packages/server/src/api/connector.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { EventEmitter } from '@regax/common' 3 | 4 | export enum ConnectorEvent { 5 | DISCONNECT = 'disconnect', 6 | CONNECTION = 'connection', 7 | } 8 | 9 | export const CONNECTOR_DEFAULT_CLIENT_TYPE = 'ws' 10 | 11 | export interface Connector extends EventEmitter { 12 | start(): Promise 13 | stop(): Promise 14 | } 15 | 16 | export interface StickyServer { 17 | start(): Promise 18 | stop(): Promise 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/src/api/controller.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Application } from '../application' 3 | import { Service } from '../service' 4 | import { createRPCProxy, RPCProxy } from './rpc' 5 | import { createProxy } from '../util/proxy' 6 | import { PromiseDelegate, RegaxError, ErrorCode } from '@regax/common' 7 | import { Session } from './session' 8 | const util = require('util') 9 | 10 | export interface Context { 11 | session: Session, 12 | traceId: string, 13 | [key: string]: any 14 | } 15 | 16 | export function getAppService(service: Service, isFrontend: boolean): Service { 17 | service = { ...service } 18 | if (isFrontend) { 19 | service.backendSession = createProxy(() => { 20 | throw new Error('Frontend server cannot use backendSession service.') 21 | }) 22 | } else { 23 | service.session = createProxy(() => { 24 | throw new Error('Backend server cannot use frontendSession service.') 25 | }) 26 | } 27 | return service 28 | } 29 | 30 | export class Controller { 31 | protected readonly ctx: Context 32 | protected readonly app: Application 33 | protected readonly service: Service 34 | protected readonly rpc: RPCProxy 35 | private promiseDelegate?: PromiseDelegate 36 | constructor(app: Application, session: Session, promiseDelegate: PromiseDelegate, isFrontend: boolean, traceId: string) { 37 | this.ctx = Object.assign({}, app.appContext, { session, traceId }) 38 | this.app = app 39 | this.service = getAppService(app.service, isFrontend) 40 | this.rpc = createRPCProxy(this.app, session, traceId) 41 | this.promiseDelegate = promiseDelegate 42 | } 43 | fail(error: string | Error, ...args: any): void { 44 | if (typeof error === 'string' && args.length) { 45 | error = util.format(error, ...args) 46 | } 47 | const promiseDelegate = this.promiseDelegate 48 | if (promiseDelegate) { 49 | this.promiseDelegate = undefined 50 | promiseDelegate.reject(RegaxError.create(error, ErrorCode.CONTROLLER_FAIL)) 51 | } 52 | } 53 | success(data: any): void { 54 | const promiseDelegate = this.promiseDelegate 55 | if (promiseDelegate) { 56 | this.promiseDelegate = undefined 57 | promiseDelegate.resolve(data) 58 | } 59 | } 60 | isFinish(): boolean { 61 | return !this.promiseDelegate 62 | } 63 | } 64 | 65 | // Cannot be called by client 66 | export const ControllerBuiltInKey = Object.getOwnPropertyNames(Controller.prototype) 67 | -------------------------------------------------------------------------------- /packages/server/src/api/filter.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { RouteData } from './router' 3 | import { Application } from '../application' 4 | import { Session } from './session' 5 | 6 | export interface FilterContext extends RouteData { 7 | args: any[], 8 | res?: any, // router response 9 | error?: Error, 10 | traceId?: string, 11 | session: Session, 12 | } 13 | 14 | export interface Filter { 15 | (ctx: FilterContext, next: (error?: string | Error) => Promise): Promise 16 | } 17 | 18 | export interface FilterConfig { 19 | [key: string]: any 20 | } 21 | export interface FilterCreator { 22 | (app: Application, filterConfig: FilterConfig): Filter, 23 | } 24 | -------------------------------------------------------------------------------- /packages/server/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin' 2 | export * from './component' 3 | export * from './connector' 4 | export * from './socket' 5 | export * from './controller' 6 | export * from './router' 7 | export * from './rpc' 8 | export * from './session' 9 | export * from './filter' 10 | export * from './application' 11 | export * from '../application' 12 | -------------------------------------------------------------------------------- /packages/server/src/api/plugin.ts: -------------------------------------------------------------------------------- 1 | export interface PluginOpts { 2 | name?: string, // the plugin name, it can be used in `dep` 3 | enable: boolean // whether enabled 4 | package?: string, // the package name of plugin 5 | path?: string // the directory of the plugin package 6 | dependencies?: string[], // the dependent plugins, you can use the plugin name 7 | env?: string[] // specify the serverEnv that only enable the plugin in it, like ['local', 'unittest' ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/src/api/router.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Application } from '../application' 3 | import { Routes } from '@regax/rpc' 4 | import { Session } from './session' 5 | 6 | export interface RouteData { 7 | route: string, 8 | routeType: RouteType, 9 | serverType: string, 10 | name: string, 11 | method: string, 12 | isFrontend: boolean, 13 | isBackend: boolean, 14 | } 15 | 16 | export enum RouteType { 17 | CONTROLLER = 'controller', // client request 18 | RPC = 'rpc', // rpc invoke 19 | } 20 | 21 | export interface Router { 22 | controller(route: RouteData, session: Session, args: any[], traceId: string): Promise 23 | rpc(route: RouteData, session: Session, args: any[], traceId: string): Promise 24 | } 25 | 26 | export interface ServerRoute { 27 | (servers: string[], session: Session, msg: any, routeData: RouteData, app: Application): string 28 | } 29 | 30 | export { 31 | Routes 32 | } 33 | -------------------------------------------------------------------------------- /packages/server/src/api/rpc.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Application } from '../application' 3 | import { Service } from '../service' 4 | import { Session } from './session' 5 | import { createProxy } from '../util/proxy' 6 | import { Context, getAppService } from './controller' 7 | 8 | export function createRPCProxy(app: Application, session: Session, traceId: string): RPCProxy { 9 | return createProxy((serverType: string, rpcName: string, method: string) => { 10 | return (...args: any[]) => { 11 | return app.service.rpc.invoke(`${serverType}.${rpcName}.${method}`, session, args, traceId) 12 | } 13 | }, 3) 14 | } 15 | 16 | export interface RPCProxy { 17 | [serverType: string]: { 18 | [rpcName: string]: { 19 | [method: string]: (...args: any[]) => Promise 20 | } 21 | } 22 | } 23 | 24 | export class RPC { 25 | protected readonly ctx: Context 26 | protected readonly service: Service 27 | protected readonly rpc: RPCProxy 28 | constructor( 29 | protected readonly app: Application, 30 | session: Session, 31 | isFrontend: boolean, 32 | traceId: string, 33 | ) { 34 | this.service = getAppService(app.service, isFrontend) 35 | this.rpc = createRPCProxy(this.app, session, traceId) 36 | this.ctx = Object.assign({}, app.appContext, { session, traceId }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/server/src/api/session.ts: -------------------------------------------------------------------------------- 1 | import { ObjectOf, PlainData, EventListener } from '@regax/common' 2 | 3 | export enum SessionEvent { 4 | BIND = 'bind', 5 | UNBIND = 'unbind', 6 | CLOSED = 'closed' 7 | } 8 | 9 | export interface SessionFields { 10 | id: number | string, // session id 11 | frontendId: string, 12 | uid?: number | string 13 | values: ObjectOf, 14 | } 15 | 16 | export const EXPORTED_SESSION_FIELDS = ['id', 'frontendId', 'uid', 'values'] 17 | 18 | export interface Session { 19 | readonly event: typeof SessionEvent 20 | readonly frontendId: string 21 | readonly id: string | number 22 | uid?: number | string 23 | bind(uid: number | string): Promise 24 | unbind(uid: number | string): Promise 25 | push(...keys: string[]): Promise 26 | pushAll(...keys: string[]): Promise 27 | toJSON(): SessionFields 28 | get(key: string): PlainData 29 | remove(key: string): void 30 | set(key: string | object, value?: PlainData): void 31 | on(event: SessionEvent, fn: EventListener): () => void 32 | once(event: SessionEvent, fn: EventListener): () => void 33 | off(event: SessionEvent, fn: EventListener): void 34 | send(route: string, msg: PlainData): Promise 35 | close(reason?: string): Promise 36 | } 37 | -------------------------------------------------------------------------------- /packages/server/src/api/socket.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, PlainData } from '@regax/common' 2 | import { PackageType, Package, strdecode, PackageDataType, ByteArray } from '@regax/protocol' 3 | 4 | export enum SocketState { 5 | INITED, 6 | WAIT_ACK, 7 | WORKING, 8 | CLOSED, 9 | } 10 | 11 | export enum SocketEvent { 12 | DISCONNECT = 'disconnect', 13 | ERROR = 'error', 14 | HANDSHAKE = 'handshake', 15 | HEARTBEAT = 'heartbeat', 16 | DATA = 'data', 17 | KICK = 'kick' // kick user 18 | } 19 | 20 | export abstract class Socket extends EventEmitter { 21 | readonly event = SocketEvent 22 | state: SocketState 23 | protected constructor() { 24 | super() 25 | this.state = SocketState.INITED 26 | } 27 | abstract readonly remoteAddress: { 28 | host: string, 29 | port: number, 30 | } 31 | abstract readonly id: number 32 | /** 33 | * Send raw byte data. 34 | */ 35 | abstract sendRaw(msg: ByteArray): void 36 | close(): void { 37 | this.state = SocketState.CLOSED 38 | } 39 | get closed(): boolean { 40 | return this.state === SocketState.CLOSED 41 | } 42 | /** 43 | * Send message to client no matter whether handshake. 44 | */ 45 | sendForce(msg: ByteArray): void { 46 | if (this.state === SocketState.CLOSED) { 47 | return 48 | } 49 | this.sendRaw(msg) 50 | } 51 | sendBatch(msgs: Buffer[]): void { 52 | if (this.state !== SocketState.WORKING) { 53 | return 54 | } 55 | const rs = [] 56 | for (let i = 0; i < msgs.length; i++) { 57 | const src = Package.encode(PackageType.DATA, msgs[i]) 58 | rs.push(src) 59 | } 60 | this.sendRaw(Buffer.concat(rs)) 61 | } 62 | /** 63 | * Send byte data to client 64 | * @param msg 65 | */ 66 | send(msg: PlainData | Buffer): void { 67 | if (this.state !== SocketState.WORKING) { 68 | return 69 | } 70 | if (typeof msg === 'string') { 71 | msg = Buffer.from(msg) 72 | } else if (!(msg instanceof Buffer)) { 73 | msg = Buffer.from(JSON.stringify(msg)) 74 | } 75 | this.sendRaw(Package.encode(PackageType.DATA, msg as Buffer)) 76 | } 77 | 78 | handshakeResponse(msg: ByteArray): void { 79 | if (this.state !== SocketState.INITED) { 80 | return 81 | } 82 | this.sendRaw(msg) 83 | this.state = SocketState.WAIT_ACK 84 | } 85 | onMessage(msg: ByteArray): void { 86 | if (msg) { 87 | const msgs = Package.decode(msg) 88 | if (Array.isArray(msgs)) { 89 | for (let i = 0; i < msgs.length; i++) { 90 | this.onPackageMessage(msgs[i]) 91 | } 92 | } else { 93 | this.onPackageMessage(msgs) 94 | } 95 | } 96 | } 97 | onPackageMessage(msg: PackageDataType): void { 98 | const { type, body } = msg 99 | switch (type) { 100 | case PackageType.HANDSHAKE: 101 | if (this.state !== SocketState.INITED) return 102 | try { 103 | this.emit(SocketEvent.HANDSHAKE, JSON.parse(strdecode(body!))) 104 | } catch (ex) { 105 | this.emit(SocketEvent.HANDSHAKE, {}) 106 | } 107 | break 108 | case PackageType.HANDSHAKE_ACK: 109 | if (this.state !== SocketState.WAIT_ACK) return 110 | this.state = SocketState.WORKING 111 | this.emit(SocketEvent.HEARTBEAT) 112 | break 113 | case PackageType.HEARTBEAT: 114 | if (this.state !== SocketState.WORKING) return 115 | this.emit(SocketEvent.HEARTBEAT) 116 | break 117 | case PackageType.DATA: 118 | if (this.state !== SocketState.WORKING) return 119 | this.emit(SocketEvent.DATA, msg) 120 | break 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/server/src/component/agent.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../application' 2 | import { Component, inject, injectable } from '../api' 3 | 4 | export interface AgentOpts { 5 | } 6 | 7 | @injectable() 8 | export default class AgentComponent implements Component { 9 | protected opts: AgentOpts 10 | constructor( 11 | @inject(Application) protected readonly app: Application, 12 | ) { 13 | this.opts = this.app.getConfig('agent') || {} 14 | } 15 | onStart(): void { 16 | // TODO 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/server/src/component/backendSession.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify' 2 | import { Component, Application } from '../api' 3 | import { BackendSessionService } from '../service/backendSessionService' 4 | 5 | export * from '../service/backendSessionService' 6 | 7 | @injectable() 8 | export default class BackendSessionComponent implements Component { 9 | constructor( 10 | @inject(Application) protected app: Application 11 | ) { 12 | } 13 | onServiceRegistry(): BackendSessionService { 14 | return new BackendSessionService(this.app) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/server/src/component/channel.ts: -------------------------------------------------------------------------------- 1 | import { Application, inject, Component, injectable } from '../api' 2 | import { ChannelService } from '../service/channelService' 3 | 4 | export * from '../service/channelService' 5 | 6 | @injectable() 7 | export default class ChannelComponent implements Component { 8 | constructor( 9 | @inject(Application) protected readonly app: Application, 10 | ) { 11 | } 12 | onServiceRegistry(): ChannelService { 13 | return new ChannelService(this.app) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/server/src/component/connection.ts: -------------------------------------------------------------------------------- 1 | import { Application, inject, Component, injectable } from '../api' 2 | import { ConnectionService } from '../service/connectionService' 3 | 4 | export * from '../service/connectionService' 5 | /** 6 | * connection statistics service 7 | * record connection, login count and list 8 | */ 9 | @injectable() 10 | export default class ConnectionComponent implements Component { 11 | constructor( 12 | @inject(Application) protected readonly app: Application, 13 | ) { 14 | } 15 | onServiceRegistry(): ConnectionService { 16 | return new ConnectionService(this.app) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/server/src/component/globalChannel.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Application } from '../application' 3 | import { inject, Component, injectable } from '../api' 4 | import { GlobalChannelService } from '../service/globalChannelService' 5 | import { RemoteCache } from '@regax/rpc' 6 | import RPC from './rpc' 7 | 8 | export interface GlobalChannelOpts { 9 | enableReadyCheck?: boolean, 10 | channelPrefix?: string, 11 | createRemoteCache?: (opts: GlobalChannelOpts) => RemoteCache, 12 | RemoteCache: any, 13 | } 14 | 15 | @injectable() 16 | export default class GlobalChannelComponent implements Component { 17 | protected remoteCache?: any 18 | constructor( 19 | @inject(Application) protected readonly app: Application, 20 | @inject(RPC) protected readonly rpc: RPC, 21 | ) { 22 | } 23 | onStart(config: GlobalChannelOpts): void { 24 | if (config.createRemoteCache) { 25 | this.remoteCache = config.createRemoteCache(config) 26 | } 27 | } 28 | onServiceRegistry(config: GlobalChannelOpts): GlobalChannelService | void { 29 | if (this.remoteCache) { 30 | return new GlobalChannelService(this.app, this.rpc, this.remoteCache, config.channelPrefix) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/src/component/messenger.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Application, inject, Component, injectable } from '../api' 3 | import { EventEmitter, Fn } from '@regax/common' 4 | import { MessengerService } from '../service/messengerService' 5 | 6 | @injectable() 7 | export default class MessengerComponent implements Component { 8 | protected emitter = new EventEmitter() 9 | constructor( 10 | @inject(Application) protected readonly app: Application, 11 | ) { 12 | process.on('message', (msg: any, data: any) => { 13 | if (typeof msg === 'string') { 14 | this.emitter.emit(msg, data) 15 | } else { 16 | this.emitter.emit(msg.type, msg.data) 17 | } 18 | }) 19 | } 20 | onServiceRegistry(): MessengerService { 21 | const self = this 22 | return { 23 | onMessage(type: string, fn: Fn): void { 24 | self.emitter.on(type, fn) 25 | }, 26 | send(type: string, data: any): void { 27 | const connected = process.connected && process.send 28 | if (connected) { 29 | process.send!({ type, data }) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/server/src/component/pushScheduler.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { inject, Component, injectable, Application } from '../api' 3 | import { Tick } from '@regax/common' 4 | import { InternalSession } from '../service/sessionService' 5 | 6 | const DEFAULT_FLUSH_INTERVAL = 20 7 | 8 | @injectable() 9 | export default class PushScheduler implements Component { 10 | protected tick: Tick = new Tick(DEFAULT_FLUSH_INTERVAL) 11 | protected queueMap: Map = new Map() 12 | constructor( 13 | @inject(Application) protected readonly app: Application, 14 | ) { 15 | } 16 | onStart(): void { 17 | } 18 | onStop(): void { 19 | this.tick.stop() 20 | } 21 | schedule(sessions: InternalSession[], msg: Buffer): void { 22 | sessions.forEach(session => { 23 | if (session.closed) return 24 | if (!this.queueMap.has(session)) { 25 | this.queueMap.set(session, []) 26 | // will registry once 27 | session.once(session.event.CLOSED, () => { 28 | this.queueMap.delete(session) 29 | }) 30 | } 31 | this.queueMap.get(session)!.push(msg) 32 | }) 33 | // TODO pushScheduler is closed 34 | // if (!this.tick.isRunning()) { 35 | this.flush() 36 | // } 37 | // this.tick.next(() => this.flush()) 38 | } 39 | flush(): void { 40 | this.queueMap.forEach((buffers, session) => { 41 | if (buffers.length === 0) return 42 | if (session.closed) this.queueMap.delete(session) 43 | session.sendBatch(buffers) 44 | buffers.length = 0 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/src/component/session.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify' 2 | import { Application, Component } from '../api' 3 | import { SessionService } from '../service/sessionService' 4 | 5 | export * from '../service/sessionService' 6 | 7 | export interface SessionOpts { 8 | singleSession?: boolean 9 | } 10 | 11 | @injectable() 12 | export default class SessionComponent implements Component { 13 | constructor( 14 | @inject(Application) protected app: Application 15 | ) { 16 | } 17 | onServiceRegistry(): SessionService { 18 | return new SessionService(this.app, this.app.getConfig('session') || {}) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/src/component/taskManager.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify' 2 | import { SeqQueue } from '../util/queue' 3 | import { Component, Application } from '../api' 4 | import { Fn } from '@regax/common' 5 | 6 | @injectable() 7 | export default class TaskManager implements Component { 8 | static timeout = 3000 9 | protected queues: Map = new Map() 10 | constructor( 11 | @inject(Application) protected app: Application 12 | ) { 13 | } 14 | onServiceRegistry(): TaskManager { 15 | return this 16 | } 17 | /** 18 | * Add tasks into task group. Create the task group if it dose not exist. 19 | * 20 | * @param key task key 21 | * @param fn task callback 22 | * @param onTimeout task timeout callback 23 | * @param timeout timeout for task 24 | */ 25 | addTask(key: string | number, fn: Fn, onTimeout?: Fn, timeout?: number): boolean { 26 | let queue = this.queues.get(key) 27 | if (!queue) { 28 | queue = new SeqQueue(TaskManager.timeout) 29 | this.queues.set(key, queue) 30 | } 31 | 32 | return queue.push(fn, onTimeout, timeout) 33 | } 34 | /** 35 | * Destroy task group 36 | * 37 | * @param {String} key task key 38 | * @param {Boolean} force whether close task group directly 39 | */ 40 | closeQueue(key: string | number, force: boolean): void { 41 | if (!this.queues.has(key)) { 42 | // ignore illeagle key 43 | return 44 | } 45 | this.queues.get(key)!.close(force) 46 | this.queues.delete(key) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/server/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { Application, ApplicationEnv, ApplicationOpts } from '../api' 2 | import { WSConnector } from '../connector/wsConnector' 3 | import { UDPConnector } from '../connector/udpConnector' 4 | import { TCPStickyServer } from '../connector/tcpStickyServer' 5 | import { UDPStickyServer } from '../connector/udpStickyServer' 6 | 7 | export default function configDefault(app: Application): ApplicationOpts { 8 | return { 9 | component: { 10 | master: [ 'master', 'rpc'], 11 | agent: [ 'agent'], 12 | frontend: [ 'session', 'backendSession', 'channel', 'rpc', 'router', 'connector', 'connection', 'taskManager', 'pushScheduler'], 13 | backend: [ 'backendSession', 'channel', 'rpc', 'router', 'connection'], 14 | all: ['messenger'], 15 | }, 16 | connectorRegistries: { 17 | ws: WSConnector, 18 | udp: UDPConnector, 19 | }, 20 | stickyServerRegistries: { 21 | ws: TCPStickyServer, 22 | tcp: TCPStickyServer, 23 | udp: UDPStickyServer, 24 | }, 25 | /** 26 | * logger options 27 | * @property {String} dir - directory of log files 28 | * @property {String} encoding - log file encoding, defaults to utf8 29 | * @property {String} level - default log level, could be: DEBUG, INFO, WARN, ERROR or NONE, defaults to INFO in production 30 | * @property {String} consoleLevel - log level of stdout, defaults to INFO in local serverEnv, defaults to WARN in unittest, defaults to NONE elsewise 31 | * @property {Boolean} disableConsoleAfterReady - disable logger console after app ready. defaults to `false` on local and unittest env, others is `true`. 32 | * @property {Boolean} outputJSON - log as JSON or not, defaults to false 33 | * @property {Boolean} buffer - if enabled, flush logs to disk at a certain frequency to improve performance, defaults to true 34 | * @property {String} errorLogName - file name of errorLogger 35 | * @property {String} coreLogName - file name of coreLogger 36 | * @property {String} agentLogName - file name of agent worker log 37 | * @property {Object} coreLogger - custom config of coreLogger 38 | */ 39 | logger: { 40 | type: app.serverType, 41 | env: app.env, 42 | dir: app.getFilePath('logs'), 43 | encoding: 'utf8', 44 | level: 'INFO', 45 | consoleLevel: 'INFO', 46 | disableConsoleAfterReady: app.env !== ApplicationEnv.local && app.env !== ApplicationEnv.unittest, 47 | outputJSON: false, 48 | buffer: true, 49 | appLogName: 'regax-app.log', 50 | agentLogName: 'regax-agent.log', 51 | coreLogName: 'regax-core.log', 52 | errorLogName: 'common-error.log', 53 | coreLogger: {}, 54 | allowDebugAtProd: false, 55 | }, 56 | /** 57 | * logrotator options 58 | * @property {Array} filesRotateByHour - list of files that will be rotated by hour 59 | * @property {Array} filesRotateBySize - list of files that will be rotated by size 60 | * @property {Number} maxFileSize - Max file size to judge if any file need rotate 61 | * @property {Number} maxFiles - pieces rotate by size 62 | * @property {Number} maxDays - keep max days log files, default is `31`. Set `0` to keep all logs. 63 | * @property {Number} rotateDuration - time interval to judge if any file need rotate 64 | * @property {Number} maxDays - keep max days log files 65 | */ 66 | logrotator: { 67 | // for rotate_by_hour 68 | filesRotateByHour: undefined, 69 | hourDelimiter: '-', 70 | // for rotate_by_size 71 | filesRotateBySize: undefined, 72 | maxFileSize: 50 * 1024 * 1024, 73 | maxFiles: 10, 74 | rotateDuration: 60000, 75 | // for clean_log 76 | maxDays: 31, 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/server/src/config/config.local.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationOpts } from '../api' 2 | 3 | export default function configLocal(): ApplicationOpts { 4 | return { 5 | logger: { 6 | coreLogger: { 7 | consoleLevel: 'INFO', 8 | }, 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/server/src/config/config.prod.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationOpts } from '../api' 2 | 3 | export default function configProd(): ApplicationOpts { 4 | return {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/server/src/config/config.unittest.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationOpts } from '../api' 2 | 3 | export default function config(): ApplicationOpts { 4 | return { 5 | logger: { 6 | consoleLevel: 'WARN', 7 | buffer: false, 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/server/src/config/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Regax builtin plugins 3 | */ 4 | export default { 5 | scheduler: { 6 | enable: true, 7 | package: '@regax/scheduler' 8 | }, 9 | logrotator: { 10 | enable: true, 11 | package: '@regax/logrotator' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/src/connector/commands/handshake.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Command } from './' 3 | import { Package, PackageType } from '@regax/protocol' 4 | import { PlainData } from '@regax/common' 5 | import { ApplicationOpts, SocketEvent, Socket } from '../../api' 6 | 7 | export type HandshakeBuffer = { 8 | sys: { 9 | type: string, 10 | version: string, 11 | rsa?: { 12 | rsa_n?: string | number, 13 | rsa_e?: string | number, 14 | }, 15 | heartbeat?: number 16 | }, 17 | user: PlainData, 18 | } & PlainData 19 | 20 | enum HandshakeCode { 21 | OK = 200, 22 | USER_ERROR = 500, 23 | OLD_CLIENT = 501 24 | } 25 | 26 | export class HandshakeCommand implements Command { 27 | constructor( 28 | protected readonly opts: ApplicationOpts['connector'] & {} 29 | ) { } 30 | get event(): SocketEvent { 31 | return SocketEvent.HANDSHAKE 32 | } 33 | async handle(socket: Socket, msg: HandshakeBuffer): Promise { 34 | let user 35 | if (!msg.sys) { 36 | processError(socket, HandshakeCode.USER_ERROR) 37 | return 38 | } 39 | if (typeof this.opts.checkClient === 'function') { 40 | if (!this.opts.checkClient(msg.sys.type, msg.sys.version)) { 41 | processError(socket, HandshakeCode.OLD_CLIENT) 42 | return 43 | } 44 | } 45 | if (typeof this.opts.userHandshake === 'function') { 46 | try { 47 | user = await this.opts.userHandshake(msg.user) 48 | } catch (e) { 49 | processError(socket, HandshakeCode.USER_ERROR, e.message) 50 | return 51 | } 52 | } 53 | const sys = { 54 | heartbeat: this.opts.heartbeatInterval ? this.opts.heartbeatInterval : undefined, 55 | } 56 | // TODO crypto and protobuf 57 | response(socket, sys, user) 58 | } 59 | } 60 | 61 | function response(socket: Socket, sys: PlainData, user?: PlainData): void { 62 | const res: PlainData = { 63 | code: HandshakeCode.OK, 64 | sys, 65 | } 66 | if (user) { 67 | res.user = user 68 | } 69 | socket.handshakeResponse(Package.encode(PackageType.HANDSHAKE, Buffer.from(JSON.stringify(res)))) 70 | } 71 | 72 | function processError(socket: Socket, code: number, error?: string): void { 73 | const res = { 74 | code: code, 75 | message: error, 76 | } 77 | socket.sendForce(Package.encode(PackageType.HANDSHAKE, Buffer.from(JSON.stringify(res)))) 78 | process.nextTick(() => { 79 | socket.close() 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /packages/server/src/connector/commands/heartbeat.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './' 2 | import { ApplicationOpts, SocketEvent, Socket } from '../../api' 3 | import { Tick } from '@regax/common' 4 | import { Package, PackageType } from '@regax/protocol' 5 | 6 | export class HeartbeatCommand implements Command { 7 | protected clients: { [key: string]: Tick } = {} 8 | protected heartbeatInterval?: number 9 | protected heartbeatTimeout?: number 10 | protected disconnectOnTimeout?: boolean = true 11 | constructor( 12 | protected readonly opts: ApplicationOpts['connector'] & {} 13 | ) { 14 | if (opts.heartbeatInterval) { 15 | this.heartbeatInterval = opts.heartbeatInterval 16 | this.heartbeatTimeout = opts.heartbeatTimeout || this.heartbeatInterval * 2 17 | this.disconnectOnTimeout = true 18 | } 19 | } 20 | get event(): SocketEvent { 21 | return SocketEvent.HEARTBEAT 22 | } 23 | handle(socket: Socket): void { 24 | if (!this.heartbeatInterval) return 25 | if (!this.clients[socket.id]) { 26 | this.clients[socket.id] = new Tick(this.heartbeatInterval, this.heartbeatTimeout) 27 | const onClear = () => this.clearClientTick(socket.id) 28 | socket.once(SocketEvent.DISCONNECT, onClear) 29 | socket.once(SocketEvent.ERROR, onClear) 30 | } 31 | socket.sendRaw(Package.encode(PackageType.HEARTBEAT)) 32 | if (this.disconnectOnTimeout) { 33 | const tick = this.clients[socket.id] 34 | tick.stop() 35 | tick.next(() => {}, () => { 36 | console.log('client %j heartbeat timeout.', socket.id) 37 | socket.close() 38 | }) 39 | } 40 | } 41 | clearClientTick(socketId: number): void { 42 | if (this.clients[socketId]) { 43 | this.clients[socketId].stop() 44 | delete this.clients[socketId] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/src/connector/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { PlainData } from '@regax/common' 2 | import { Socket, SocketEvent } from '../../api' 3 | 4 | import { HandshakeCommand } from './handshake' 5 | import { HeartbeatCommand } from './heartbeat' 6 | import { KickCommand } from './kick' 7 | 8 | export interface Command { 9 | event: SocketEvent, 10 | start?: (socket: Socket) => void 11 | handle?: (socket: Socket, msg: PlainData) => void 12 | } 13 | 14 | export const commands = [ 15 | HandshakeCommand, 16 | HeartbeatCommand, 17 | KickCommand, 18 | ] 19 | -------------------------------------------------------------------------------- /packages/server/src/connector/commands/kick.ts: -------------------------------------------------------------------------------- 1 | import { Package, PackageType } from '@regax/protocol' 2 | import { Command } from './' 3 | import { SocketEvent, Socket } from '../../api' 4 | 5 | export class KickCommand implements Command { 6 | get event(): SocketEvent { 7 | return SocketEvent.KICK 8 | } 9 | handle(socket: Socket, msg: string | number): void { 10 | // websocket close code 1000 would emit when client close the connection 11 | if (typeof msg === 'string') { 12 | const res = { 13 | reason: msg 14 | } 15 | socket.sendRaw(Package.encode(PackageType.KICK, Buffer.from(JSON.stringify(res)))) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/server/src/connector/message.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageType, MessageDataType } from '@regax/protocol' 2 | import { PlainData } from '@regax/common' 3 | import {ByteArray} from '@regax/protocol' 4 | 5 | export interface MessageDataDecodeType { 6 | id?: number, 7 | route: string, 8 | body: PlainData 9 | } 10 | 11 | export { 12 | MessageDataType, 13 | } 14 | 15 | export function defaultMessageEncode(reqId: number, route: number | string, data?: PlainData): ByteArray | undefined { 16 | if (reqId) { 17 | // response 18 | if (!reqId || !route || !data) return 19 | return Message.encode(reqId, MessageType.RESPONSE, 0, undefined, encodeBody(data)) 20 | } else { 21 | // push 22 | if (!route || !data) return 23 | return Message.encode(0, MessageType.PUSH, 0, route, encodeBody(data)) 24 | } 25 | } 26 | 27 | export function defaultMessageDecode(data: MessageDataType): MessageDataDecodeType { 28 | const msg = Message.decode(data.body) 29 | let body = {} 30 | try { 31 | body = JSON.parse(msg.body.toString('utf8')) 32 | } catch (e) { 33 | body = {} 34 | } 35 | return { id: msg.id, route: msg.route as string, body } 36 | } 37 | 38 | function encodeBody(msgBody: PlainData): Buffer { 39 | // TODO encode use protobuf 40 | return Buffer.from(JSON.stringify(msgBody), 'utf8') 41 | } 42 | -------------------------------------------------------------------------------- /packages/server/src/connector/tcpStickyServer.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as net from 'net' 3 | import { PromiseDelegate } from '@regax/common' 4 | import Master from '../component/master' 5 | import { StickyServer, Application } from '../api' 6 | 7 | export const STICKY_CONNECTION = 'regax:sticky-connection' 8 | 9 | export class TCPStickyServer implements StickyServer { 10 | protected server: net.Server 11 | protected master: Master 12 | constructor(protected port: number, app: Application) { 13 | this.master = app.get('master') 14 | } 15 | protected onConnection(connection: any): void { 16 | // default use round-robin to select worker 17 | const worker = this.master.roundRobinWorker('ws') 18 | worker.send(STICKY_CONNECTION, connection) 19 | } 20 | start(): Promise { 21 | const p = new PromiseDelegate() 22 | this.server = net.createServer({ 23 | pauseOnConnect: true, 24 | }, (connection: any) => { 25 | // We received a connection and need to pass it to the appropriate 26 | // worker. Get the worker for this connection's source IP and pass 27 | // it the connection. 28 | 29 | /* istanbul ignore next */ 30 | if (!connection.remoteAddress) { 31 | // This will happen when a client sends an RST(which is set to 1) right 32 | // after the three-way handshake to the server. 33 | // Read https://en.wikipedia.org/wiki/TCP_reset_attack for more details. 34 | connection.destroy() 35 | } else { 36 | this.onConnection(connection) 37 | } 38 | }) 39 | this.server.listen(this.port, () => { 40 | p.resolve() 41 | }) 42 | return p.promise 43 | } 44 | async stop(): Promise { 45 | if (this.server) this.server.close() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/src/connector/udpSocket.ts: -------------------------------------------------------------------------------- 1 | import { ByteArray } from '@regax/protocol' 2 | import { Socket as DgramSocket } from 'dgram' 3 | import { SocketEvent, Socket, SocketState } from '../api' 4 | import { Logger } from '@regax/logger' 5 | 6 | export class UDPSocket extends Socket { 7 | constructor( 8 | readonly id: number, 9 | protected readonly socket: DgramSocket, 10 | public remoteAddress: { host: string, port: number }, 11 | protected readonly logger: Logger 12 | ) { 13 | super() 14 | this.state = SocketState.INITED 15 | } 16 | /** 17 | * Send raw byte data. 18 | */ 19 | sendRaw(msg: ByteArray): void { 20 | this.socket.send(msg, 0, msg.length, this.remoteAddress.port, this.remoteAddress.host, (err: Error) => { 21 | if (err) { 22 | this.logger.error('[regax-udpsocket] send msg to remote with err: %j', err.stack) 23 | } 24 | }) 25 | } 26 | close(): void { 27 | if (this.state === SocketState.CLOSED) { 28 | return 29 | } 30 | this.state = SocketState.CLOSED 31 | this.emit(SocketEvent.DISCONNECT) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/src/connector/udpStickyServer.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { PromiseDelegate } from '@regax/common' 3 | import { StickyServer, ApplicationOpts, Application } from '../api' 4 | import { DEFAULT_UDP_TYPE } from './udpConnector' 5 | import Master from '../component/master' 6 | import { Socket } from 'dgram' 7 | const dgram = require('dgram') 8 | 9 | export const STICKY_SERVER = 'regax:sticky-server' 10 | 11 | export class UDPStickyServer implements StickyServer { 12 | protected server: Socket 13 | constructor( 14 | protected port: number, 15 | protected app: Application, 16 | ) { 17 | } 18 | start(): Promise { 19 | const p = new PromiseDelegate() 20 | const opts = this.app.getConfig('udpConnector') || {} 21 | const server = this.server = dgram.createSocket(opts.udpType || DEFAULT_UDP_TYPE) 22 | server.on('listening', () => { 23 | this.app.get('master') 24 | .getStickyWorkers('udp') 25 | .forEach(worker => { 26 | worker.send(STICKY_SERVER, server) 27 | }) 28 | p.resolve() 29 | }) 30 | server.bind(this.port) 31 | return p.promise 32 | } 33 | async stop(): Promise { 34 | this.server.close() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/server/src/connector/wsSocket.ts: -------------------------------------------------------------------------------- 1 | import { ByteArray } from '@regax/protocol' 2 | import * as WebSocket from 'ws' 3 | import { SocketEvent, Socket, SocketState } from '../api' 4 | import { Logger } from '@regax/logger' 5 | 6 | export class WSSocket extends Socket { 7 | constructor( 8 | readonly id: number, 9 | protected readonly socket: WebSocket, 10 | readonly remoteAddress: { host: string, port: number }, 11 | protected readonly logger: Logger 12 | ) { 13 | super() 14 | socket.once('close', this.emit.bind(this, SocketEvent.DISCONNECT)) 15 | socket.on('error', this.emit.bind(this, SocketEvent.ERROR)) 16 | socket.on('message', this.onMessage.bind(this)) 17 | } 18 | /** 19 | * Send raw byte data. 20 | */ 21 | sendRaw(msg: ByteArray): void { 22 | this.socket.send(msg, {binary: true}, err => { 23 | if (err && !err.message.match('CLOSING') && !err.message.match('CLOSED')) { 24 | this.logger.error('[regax-websocket] websocket send binary data failed: %j', err.stack) 25 | return 26 | } 27 | }) 28 | } 29 | close(): void { 30 | if (this.state === SocketState.CLOSED) { 31 | return 32 | } 33 | this.state = SocketState.CLOSED 34 | this.socket.emit('close') 35 | this.socket.close() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/server/src/filter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './timeout' 2 | -------------------------------------------------------------------------------- /packages/server/src/filter/timeout.ts: -------------------------------------------------------------------------------- 1 | import { Application, Filter } from '../api' 2 | 3 | const DEFAULT_TIMEOUT = 3000 4 | const DEFAULT_SIZE = 500 5 | 6 | export interface TimeoutFilterOpts { 7 | timeout: number, 8 | maxSize: number 9 | } 10 | 11 | /** 12 | * Filter for timeout. 13 | * Print a warn information when request timeout. 14 | */ 15 | export function TimeoutFilter(app: Application, opts: TimeoutFilterOpts): Filter { 16 | const timeout: number = opts.timeout || DEFAULT_TIMEOUT 17 | const maxSize = opts.maxSize || DEFAULT_SIZE 18 | let timeoutSize = 0 19 | return async (ctx, next) => { 20 | if (timeoutSize > maxSize) { 21 | app.logger.warn('timeout filter is out of range, current size is %s, max size is %s', timeoutSize, maxSize) 22 | await next() 23 | return 24 | } 25 | timeoutSize++ 26 | const timeoutId = setTimeout(() => { 27 | app.logger.error('request %j timeout.', ctx.route) 28 | }, timeout) 29 | await next() 30 | clearTimeout(timeoutId) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './filter' 3 | export * from './service' 4 | 5 | export * from './monitor/monitor' 6 | -------------------------------------------------------------------------------- /packages/server/src/loader/index.ts: -------------------------------------------------------------------------------- 1 | export * from './loader' 2 | -------------------------------------------------------------------------------- /packages/server/src/loader/loader.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Timing } from './timing' 3 | import { Application, PluginOpts } from '../api' 4 | 5 | export interface Loader { 6 | preload?(): void 7 | load?(): void 8 | } 9 | 10 | export type LoadUnitType = 'plugin' | 'framework' | 'app' 11 | export interface LoaderUnit { 12 | path: string, 13 | type: LoadUnitType 14 | name: string, 15 | useAppDirMode: boolean, 16 | } 17 | 18 | const loaderClasses = [ 19 | require('./loaders/plugin').default, 20 | require('./loaders/config').default, 21 | require('./loaders/component').default, 22 | ] 23 | 24 | export interface LoaderOpts { 25 | plugins?: { 26 | [pluginName: string]: PluginOpts 27 | } 28 | [others: string]: any 29 | } 30 | 31 | export class LoaderManager { 32 | timing = new Timing() 33 | protected loaderUnits: LoaderUnit[] = [] 34 | protected loaderUnitsCache: LoaderUnit[] 35 | public pluginMap: Map = new Map() 36 | protected loaders: Loader[] = [] 37 | readonly opts: LoaderOpts 38 | constructor( 39 | protected app: Application, 40 | ) { 41 | loaderClasses.forEach(LoaderCls => this.loaders.push(new LoaderCls(this, app))) 42 | this.opts = this.app.getConfig('loader') || {} 43 | } 44 | preload(): void { 45 | this.loaders.forEach(loader => { 46 | if (loader.preload) loader.preload() 47 | }) 48 | } 49 | load(): void { 50 | this.loaders.forEach(loader => { 51 | try { 52 | if (loader.load) loader.load() 53 | } catch (e) { 54 | this.app.coreLogger.error('[regax-loader] exec loader %s with error: %s', loader.constructor.name, e.message) 55 | throw e 56 | } 57 | }) 58 | // this.app.coreLogger.info('[regax-loader] [%s] exec loader : %j', this.app.serverType, this.timing.toJSON()) 59 | } 60 | addLoaderUnit(path: string, type: LoadUnitType, useAppDirMode = true, name: string): void { 61 | this.loaderUnits.push({ path, type, useAppDirMode, name }) 62 | } 63 | getLoaderUnits(): LoaderUnit[] { 64 | if (this.loaderUnitsCache) return this.loaderUnitsCache 65 | this.loaderUnitsCache = this.loaderUnits.filter(unit => { 66 | if (unit.type === 'plugin' && this.pluginMap.get(unit.name) && !this.pluginMap.get(unit.name)!.enable) return false 67 | return true 68 | }) 69 | this.loaderUnits.length = 0 70 | return this.loaderUnitsCache 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/server/src/loader/loaders/component.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Loader, LoaderManager, LoaderUnit } from '../loader' 3 | import * as path from 'path' 4 | import { each } from '@regax/common' 5 | import { Application, ComponentOpts } from '../../api' 6 | import { tryToRequire } from '../../util/fs' 7 | const debug = require('debug')('regax-core:loader:component') 8 | 9 | export default class ComponentLoader implements Loader { 10 | protected componentConfigs: ComponentOpts<{ name: string, path: string }> = {} 11 | constructor( 12 | protected loader: LoaderManager, 13 | protected app: Application 14 | ) { 15 | } 16 | protected tryToLoadConfig(unit: LoaderUnit, configName: string): boolean { 17 | const configPath = path.join(unit.path, configName) 18 | let config = tryToRequire(configPath) 19 | if (typeof config === 'function') { 20 | config = config(this.app) 21 | } 22 | // normalize the component config 23 | if (config && config.component) { 24 | each(config.component, (compNames, serverType) => { 25 | const compConfigs = this.componentConfigs[serverType] || (this.componentConfigs[serverType] = []) 26 | compNames.forEach((name: string) => { 27 | compConfigs.push({ 28 | name, 29 | path: path.join(unit.path, unit.useAppDirMode ? 'app/component' : 'component', name), 30 | }) 31 | }) 32 | }) 33 | return true 34 | } 35 | return false 36 | } 37 | loadComponents(): void { 38 | const compConfig = this.componentConfigs 39 | let compKeys: { name: string, path: string}[] = compConfig.all || [] 40 | if (compConfig.frontend && this.app.isFrontendServer) { 41 | compKeys = compKeys.concat(compConfig.frontend) 42 | } 43 | if (compConfig.backend && this.app.isBackendServer) { 44 | compKeys = compKeys.concat(compConfig.backend) 45 | } 46 | if (compConfig[this.app.serverType]) { 47 | compKeys = compKeys.concat(compConfig[this.app.serverType]!) 48 | } 49 | const compsMap = compKeys.reduce((res: { [compName: string]: any }, comp: { name: string, path: string }) => { 50 | if (!res[comp.name]) { 51 | res[comp.name] = require(comp.path) 52 | } 53 | return res 54 | }, {}) 55 | this.app.addComponents(compsMap) 56 | debug('components(%s) %s loaded.', compKeys.length, compKeys.map(c => c.name)) 57 | } 58 | load(): void { 59 | this.loader.timing.start('Load component') 60 | this.loader.getLoaderUnits().forEach((unit: LoaderUnit) => { 61 | const loaded = this.tryToLoadConfig(unit, 'config/config.' + this.app.env) 62 | if (!loaded) this.tryToLoadConfig(unit, 'config/config.default') 63 | }) 64 | this.loadComponents() 65 | this.loader.timing.end('Load component') 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/server/src/loader/loaders/config.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as path from 'path' 3 | import { tryToRequire } from '../../util/fs' 4 | import { Application } from '../../api' 5 | import { Loader, LoaderUnit, LoaderManager } from '../loader' 6 | 7 | export default class ConfigLoader implements Loader { 8 | constructor( 9 | protected loader: LoaderManager, 10 | protected app: Application 11 | ) { 12 | } 13 | protected tryToLoadConfig(unit: LoaderUnit, configName: string): void { 14 | const configPath = path.join(unit.path, configName) 15 | let config = tryToRequire(configPath) 16 | if (typeof config === 'function') { 17 | config = config(this.app) 18 | } 19 | this.app.setConfig(config || {}) 20 | } 21 | preload(): void { 22 | this.loader.timing.start('Load config') 23 | this.loader.getLoaderUnits().forEach((unit: LoaderUnit) => { 24 | this.tryToLoadConfig(unit, 'config/config.default') 25 | }) 26 | this.loader.getLoaderUnits().forEach((unit: LoaderUnit) => { 27 | this.tryToLoadConfig(unit, 'config/config.' + this.app.env) 28 | }) 29 | this.loader.timing.end('Load config') 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/server/src/loader/loaders/context.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../../api' 2 | import * as path from 'path' 3 | import { tryToRequire } from '../../util/fs' 4 | import { Loader, LoaderUnit, LoaderManager } from '../loader' 5 | 6 | export default class ContextLoader implements Loader { 7 | constructor( 8 | protected loader: LoaderManager, 9 | protected app: Application 10 | ) { 11 | } 12 | preload(): void { 13 | this.loader.getLoaderUnits().forEach((unit: LoaderUnit) => { 14 | let ctx = tryToRequire(path.join(unit.path, unit.useAppDirMode ? 'app/extend/context' : 'extend/context')) 15 | if (typeof ctx === 'function') { 16 | ctx = ctx(this.app) 17 | } 18 | this.app.extendContext(ctx) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/src/loader/loaders/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import { each } from '@regax/common' 4 | import { Loader, LoaderManager } from '../loader' 5 | import { Application, PluginOpts } from '../../api' 6 | import { tryToRequire } from '../../util/fs' 7 | 8 | // TODO plugin recursive dependencies check 9 | export default class PluginLoader implements Loader { 10 | protected pluginList: PluginOpts[] = [] 11 | public pluginMap: Map 12 | constructor( 13 | protected loader: LoaderManager, 14 | protected app: Application 15 | ) { 16 | this.pluginMap = this.loader.pluginMap 17 | } 18 | preload(): void { 19 | this.loader.timing.start('plugin preload') 20 | this.readPluginConfig(this.app.frameworkPath) // 1. load framework plugins 21 | this.addPluginLoaderUnits() // 2. register the framework plugins to loader units 22 | this.loader.addLoaderUnit(this.app.frameworkPath, 'framework', false, 'framework') // 3. add the framework path to loader units 23 | each(this.loader.opts.plugins || {}, (opts, name) => this.addPlugin(name, opts)) // 4. load app plugins from opts 24 | this.readPluginConfig(this.app.baseDir) // 5. load app plugins from basedir 25 | this.addPluginLoaderUnits() // 6. register the app plugins to loader units 26 | this.loader.addLoaderUnit(this.app.baseDir, 'app', this.app.useAppDirMode, 'app') // 7. register the app path to loader units 27 | this.loader.timing.end('plugin preload') 28 | } 29 | protected addPluginLoaderUnits(): void { 30 | this.pluginList.forEach(p => { 31 | if (p.enable) { 32 | let useAppDirMode = false 33 | try { 34 | if (fs.statSync(path.join(p.path!, 'app')).isDirectory()) useAppDirMode = true 35 | } catch (e) { /* ignore */ } 36 | this.loader.addLoaderUnit(p.path!, 'plugin', useAppDirMode, p.name!) 37 | } 38 | }) 39 | this.pluginList.length = 0 40 | } 41 | protected addPlugin(name: string, pluginOpts: PluginOpts): void { 42 | const env = this.app.env 43 | let enable = pluginOpts.enable 44 | // plugin is disabled 45 | if (pluginOpts.env && enable && !pluginOpts.env.includes(env)) { 46 | enable = false 47 | } 48 | if (!enable && this.pluginMap.has(name)) { 49 | this.pluginMap.get(name)!.enable = false 50 | return 51 | } 52 | if (this.pluginMap.has(name)) { 53 | throw new Error(`Plugin "${name}" has added before: `) 54 | } 55 | if (!pluginOpts.package && !pluginOpts.path) { 56 | throw new Error(`Plugin "${name}" required "package" or "path" opts.`) 57 | } 58 | const pkgPath = require.resolve(path.join(pluginOpts.path || pluginOpts.package!, 'package.json')) 59 | // TODO check the dependencies 60 | // Read the package.json of plugin 61 | const pkg = require(pkgPath) 62 | const pluginPath = path.join(pkgPath, '../', pkg.regaxPlugin && pkg.regaxPlugin.dir ? pkg.regaxPlugin.dir : 'lib') 63 | this.pluginMap.set(name, { ...pluginOpts, name, path: pluginPath }) 64 | this.readPluginConfig(pluginPath) 65 | this.pluginList.push(this.pluginMap.get(name)!) 66 | } 67 | protected readPluginConfig(pluginDir: string): void { 68 | const config = tryToRequire(path.join(pluginDir, 'config/plugin')) 69 | if (config) { 70 | for (const name in config) { 71 | if (config.hasOwnProperty(name)) { 72 | const pluginOpts: PluginOpts = config[name] 73 | if (typeof pluginOpts === 'boolean') { 74 | if (this.pluginMap.has(name)) { 75 | this.pluginMap.get(name)!.enable = pluginOpts as boolean 76 | } 77 | } else { 78 | this.addPlugin(name, pluginOpts) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/server/src/loader/timing.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | interface TimingRecord { 4 | desc: string, 5 | start: number, 6 | end?: number, 7 | duration?: number, 8 | pid: number, 9 | index: number, 10 | } 11 | export class Timing { 12 | 13 | protected recordMap: Map = new Map 14 | protected recordList: TimingRecord[] = [] 15 | start(desc: string): void { 16 | if (!desc) return 17 | if (this.recordMap.has(desc)) this.end(desc) 18 | const start = Date.now() 19 | const record = { 20 | desc, 21 | start, 22 | end: undefined, 23 | duration: undefined, 24 | pid: process.pid, 25 | index: this.recordList.length, 26 | } 27 | this.recordMap.set(desc, record) 28 | this.recordList.push(record) 29 | } 30 | 31 | end(desc: string): void { 32 | if (!desc) return 33 | assert(this.recordMap.has(desc), `should run timing.start('${desc}') first`) 34 | 35 | const record = this.recordMap.get(desc)! 36 | record.end = Date.now() 37 | record.duration = record.end - record.start 38 | } 39 | 40 | toJSON(): TimingRecord[] { 41 | return this.recordList 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/server/src/master/gracefulExit.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../api' 2 | 3 | export function gracefulExit(app: Application): void { 4 | let closed = false 5 | function onSignal(signal: string): void { 6 | if (closed) return 7 | closed = true 8 | // app.coreLogger.info('[regax-gracefulExit] %s receive signal %s, closing', app.serverId, signal) 9 | app.stop().then(() => { 10 | // app.coreLogger.info('[regax-gracefulExit] %s close done, exiting with code: 0', app.serverId) 11 | process.exit(0) 12 | }).catch((e: Error) => { 13 | app.coreLogger.error('[regax-gracefulExit] %s close with error: ', app.serverId, e) 14 | process.exit(1) 15 | }) 16 | } 17 | function onExit(code: number): void { 18 | closed = true 19 | const level = code === 0 ? 'info' : 'error' 20 | app.coreLogger[level]('[regax-gracefulExit] %s process exit with code: %s', app.serverId, code) 21 | } 22 | // https://nodejs.org/api/process.html#process_signal_events 23 | // https://en.wikipedia.org/wiki/Unix_signal 24 | // kill(2) Ctrl-C 25 | process.once('SIGINT', onSignal.bind(undefined, 'SIGINT')) 26 | // kill(3) Ctrl-\ 27 | process.once('SIGQUIT', onSignal.bind(undefined, 'SIGQUIT')) 28 | // kill(15) default 29 | process.once('SIGTERM', onSignal.bind(undefined, 'SIGTERM')) 30 | process.once('exit', onExit) 31 | } 32 | -------------------------------------------------------------------------------- /packages/server/src/master/terminate.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | // fork from egg-cluster/lib/utils/terminate 4 | const awaitEvent = require('await-event') 5 | const pstree = require('ps-tree') 6 | import { delay } from '@regax/common' 7 | 8 | export async function terminate(subProcess: any, timeout: number): Promise { 9 | const pid = subProcess.process ? subProcess.process.pid : subProcess.pid 10 | const childPids = await getChildPids(pid) 11 | await Promise.all([ 12 | killProcess(subProcess, timeout), 13 | killChildren(childPids, timeout), 14 | ]) 15 | } 16 | 17 | // kill process, if SIGTERM not work, try SIGKILL 18 | async function killProcess(subProcess: any, timeout: number): Promise { 19 | subProcess.kill('SIGTERM') 20 | await Promise.race([ 21 | awaitEvent(subProcess, 'exit'), 22 | delay(timeout), 23 | ]) 24 | if (subProcess.killed) return 25 | // SIGKILL: http://man7.org/linux/man-pages/man7/signal.7.html 26 | // worker: https://github.com/nodejs/node/blob/master/lib/internal/cluster/worker.js#L22 27 | // subProcess.kill is wrapped to subProcess.destroy, it will wait to disconnected. 28 | (subProcess.process || subProcess).kill('SIGKILL') 29 | } 30 | 31 | // kill all children processes, if SIGTERM not work, try SIGKILL 32 | async function killChildren(children: number[], timeout: number): Promise { 33 | if (!children.length) return 34 | kill(children, 'SIGTERM') 35 | 36 | const start = Date.now() 37 | // if timeout is 1000, it will check twice. 38 | const checkInterval = 400 39 | let unterminated: number[] = [] 40 | 41 | while (Date.now() - start < timeout - checkInterval) { 42 | await delay(checkInterval) 43 | unterminated = getUnterminatedProcesses(children) 44 | if (!unterminated.length) return 45 | } 46 | kill(unterminated, 'SIGKILL') 47 | } 48 | 49 | async function getChildPids(pid: number): Promise { 50 | return new Promise(resolve => { 51 | pstree(pid, (err: Error, children: any[]) => { 52 | // if get children error, just ignore it 53 | if (err) children = [] 54 | resolve(children.map(child => parseInt(child.PID, 10))) 55 | }) 56 | }) 57 | } 58 | 59 | function kill(pids: number[], signal: string): void { 60 | for (const pid of pids) { 61 | try { 62 | process.kill(pid, signal) 63 | } catch (_) { 64 | // ignore 65 | } 66 | } 67 | } 68 | 69 | function getUnterminatedProcesses(pids: number[]): number[] { 70 | return pids.filter(pid => { 71 | try { 72 | // success means it's still alive 73 | process.kill(pid, 0) 74 | return true 75 | } catch (err) { 76 | // error means it's dead 77 | return false 78 | } 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /packages/server/src/master/workerRegistry.ts: -------------------------------------------------------------------------------- 1 | import { Registry, RegistryEvent, RegistryOpts, ServerInfo, ServerMap } from '@regax/rpc' 2 | import { delay, EventEmitter } from '@regax/common' 3 | import { Logger, defaultLogger } from '@regax/logger' 4 | 5 | export class WorkerRegistry extends EventEmitter implements Registry { 6 | protected started = false 7 | protected stopped = false 8 | protected registerMap: ServerMap = {} 9 | protected logger: Logger = defaultLogger 10 | protected version: 0 11 | constructor( 12 | readonly opts: RegistryOpts 13 | ) { 14 | super() 15 | if (opts.logger) this.logger = opts.logger 16 | } 17 | start(): void { 18 | if (this.started) return 19 | this.started = true 20 | this.emit(RegistryEvent.CONNECTION) 21 | } 22 | stop(): void { 23 | if (!this.started || this.stopped) return 24 | this.stopped = true 25 | this.off(RegistryEvent.CHANGED) 26 | } 27 | async register(serverInfo: ServerInfo): Promise { 28 | this.registerMap[serverInfo.serverId] = serverInfo 29 | this.emitChanged() 30 | } 31 | async unRegister(serverId: string): Promise { 32 | delete this.registerMap[serverId] 33 | this.emitChanged() 34 | } 35 | subscribe(fn: (servers: ServerMap) => void): () => void { 36 | let subscribeVersion = this.version 37 | const listener = async () => { 38 | try { 39 | if (!this.started || this.stopped) return 40 | const servers = await this.getAllServers() 41 | if (subscribeVersion === this.version) return 42 | subscribeVersion = this.version 43 | fn(servers) 44 | } catch (e) { 45 | this.logger.error(e) 46 | this.emit(RegistryEvent.ERROR, e) 47 | } 48 | } 49 | listener.fn = fn 50 | this.on(RegistryEvent.CHANGED, listener) 51 | return () => this.unSubscribe(fn) 52 | } 53 | unSubscribe(fn: (servers: ServerMap) => void): void { 54 | this.off(RegistryEvent.CHANGED, fn) 55 | } 56 | async getServerInfo(serverId: string): Promise { 57 | return this.registerMap[serverId] 58 | } 59 | async getAllServers(): Promise { 60 | return this.registerMap 61 | } 62 | isConnected(): boolean { 63 | return this.started && !this.stopped 64 | } 65 | updateServerMap(serverMap: ServerMap): void { 66 | this.registerMap = serverMap 67 | this.emitChanged() 68 | } 69 | protected emitChanged(): void { 70 | this.version ++ 71 | delay(0).then(() => { 72 | this.emit(RegistryEvent.CHANGED) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/server/src/monitor/monitor.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Client, createClient, LocalRegistry, ClientOpts, Registry, ServerMap, RPC_ERROR } from '@regax/rpc' 3 | import { ErrorCode } from '@regax/common' 4 | import { Logger, defaultLogger } from '@regax/logger' 5 | 6 | // export const DEFAULT_MONITOR_KEEPALIVE = 1000 7 | export const DEFAULT_MONITOR_INVOKE_TIMEOUT = 3000 8 | 9 | const rpcErrors = Object.values(RPC_ERROR) 10 | export interface MonitorOpts extends ClientOpts {} 11 | 12 | export class Monitor { 13 | readonly rpcClient: Client 14 | readonly rpcRegistry: Registry 15 | protected readonly logger: Logger = defaultLogger 16 | protected started: boolean = false 17 | protected stopped: boolean = false 18 | constructor( 19 | protected readonly opts: MonitorOpts = {} 20 | ) { 21 | if (opts.logger) this.logger = opts.logger 22 | this.rpcRegistry = opts.registry ? opts.registry : new LocalRegistry({ 23 | rootPath: opts.registryRootPath, 24 | logger: this.logger, 25 | }) 26 | this.rpcClient = createClient({ 27 | ...opts, 28 | clientId: 'monitor_client', 29 | registry: this.rpcRegistry, 30 | // keepalive: opts.keepalive || DEFAULT_MONITOR_KEEPALIVE, // monitor keep alive 31 | invokeTimeout: opts.invokeTimeout || DEFAULT_MONITOR_INVOKE_TIMEOUT, 32 | logger: this.logger, 33 | }) 34 | } 35 | async start(): Promise { 36 | if (this.started) return 37 | this.started = true 38 | this.rpcRegistry.start() 39 | await this.rpcClient.start() 40 | } 41 | stop(): void { 42 | if (!this.started || this.stopped) return 43 | this.stopped = true 44 | this.rpcRegistry.stop() 45 | this.rpcClient.stop() 46 | } 47 | getAllServers(): Promise { 48 | return this.rpcRegistry.getAllServers() 49 | } 50 | async checkServerAlive(serverId: string): Promise { 51 | try { 52 | return (await this.rpcClient.rpcInvoke(serverId, 'ping')) === 'pong' 53 | } catch (e) { 54 | if (e.code === ErrorCode.TIMEOUT || rpcErrors.includes(e.code)) return false 55 | throw e 56 | } 57 | } 58 | async checkAllServersAlive(serverIds?: string[]): Promise { 59 | serverIds = serverIds || Object.keys(await this.getAllServers()) 60 | const unAliveServers: string[] = [] 61 | for (let i = 0; i < serverIds.length; i ++) { 62 | const serverId = serverIds[i] 63 | const isAlive = await this.checkServerAlive(serverId) 64 | if (!isAlive) unAliveServers.push(serverId) 65 | } 66 | return unAliveServers 67 | } 68 | async clearDiedServers(serverIds?: string[]): Promise { 69 | serverIds = serverIds || Object.keys(await this.getAllServers()) 70 | const unAliveServers: string[] = [] 71 | for (let i = 0; i < serverIds.length; i ++) { 72 | const serverId = serverIds[i] 73 | const isAlive = await this.checkServerAlive(serverId) 74 | if (!isAlive) { 75 | // remove from registry 76 | await this.rpcRegistry.unRegister(serverId) 77 | unAliveServers.push(serverId) 78 | } 79 | } 80 | return unAliveServers 81 | } 82 | async stopServer(serverId: string, waitTime?: number): Promise { 83 | return this.rpcClient.rpcInvoke(serverId, 'stop', waitTime !== undefined ? [waitTime] : []) 84 | } 85 | async restartServer(serverId: string, waitTime?: number): Promise { 86 | return this.rpcClient.rpcInvoke(serverId, 'restart', waitTime !== undefined ? [waitTime] : []) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/server/src/remote/backend/routeRemote.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Application, RouteType, SessionFields, RouteData } from '../../api' 3 | import { Remote } from '../index' 4 | import Router from '../../component/router' 5 | 6 | export class RouteRemote implements Remote { 7 | constructor( 8 | protected app: Application, 9 | ) { 10 | } 11 | routeInvoke(route: string, routeType: RouteType, sessionFields: SessionFields, args: any[], traceId: string): Promise { 12 | const app = this.app 13 | const router: Router = app.get('router') 14 | const routeData: RouteData = router.getRouteData(route, routeType, true) 15 | return (router.localRouter as any)[routeData.routeType](routeData, app.service.backendSession.create(sessionFields), args, traceId) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/server/src/remote/frontend/channelRemote.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../../api' 2 | import { InternalSession } from '../../component/session' 3 | import Connector from '../../component/connector' 4 | import { PlainData } from '@regax/common' 5 | import { Remote } from '../index' 6 | 7 | export class ChannelRemote implements Remote { 8 | constructor( 9 | protected app: Application, 10 | ) { 11 | } 12 | async pushMessage(route: string, msg: PlainData, uids: (string | number)[]): Promise { 13 | if (!msg) { 14 | this.app.logger.error('Can not send empty message! route : %j, compressed msg : %j', 15 | route, msg) 16 | return 17 | } 18 | const connector = this.app.get('connector') 19 | const sessionService = this.app.service.session 20 | const ss: InternalSession[] = [] 21 | for (let i = 0, l = uids.length; i < l; i++) { 22 | const sessions = sessionService.getByUid(uids[i]) 23 | if (sessions) { 24 | for (let j = 0, k = sessions.length; j < k; j++) { 25 | ss.push(sessions[j]) 26 | } 27 | } 28 | } 29 | connector.send(ss, 0, route, { data: msg }) 30 | } 31 | async broadcast(route: string, msg: PlainData, binded: boolean = true): Promise { 32 | const connector = this.app.get('connector') 33 | const sessionService = this.app.service.session 34 | const getSessions = () => { 35 | const sessions: InternalSession[] = [] 36 | if (binded) { 37 | sessionService.forEachBindedSession(session => sessions.push(session)) 38 | } else { 39 | sessionService.forEachSession(session => sessions.push(session)) 40 | } 41 | return sessions 42 | } 43 | connector.send(getSessions(), 0, route, { data: msg }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/server/src/remote/frontend/sessionRemote.ts: -------------------------------------------------------------------------------- 1 | import { Application, SessionFields } from '../../api' 2 | import { SessionService } from '../../service/sessionService' 3 | import { Remote } from '../index' 4 | import { PlainData, ObjectOf } from '@regax/common' 5 | 6 | export class SessionRemote implements Remote { 7 | constructor( 8 | protected app: Application, 9 | ) { 10 | } 11 | protected get sessionService(): SessionService { 12 | return this.app.service.session 13 | } 14 | async getBackendSessionBySid(sid: string | number): Promise { 15 | const session = this.sessionService.get(sid) 16 | if (session) return session.toFrontendSession().toJSON() 17 | } 18 | async getBackendSessionsByUid(uid: string | number): Promise { 19 | const sessions = this.sessionService.getByUid(uid) 20 | if (sessions) { 21 | return sessions.map(session => session.toFrontendSession().toJSON()) 22 | } 23 | } 24 | async kickBySessionId(sid: string | number, reason?: string): Promise { 25 | this.sessionService.kickBySessionId(sid, reason) 26 | } 27 | async kickByUid(uid: string | number, reason?: string): Promise { 28 | this.sessionService.kickByUid(uid, reason) 29 | } 30 | async bind(sid: string | number, uid: string | number): Promise { 31 | this.sessionService.bind(sid, uid) 32 | } 33 | async unbind(sid: string | number, uid: string | number): Promise { 34 | this.sessionService.unbind(sid, uid) 35 | } 36 | async push(sid: string | number, values: ObjectOf): Promise { 37 | this.sessionService.import(sid, values) 38 | } 39 | async pushAll(sid: string | number, values: ObjectOf): Promise { 40 | this.sessionService.importAll(sid, values) 41 | } 42 | async sendMessage(sid: string | number, route: string, msg: PlainData): Promise { 43 | return this.sessionService.sendMessage(sid, route, msg) 44 | } 45 | async sendMessageByUid(uid: string | number, route: string, msg: PlainData): Promise { 46 | return this.sessionService.sendMessageByUid(uid, route, msg) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/server/src/remote/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | // Remote interface 4 | export type Remote = { 5 | /** 6 | * @param serverId - server remote address 7 | */ 8 | readonly [P in keyof T]?: (...args: any[]) => Promise 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/src/service/connectionService.ts: -------------------------------------------------------------------------------- 1 | import { values } from '@regax/common' 2 | import { Application } from '../application' 3 | 4 | export interface ConnectionLoginInfo { 5 | loginTime: number, 6 | uid: number | string 7 | address: string 8 | } 9 | 10 | export interface ConnectionStatisticsInfo { 11 | serverId: string, 12 | loginedCount: number, 13 | totalConnCount: number, 14 | loginedList: ConnectionLoginInfo[] 15 | } 16 | 17 | /** 18 | * connection statistics service 19 | * record connection, login count and list 20 | */ 21 | export class ConnectionService { 22 | protected loginedCount = 0 // login count 23 | protected connCount = 0 // connection count 24 | protected logined: { [uid: string]: ConnectionLoginInfo } = {} 25 | constructor( 26 | protected app: Application 27 | ) { 28 | } 29 | get totalConnCount(): number { 30 | return this.connCount 31 | } 32 | /** 33 | * Add logined user 34 | * @param uid user id 35 | * @param info record for logined user 36 | */ 37 | addLoginedUser(uid: string | number, info: ConnectionLoginInfo): void { 38 | if (!this.logined[uid]) { 39 | this.loginedCount ++ 40 | } 41 | this.logined[uid] = { ...info, uid } 42 | } 43 | /** 44 | * Update user info. 45 | * @param uid user id 46 | * @param info info for update. 47 | */ 48 | updateUserInfo(uid: string | number, info: {}): void { 49 | const user = this.logined[uid] 50 | if (!user) return 51 | this.logined[uid] = { ...user, ...info } 52 | } 53 | 54 | /** 55 | * @param uid user id 56 | */ 57 | removeLoginedUser(uid: string | number): void { 58 | if (this.logined[uid]) { 59 | this.loginedCount -- 60 | } 61 | delete this.logined[uid] 62 | } 63 | /** 64 | * Increase connection count 65 | */ 66 | increaseConnectionCount(): void { 67 | this.connCount++ 68 | } 69 | decreaseConnectionCount(uid?: string | number): void { 70 | if (this.connCount) { 71 | this.connCount -- 72 | } 73 | if (uid) { 74 | this.removeLoginedUser(uid) 75 | } 76 | } 77 | /** 78 | * Get statistics info 79 | */ 80 | getStatisticsInfo(): ConnectionStatisticsInfo { 81 | return { 82 | serverId: this.app.serverId, 83 | loginedCount: this.loginedCount, 84 | totalConnCount: this.connCount, 85 | loginedList: values(this.logined) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/server/src/service/globalChannelService.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../application' 2 | import { RemoteCache } from '@regax/rpc' 3 | import RPC from '../component/rpc' 4 | 5 | type SID = string | number // frontend server id 6 | type UID = string | number // user id 7 | 8 | export const DEFAULT_CHANNEL_PREFIX = 'REGAX:CHANNEL' 9 | // tslint:disable:no-any 10 | export class GlobalChannelService { 11 | constructor( 12 | protected readonly app: Application, 13 | protected readonly rpc: RPC, 14 | protected readonly remoteCache: RemoteCache, // like ioredis 15 | protected readonly channelPrefix: string = DEFAULT_CHANNEL_PREFIX, 16 | ) {} 17 | async add(name: string, sid: SID, uid: UID): Promise { 18 | const key = this.genKey(name, sid) 19 | await this.remoteCache.sadd(key, uid) 20 | } 21 | async leave(name: string, sid: SID, uid: UID): Promise { 22 | const key = this.genKey(name, sid) 23 | await this.remoteCache.srem(key, uid) 24 | } 25 | async getMembersBySid(name: string, sid: SID): Promise { 26 | const key = this.genKey(name, sid) 27 | return this.remoteCache.smembers(key) 28 | } 29 | async getMembersByChannelName(name: string, serverType: string): Promise { 30 | const servers = this.rpc.getServersByType(serverType) 31 | if (servers.length === 0) return [] 32 | const uids: any = {} 33 | for (let i = 0, l = servers.length; i < l; i++) { 34 | const items = await this.getMembersBySid(name, servers[i]) 35 | items.forEach(item => (uids[item] = 1)) 36 | } 37 | return Object.keys(uids) 38 | } 39 | async destroyChannel(name: string, serverType: string): Promise { 40 | const servers = this.rpc.getServersByType(serverType) 41 | if (servers.length === 0) return 42 | for (let i = 0, l = servers.length; i < l; i++) { 43 | await this.remoteCache.del(this.genKey(name, servers[i])) 44 | } 45 | } 46 | async pushMessage(serverType: string, channelName: string, route: string, msg: any): Promise { 47 | const servers = this.rpc.getServersByType(serverType) 48 | if (servers.length === 0) return 49 | const rpc = this.app.service.rpc 50 | for (let i = 0, l = servers.length; i < l; i++) { 51 | const serverId = servers[i] 52 | const uids = await this.getMembersBySid(channelName, servers[i]) 53 | if (uids.length > 0) { 54 | rpc.remote(serverId).channel.pushMessage(route, msg, uids) 55 | } 56 | } 57 | } 58 | protected genKey(name: string, sid: SID): string { 59 | return `${this.channelPrefix}:${name}:${sid}` 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/server/src/service/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { ChannelService } from './channelService' 3 | import { ConnectionService } from './connectionService' 4 | import { GlobalChannelService } from './globalChannelService' 5 | import { SessionService } from './sessionService' 6 | import { BackendSessionService } from './backendSessionService' 7 | import { RPCService } from './rpcService' 8 | import { MessengerService } from './messengerService' 9 | 10 | export * from './channelService' 11 | export * from './connectionService' 12 | export * from './globalChannelService' 13 | export * from './sessionService' 14 | export * from './backendSessionService' 15 | export * from './rpcService' 16 | export * from './messengerService' 17 | 18 | export interface Service { 19 | channel: ChannelService, 20 | globalChannel: GlobalChannelService, 21 | session: SessionService, 22 | backendSession: BackendSessionService, 23 | connection: ConnectionService, 24 | rpc: RPCService 25 | messenger: MessengerService 26 | [key: string]: any 27 | } 28 | -------------------------------------------------------------------------------- /packages/server/src/service/messengerService.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | 3 | import { Fn } from '@regax/common' 4 | 5 | export interface MessengerService { 6 | onMessage(type: string, fn: Fn): void 7 | send(type: string, data: any): void 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/src/service/rpcService.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { Session, RouteType, Application } from '../api' 3 | import { ServerServices } from '@regax/rpc' 4 | import { ChannelRemote } from '../remote/frontend/channelRemote' 5 | import { SessionRemote } from '../remote/frontend/sessionRemote' 6 | import Router from '../component/router' 7 | import { createProxy } from '../util/proxy' 8 | import RPC from '../component/rpc' 9 | 10 | interface RemoteServices { 11 | channel: ChannelRemote, 12 | session: SessionRemote, 13 | } 14 | 15 | export class RPCService { 16 | constructor( 17 | protected app: Application 18 | ) { 19 | } 20 | invoke(route: string, session: Session, args: any[], traceId?: string): Promise { 21 | return this.app.get('router').invoke(route, RouteType.RPC, session, args, traceId) 22 | } 23 | remote(serverId: string): RemoteServices { 24 | const proxy = this.app.get('rpc').invokeProxy(serverId) 25 | return { 26 | channel: createProxy((method: string) => (...args: any[]) => proxy.channelRemote(method, args)), 27 | session: createProxy((method: string) => (...args: any[]) => proxy.sessionRemote(method, args)), 28 | } 29 | } 30 | invokeServer(serverId: string, serviceName: string, ...args: any[]): Promise { 31 | return this.app.get('rpc').rpcInvoke(serverId, serviceName, args) 32 | } 33 | extendServices(services: ServerServices): void { 34 | this.app.get('rpc').extendServices(services) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/server/src/util/compose.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | // Fork from https://github.com/koajs/compose/blob/master/index.js 3 | 4 | export interface ComposeMiddleware { 5 | (...args: any): any, 6 | } 7 | export interface ComposeDispatcher { 8 | (context: Context, next: any): Promise 9 | } 10 | 11 | export function compose(middlewares: ComposeMiddleware[]): ComposeDispatcher { 12 | if (!Array.isArray(middlewares)) throw new TypeError('Middleware stack must be an array!') 13 | for (const fn of middlewares) { 14 | if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') 15 | } 16 | return (context, next) => { 17 | let index = -1 18 | return dispatch(0) 19 | function dispatch(i: number, error?: string | Error): Promise { 20 | if (error) return Promise.reject(typeof error === 'string' ? new Error(error) : error) 21 | if (i <= index) return Promise.reject(new Error('next() called multiple times')) 22 | index = i 23 | let fn = middlewares[i] 24 | if (i === middlewares.length) fn = next 25 | if (!fn) return Promise.resolve() 26 | try { 27 | return Promise.resolve(fn(context, dispatch.bind(undefined, i + 1))) 28 | } catch (err) { 29 | return Promise.reject(err) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/src/util/fs.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import * as path from 'path' 3 | import * as fs from 'fs' 4 | 5 | export function safeReaddirSync(dirname: string, fullPath = false): string[] { 6 | if (fs.existsSync(dirname)) { 7 | const list = fs.readdirSync(dirname) 8 | return fullPath ? list.map(l => path.join(dirname, l)) : list 9 | } 10 | return [] 11 | } 12 | 13 | export function tryToRequire(filePath: string): any { 14 | if (fs.existsSync(`${filePath}.js`) || fs.existsSync(`${filePath}.ts`)) { 15 | const module = require(filePath) 16 | return module && module.default ? module.default : module 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/server/src/util/logger.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | import { ApplicationOpts, ApplicationEnv } from '../api/application' 3 | import { Application } from '../application' 4 | import { RegaxLoggerManager } from '@regax/logger' 5 | 6 | export function createLoggerManager(app: Application): RegaxLoggerManager { 7 | const loggerConfig = app.getConfig('logger') || {} 8 | const customLogger = app.getConfig('customLogger') || {} 9 | loggerConfig.type = app.serverType 10 | 11 | if (app.env === ApplicationEnv.prod && loggerConfig.level === 'DEBUG' && !loggerConfig.allowDebugAtProd) { 12 | loggerConfig.level = 'INFO' 13 | } 14 | 15 | const loggers = new RegaxLoggerManager(Object.assign({}, loggerConfig, { customLogger })) 16 | 17 | // won't print to console after started, except for local and unittest 18 | app.onReady(() => { 19 | if (loggerConfig.disableConsoleAfterReady) { 20 | loggers.disableConsole() 21 | } 22 | }) 23 | // loggers.coreLogger.info('[regax-logger] init all loggers with options: %j', loggerConfig) 24 | 25 | return loggers 26 | } 27 | -------------------------------------------------------------------------------- /packages/server/src/util/logo.ts: -------------------------------------------------------------------------------- 1 | const { version } = require('../../package.json') 2 | // tslint:disable:no-trailing-whitespace 3 | export function printLogo(): void { 4 | console.log(` 5 | 6 | ############################################################################################################################## 7 | 8 | RRRRRRRRRRRRRRRRR EEEEEEEEEEEEEEEEEEEEEE GGGGGGGGGGGGG AAA XXXXXXX XXXXXXX 9 | R::::::::::::::::R E::::::::::::::::::::E GGG::::::::::::G A:::A X:::::X X:::::X 10 | R::::::RRRRRR:::::R E::::::::::::::::::::E GG:::::::::::::::G A:::::A X:::::X X:::::X 11 | RR:::::R R:::::REE::::::EEEEEEEEE::::E G:::::GGGGGGGG::::G A:::::::A X::::::X X::::::X 12 | R::::R R:::::R E:::::E EEEEEE G:::::G GGGGGG A:::::::::A XXX:::::X X:::::XXX 13 | R::::R R:::::R E:::::E G:::::G A:::::A:::::A X:::::X X:::::X 14 | R::::RRRRRR:::::R E::::::EEEEEEEEEE G:::::G A:::::A A:::::A X:::::X:::::X 15 | R:::::::::::::RR E:::::::::::::::E G:::::G GGGGGGGGGG A:::::A A:::::A X:::::::::X 16 | R::::RRRRRR:::::R E:::::::::::::::E G:::::G G::::::::G A:::::A A:::::A X:::::::::X 17 | R::::R R:::::R E::::::EEEEEEEEEE G:::::G GGGGG::::G A:::::AAAAAAAAA:::::A X:::::X:::::X 18 | R::::R R:::::R E:::::E G:::::G G::::G A:::::::::::::::::::::A X:::::X X:::::X 19 | R::::R R:::::R E:::::E EEEEEE G:::::G G::::G A:::::AAAAAAAAAAAAA:::::A XXX:::::X X:::::XXX 20 | RR:::::R R:::::REE::::::EEEEEEEE:::::E G:::::GGGGGGGG::::G A:::::A A:::::A X::::::X X::::::X 21 | R::::::R R:::::RE::::::::::::::::::::E GG:::::::::::::::G A:::::A A:::::A X:::::X X:::::X 22 | R::::::R R:::::RE::::::::::::::::::::E GGG::::::GGG:::G A:::::A A:::::A X:::::X X:::::X 23 | RRRRRRRR RRRRRRREEEEEEEEEEEEEEEEEEEEEE GGGGGG GGGGAAAAAAA AAAAAAAXXXXXXX XXXXXXX 24 | 25 | version: ${version} 26 | 27 | ############################################################################################################################## 28 | 29 | `) 30 | } 31 | -------------------------------------------------------------------------------- /packages/server/src/util/proxy.spec.ts: -------------------------------------------------------------------------------- 1 | import * as expect from 'expect' 2 | // tslint:disable:no-any 3 | import { createProxy } from './proxy' 4 | describe('util.proxy', () => { 5 | it('util.proxy.createProxy', () => { 6 | const services = { 7 | s1: { 8 | s2: { 9 | add(n1: number, n2: number): number { 10 | return n1 + n2 11 | } 12 | } 13 | } 14 | } 15 | const servicesProxy = createProxy((s1, s2, method) => { 16 | return (services as any)[s1][s2][method] 17 | }, 3) 18 | expect(servicesProxy.s1.s2.add(3, 4)).toEqual(7) 19 | // twice 20 | expect(servicesProxy.s1.s2.add(3, 4)).toEqual(7) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/server/src/util/proxy.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-any 2 | /** 3 | * @param args - init service args 4 | * @param fn - service method 5 | * @param depth - call depth 6 | * @param depIndex - current dep index 7 | * @example 8 | * const myServices = { 9 | * s1: { 10 | * s2: { 11 | * add(a, b): number { 12 | * return a + b 13 | * } 14 | * } 15 | * } 16 | * } 17 | * const serviceProxy = createProxy((service1, service2, method) => { 18 | * return myServices[service1][service2][method] 19 | * }, 3) 20 | * serviceProxy.s1.s2.add(3, 4) // 7 21 | */ 22 | export function createProxy(fn: (...args: any[]) => any, depth = 1, args: any[] = [], depIndex = 1): T { 23 | return new Proxy({}, { 24 | get(t, field: string): any { 25 | const newArgs = args.slice() 26 | newArgs.push(field) 27 | if (depIndex === depth) { 28 | return fn(...newArgs) 29 | } 30 | return createProxy(fn, depth, newArgs, depIndex + 1) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/src/util/queue.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Fn, MaybePromise } from '@regax/common' 2 | 3 | const DEFAULT_TIMEOUT = 3000 4 | export enum QueueEvent { 5 | CLOSED = 'closed', 6 | DRAINED = 'drained', 7 | TIMEOUT = 'timeout', 8 | ERROR = 'error' 9 | } 10 | 11 | export enum QueueStatus { 12 | IDLE, 13 | BUSY, // queue is working for some tasks now 14 | CLOSED, // queue has closed and would not receive task any more and is processing the remaining tasks now. 15 | DRAINED, // queue is ready to be destroy 16 | } 17 | 18 | export interface QueueTask { 19 | id?: number, 20 | fn: Fn, 21 | onTimeout?: Fn, 22 | timeout?: number 23 | } 24 | 25 | export class SeqQueue extends EventEmitter { 26 | protected queue: QueueTask[] = [] 27 | protected curId = 0 28 | protected status = QueueStatus.IDLE 29 | protected timerId?: NodeJS.Timer 30 | constructor( 31 | protected readonly timeout: number = DEFAULT_TIMEOUT 32 | ) { 33 | super() 34 | this.queue = [] 35 | } 36 | push(fn: Fn>, onTimeout?: Fn, timeout?: number): boolean { 37 | if (this.isClosed()) return false 38 | this.queue.push({ fn, onTimeout, timeout }) 39 | if (this.status === QueueStatus.IDLE) { 40 | this.status = QueueStatus.BUSY 41 | this.runTask(this.curId) 42 | } 43 | return true 44 | } 45 | isClosed(): boolean { 46 | return this.status !== QueueStatus.IDLE && this.status !== QueueStatus.BUSY 47 | } 48 | protected runTask(taskId: number): void { 49 | process.nextTick(() => { 50 | this.next(taskId) 51 | }) 52 | } 53 | close(force: boolean): void { 54 | if (this.isClosed()) { 55 | // ignore invalid status 56 | return 57 | } 58 | 59 | if (force) { 60 | this.status = QueueStatus.DRAINED 61 | if (this.timerId) { 62 | clearTimeout(this.timerId) 63 | this.timerId = undefined 64 | } 65 | this.emit(QueueEvent.DRAINED) 66 | } else { 67 | this.status = QueueStatus.CLOSED 68 | this.emit(QueueEvent.CLOSED) 69 | } 70 | } 71 | protected async next(taskId: number): Promise { 72 | if (taskId !== this.curId || this.status !== QueueStatus.BUSY && this.status !== QueueStatus.CLOSED) { 73 | // ignore invalid next call 74 | return 75 | } 76 | 77 | if (this.timerId) { 78 | clearTimeout(this.timerId) 79 | this.timerId = undefined 80 | } 81 | 82 | const task = this.queue.shift() 83 | if (!task) { 84 | if (this.status === QueueStatus.BUSY) { 85 | this.status = QueueStatus.IDLE 86 | this.curId++ // modify curId to invalidate timeout task 87 | } else { 88 | this.status = QueueStatus.DRAINED 89 | this.emit(QueueEvent.DRAINED) 90 | } 91 | return 92 | } 93 | 94 | task.id = ++this.curId 95 | 96 | const timeout = task.timeout ? task.timeout : this.timeout 97 | this.timerId = global.setTimeout(() => { 98 | this.runTask(task.id!) 99 | this.emit(QueueEvent.TIMEOUT, task) 100 | if (task.onTimeout) { 101 | task.onTimeout() 102 | } 103 | }, timeout) 104 | 105 | try { 106 | // tslint:disable-next-line 107 | await task.fn() 108 | } catch (err) { 109 | this.emit(QueueEvent.ERROR, err, task) 110 | } finally { 111 | // run next task 112 | this.runTask(task.id) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/server/src/util/readFiles.ts: -------------------------------------------------------------------------------- 1 | import * as fs from './fs' 2 | import * as path from 'path' 3 | 4 | // support ts 5 | const defaultLoadFilter = (name: string): boolean => (path.extname(name) === '.ts' && !/.d.ts$/.test(name)) || path.extname(name) === '.js' 6 | 7 | export function readFiles(dir: string, fn: (fullPath: string, name: string) => T, filter: (name: string) => boolean = defaultLoadFilter): { [key: string]: T } { 8 | let list = fs.safeReaddirSync(dir) 9 | if (filter) { 10 | list = list.filter(n => filter(n)) 11 | } 12 | return list 13 | .reduce((res: { [key: string]: T }, key: string): { [key: string]: T } => { 14 | // remove file extname 15 | const name = key.replace(/\.[^\.]+$/, '') 16 | // maybe ts loaded 17 | if (name in res) return res 18 | res[name] = fn(path.join(dir, name), name) 19 | return res 20 | }, {}) 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/src/util/routeUtil.ts: -------------------------------------------------------------------------------- 1 | interface RouteData { 2 | route: string, 3 | serverType: string, 4 | method: string, 5 | name: string, 6 | isFrontend: boolean, 7 | isBackend: boolean, 8 | } 9 | 10 | // like 'connector.entry.enter' 11 | export function parseRoute(route: string): RouteData | undefined { 12 | if (!route) return 13 | const ts = route.split('.') 14 | if (ts.length !== 3) { 15 | return 16 | } 17 | const isFrontend = ts[0] === 'connector' || ts[0] === 'gate' 18 | return { 19 | route: route, 20 | serverType: ts[0], 21 | name: ts[1], 22 | method: ts[2], 23 | isFrontend, 24 | isBackend: !isFrontend, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./configs/base.tsconfig", 3 | "compilerOptions": { 4 | "skipLibCheck": false, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@regax/common": "packages/common/src", 8 | "@regax/logger": "packages/logger/src", 9 | "@regax/protocol": "packages/protocol/src/protocol", 10 | "@regax/server": "packages/server/src", 11 | "@regax/client-websocket": "packages/client-websocket/src", 12 | "@regax/client-udpsocket": "packages/client-udpsocket/src", 13 | "@regax/rpc": "packages/rpc/src", 14 | "@regax/logrotator": "packages/logrotator/src", 15 | "@regax/scheduler": "packages/scheduler/src" 16 | } 17 | }, 18 | "include": [ 19 | "dev-packages/*/src", 20 | "packages/*/src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tsfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true 3 | } 4 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": [ 3 | "Lint rules expected to be used in interactive editors." 4 | ], 5 | "extends": [ 6 | "./configs/errors.tslint.json", 7 | "./configs/warnings.tslint.json" 8 | ] 9 | } 10 | --------------------------------------------------------------------------------