├── .eslintignore ├── .eslintrc ├── index.js ├── .gitignore ├── lib ├── utils.js └── connection.js ├── .github └── workflows │ ├── release.yml │ └── nodejs.yml ├── test ├── options.test.js ├── connect.test.js └── connection.test.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── index.d.ts ├── example └── rpc_demo.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/connection'); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | npm-debug.log* 5 | .logs 6 | logs 7 | *.swp 8 | run 9 | *-run 10 | .idea 11 | .DS_Store 12 | .tmp 13 | 14 | .* 15 | !.github 16 | !.eslintignore 17 | !.eslintrc 18 | !.gitignore 19 | !.travis.yml 20 | !.autod.conf.js 21 | *.bin 22 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MAX_PACKET_ID = Math.pow(2, 30); // 避免 hessian 写大整数 4 | 5 | exports.id = 0; 6 | 7 | exports.nextId = () => { 8 | exports.id += 1; 9 | if (exports.id >= MAX_PACKET_ID) { 10 | exports.id = 1; 11 | } 12 | return exports.id; 13 | }; 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: node-modules/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | os: 'ubuntu-latest, macos-latest, windows-latest' 15 | version: '8, 10, 12, 14, 16, 18, 20, 22' 16 | secrets: 17 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 18 | -------------------------------------------------------------------------------- /test/options.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Decoder = require('sofa-bolt-node/lib/decoder'); 4 | const Encoder = require('sofa-bolt-node/lib/encoder'); 5 | const net = require('net'); 6 | const awaitEvent = require('await-event'); 7 | const assert = require('assert'); 8 | const Connection = require('../lib/connection'); 9 | 10 | const protocol = { 11 | name: 'Rpc', 12 | encoder: opts => new Encoder(opts), 13 | decoder: opts => new Decoder(opts), 14 | }; 15 | 16 | describe('test/options.test.js', () => { 17 | it('protocolOptions should work', async () => { 18 | const port = 12200; 19 | const server = net.createServer(); 20 | server.listen(port); 21 | const socket = net.createConnection({ port }); 22 | await awaitEvent(socket, 'connect'); 23 | const clientConn = new Connection({ 24 | logger: console, 25 | socket, 26 | protocol, 27 | protocolOptions: { 28 | mock: 'foo', 29 | }, 30 | }); 31 | assert(clientConn._encoder.options.mock === 'foo'); 32 | 33 | await clientConn.close(); 34 | server.close(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present node-modules and other contributors. 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connection", 3 | "version": "1.5.0", 4 | "engines": { 5 | "node": ">= 8.0.0" 6 | }, 7 | "description": "wrap for socket", 8 | "files": [ 9 | "lib", 10 | "index.js", 11 | "index.d.ts" 12 | ], 13 | "dependencies": { 14 | "await-first": "^1.0.0", 15 | "pump": "^3.0.0", 16 | "sdk-base": "^3.5.1" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "20", 20 | "await-event": "^2.1.0", 21 | "egg-bin": "^4.7.1", 22 | "eslint": "^5.2.0", 23 | "eslint-config-egg": "^7.0.0", 24 | "git-contributor": "^2.1.5", 25 | "mm": "^2.2.2", 26 | "sofa-bolt-node": "^1.1.1" 27 | }, 28 | "scripts": { 29 | "lint": "eslint . --ext .js", 30 | "cov": "egg-bin cov", 31 | "test": "npm run lint && npm run test-local", 32 | "test-local": "egg-bin test", 33 | "ci": "npm run lint && npm run cov", 34 | "contributor": "git-contributor" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/node-modules/connection.git" 39 | }, 40 | "keywords": [ 41 | "socket", 42 | "tcp", 43 | "connection" 44 | ], 45 | "author": "killagu ", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/node-modules/connection/issues" 49 | }, 50 | "homepage": "https://github.com/node-modules/connection#readme" 51 | } 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.5.0](https://github.com/node-modules/connection/compare/v1.4.0...v1.5.0) (2024-06-19) 4 | 5 | 6 | ### Features 7 | 8 | * remove @types/node from dependencies ([#7](https://github.com/node-modules/connection/issues/7)) ([ef2dd03](https://github.com/node-modules/connection/commit/ef2dd03422bcfb9c5305c28f5ff0050d92f33b48)) 9 | 10 | 11 | --- 12 | 13 | 14 | 1.4.0 / 2020-11-03 15 | ================== 16 | 17 | **features** 18 | * [[`8a5c908`](http://github.com/node-modules/connection/commit/8a5c908056e41453a1879b9fd45a76db76578815)] - feat: impl writeHeartbeat/writeHeartbeatAck (weijiafu14 <<1527752351@qq.com>>) 19 | 20 | 1.3.0 / 2020-10-10 21 | ================== 22 | 23 | **features** 24 | * [[`c8fb1b8`](http://github.com/node-modules/connection/commit/c8fb1b80677e944cd6b8c976bb56675a623ed538)] - feat: add ts type definition (#5) (killa <>) 25 | 26 | 1.2.0 / 2020-08-25 27 | ================== 28 | 29 | **features** 30 | * [[`60afa93`](http://github.com/node-modules/connection/commit/60afa939ea28bc637ef76c2ce6226f293df61e08)] - feat: add protocolOptions (killagu <>) 31 | 32 | 1.1.0 / 2019-08-02 33 | ================== 34 | 35 | **features** 36 | * [[`f518e35`](http://github.com/node-modules/connection/commit/f518e351dc6e7d7069e15d42e692d7b9c083c5e4)] - feat: oneway should throw encode error (killagu <>) 37 | 38 | 1.0.0 / 2019-03-19 39 | ================== 40 | 41 | **others** 42 | ,fatal: No names found, cannot describe anything. 43 | 44 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net'; 2 | import { 3 | Transform, 4 | Writable, 5 | TransformOptions, 6 | WritableOptions, 7 | } from 'stream'; 8 | import Base, { BaseOptions } from 'sdk-base'; 9 | 10 | export type WriteCallback = (err?: Error) => void; 11 | 12 | export interface Request { 13 | timeout: number; 14 | } 15 | 16 | export interface Response { 17 | } 18 | 19 | export interface ProtocolEncoder extends Transform { 20 | writeRequest(id: number, req: Request, cb: WriteCallback); 21 | 22 | writeResponse(req: Request, res: Response, cb: WriteCallback); 23 | } 24 | 25 | export interface ProtocolDecoder extends Writable { 26 | } 27 | 28 | type ProtocolOptions = TransformOptions & WritableOptions; 29 | 30 | export interface Protocol { 31 | name: string; 32 | 33 | encoder(protocolOptions: ProtocolOptions): ProtocolEncoder; 34 | 35 | decoder(protocolOptions: ProtocolOptions): ProtocolDecoder; 36 | } 37 | 38 | export interface ConnectionOptions extends BaseOptions { 39 | socket: Socket; 40 | logger: unknown; 41 | protocol: Protocol; 42 | 43 | protocolOptions?: ProtocolOptions; 44 | sendReqs?: Map; 45 | url?: string; 46 | connectTimeout?: number; 47 | } 48 | 49 | export default interface Connection extends Base { 50 | constructor(options: ConnectionOptions); 51 | 52 | /** 53 | * write request and wait response 54 | * @param req 55 | */ 56 | writeRequest(req: Request): Promise; 57 | 58 | /** 59 | * write heartbeat and wait heartbeatAck 60 | * @param hb 61 | */ 62 | writeHeartbeat(hb: Request): Promise; 63 | 64 | /** 65 | * write response 66 | * @param res 67 | */ 68 | writeResponse(res: Response): Promise; 69 | 70 | /** 71 | * write heartbeatAck 72 | * @param hb 73 | */ 74 | writeHeartbeatAck(hb: Response): Promise; 75 | 76 | /** 77 | * write request and not wait response 78 | * @param req 79 | */ 80 | oneway(req: Request); 81 | 82 | /** 83 | * close the connection and not wait inflight request 84 | */ 85 | forceClose(): Promise; 86 | 87 | /** 88 | * close the connection and wait inflight request 89 | */ 90 | close(): Promise; 91 | } 92 | 93 | 94 | -------------------------------------------------------------------------------- /example/rpc_demo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | const awaitFirst = require('await-first'); 5 | const awaitEvent = require('await-event'); 6 | const Connection = require('..'); 7 | const Decoder = require('sofa-bolt-node/lib/decoder'); 8 | const Encoder = require('sofa-bolt-node/lib/encoder'); 9 | const protocol = { 10 | encoder: opts => new Encoder(opts), 11 | decoder: opts => new Decoder(opts), 12 | }; 13 | 14 | const HOST = '127.0.0.1'; 15 | const PORT = 12200; 16 | 17 | async function createConnection(hostname, port) { 18 | const socket = net.connect(port, hostname); 19 | await awaitFirst(socket, [ 'connect', 'error' ]); 20 | return new Connection({ 21 | logger: console, 22 | socket, 23 | protocol, 24 | }); 25 | } 26 | 27 | async function createServer(port) { 28 | const server = net.createServer(); 29 | server.listen(port); 30 | await awaitEvent(server, 'listening'); 31 | return server; 32 | } 33 | 34 | async function waitServerConnection(server) { 35 | const socket = await awaitEvent(server, 'connection'); 36 | return new Connection({ 37 | logger: console, 38 | socket, 39 | protocol, 40 | }); 41 | } 42 | 43 | async function doRequest(conn) { 44 | return conn.writeRequest({ 45 | timeout: 100, 46 | targetAppName: 'foo', 47 | args: [ 'peter' ], 48 | serverSignature: 'com.alipay.sofa.rpc.quickstart.HelloService:1.0', 49 | methodName: 'sayHello', 50 | methodArgSigs: [ 'java.lang.String' ], 51 | requestProps: null, 52 | }); 53 | } 54 | 55 | async function doResponse(conn) { 56 | const req = await awaitEvent(conn, 'request'); 57 | console.log('get request: ', req); 58 | return conn.writeResponse(req, { 59 | error: null, 60 | appResponse: 'hello, peter', 61 | responseProps: null, 62 | }); 63 | } 64 | 65 | async function main() { 66 | let server; 67 | let clientConn; 68 | let serverConn; 69 | try { 70 | server = await createServer(PORT); 71 | const serverPromise = waitServerConnection(server); 72 | clientConn = await createConnection(HOST, PORT); 73 | serverConn = await serverPromise; 74 | doResponse(serverConn); 75 | const res = await doRequest(clientConn); 76 | console.log('get response: ', res); 77 | } finally { 78 | await Promise.all([ 79 | clientConn.close(), 80 | serverConn.close(), 81 | ]); 82 | server.close(); 83 | } 84 | } 85 | 86 | main().catch(console.log); 87 | -------------------------------------------------------------------------------- /test/connect.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | const awaitEvent = require('await-event'); 5 | const assert = require('assert'); 6 | const Connection = require('../lib/connection'); 7 | const Decoder = require('sofa-bolt-node/lib/decoder'); 8 | const Encoder = require('sofa-bolt-node/lib/encoder'); 9 | const protocol = { 10 | name: 'Rpc', 11 | encoder: opts => new Encoder(opts), 12 | decoder: opts => new Decoder(opts), 13 | }; 14 | 15 | describe('connect.test.js', () => { 16 | let server; 17 | 18 | beforeEach(async () => { 19 | server = net.createServer(); 20 | server.listen(12200); 21 | await awaitEvent(server, 'listening'); 22 | }); 23 | 24 | afterEach(() => { 25 | server.close(); 26 | }); 27 | 28 | it('connect success', async () => { 29 | const socket = net.createConnection(12200); 30 | const conn = new Connection({ 31 | socket, 32 | logger: console, 33 | protocol, 34 | }); 35 | await conn.ready(); 36 | await conn.close(); 37 | }); 38 | 39 | it('connect timeout', async () => { 40 | const socket = net.createConnection({ host: '2.2.2.2', port: 12200 }); 41 | const conn = new Connection({ 42 | socket, 43 | logger: console, 44 | protocol, 45 | connectTimeout: 1, 46 | url: '2.2.2.2:12200', 47 | }); 48 | let error; 49 | try { 50 | await conn.ready(); 51 | } catch (e) { 52 | error = e; 53 | } finally { 54 | assert(error); 55 | assert(error.name === 'RpcSocketConnectTimtoutError'); 56 | assert(error.message === 'connect timeout(1ms), 2.2.2.2:12200'); 57 | } 58 | assert(conn._closed === true); 59 | assert(conn.socket.destroyed === true); 60 | assert(conn._encoder.destroyed === true); 61 | assert(conn._decoder.destroyed === true); 62 | }); 63 | 64 | it('connect error', async () => { 65 | const socket = net.createConnection({ host: 'never_can_found', port: 12200 }); 66 | const conn = new Connection({ 67 | socket, 68 | logger: console, 69 | protocol, 70 | url: '2.2.2.2:12200', 71 | connectTimeout: 200, 72 | }); 73 | let error; 74 | try { 75 | await conn.ready(); 76 | } catch (e) { 77 | error = e; 78 | } finally { 79 | assert(error); 80 | assert(error.name === 'RpcSocketError' || error.name === 'RpcSocketConnectTimtoutError'); 81 | } 82 | assert(conn._closed === true); 83 | assert(conn.socket.destroyed === true); 84 | assert(conn._encoder.destroyed === true); 85 | assert(conn._decoder.destroyed === true); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connection 2 | 3 | [connection](https://github.com/node-modules/connection) socket wrapper 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![CI](https://github.com/node-modules/connection/actions/workflows/nodejs.yml/badge.svg?branch=master)](https://github.com/node-modules/connection/actions/workflows/nodejs.yml) 7 | [![Test coverage][codecov-image]][codecov-url] 8 | [![Known Vulnerabilities][snyk-image]][snyk-url] 9 | [![npm download][download-image]][download-url] 10 | 11 | [npm-image]: https://img.shields.io/npm/v/connection.svg?style=flat-square 12 | [npm-url]: https://npmjs.org/package/connection 13 | [codecov-image]: https://codecov.io/gh/node-modules/connection/branch/master/graph/badge.svg 14 | [codecov-url]: https://codecov.io/gh/node-modules/connection 15 | [snyk-image]: https://snyk.io/test/npm/connection/badge.svg?style=flat-square 16 | [snyk-url]: https://snyk.io/test/npm/connection 17 | [download-image]: https://img.shields.io/npm/dm/connection.svg?style=flat-square 18 | [download-url]: https://npmjs.org/package/connection 19 | 20 | ## Usage 21 | 22 | ### Client Socket 23 | 24 | ```js 25 | const net = require('net'); 26 | const awaitFirst = require('await-first'); 27 | const Connection = require('connection'); 28 | 29 | const Decoder = require('sofa-bolt-node/lib/decoder'); 30 | const Encoder = require('sofa-bolt-node/lib/encoder'); 31 | // bolt protocol example 32 | const protocol = { 33 | name: 'Rpc', 34 | encoder: opts => new Encoder(opts), 35 | decoder: opts => new Decoder(opts), 36 | }; 37 | 38 | async function createConnection(hostname, port) { 39 | const socket = net.connect(port, hostname); 40 | await awaitFirst(socket, [ 'connect', 'error' ]); 41 | return new Connection({ 42 | logger: console, 43 | socket, 44 | protocol, 45 | }); 46 | } 47 | 48 | const conn = await createConnection('127.0.0.1', 12200); 49 | 50 | conn.writeRequest({ 51 | targetAppName: 'foo', 52 | args: [ 'peter' ], 53 | serverSignature: 'com.alipay.sofa.rpc.quickstart.HelloService:1.0', 54 | methodName: 'sayHello', 55 | methodArgSigs: [ 'java.lang.String' ], 56 | requestProps: null, 57 | }); 58 | ``` 59 | 60 | ### Server Socket 61 | 62 | ```js 63 | const Connection = require('connection'); 64 | const server = net.createServer(); 65 | server.listen(port); 66 | 67 | server.on('connection', sock => { 68 | const conn = new Connection({ 69 | logger: console, 70 | socket: sock, 71 | protocol, 72 | }); 73 | 74 | conn.on('request', req => { 75 | conn.writeResponse(req, { 76 | error: null, 77 | appResponse: 'hello, peter', 78 | responseProps: null, 79 | }); 80 | }); 81 | }); 82 | ``` 83 | 84 | [More example](./example) 85 | 86 | ### API 87 | 88 | - oneway() - one way call 89 | - async writeRequest(req) - write request and wait response 90 | - async writeResponse(req, res) - write response 91 | - async close() - wait all pending request done and destroy the socket 92 | - async forceClose() - abort all pending request and destroy the socket 93 | - get protocolOptions() - encoder/decoder constructor options, can be overwrite when custom protocol 94 | 95 | ### Protocol implement 96 | 97 | ```typescript 98 | interface Request { 99 | /** 100 | * If request is oneway, shoule set to true 101 | */ 102 | oneway: boolean, 103 | /** 104 | * writeRequest will use the timeout to set the timer 105 | */ 106 | timeout: number, 107 | /** 108 | * request packet type, request|heartbeat|response 109 | */ 110 | packetType: string, 111 | } 112 | 113 | interface Response { 114 | packetId: number, 115 | } 116 | 117 | interface Encoder extends Transform { 118 | /** 119 | * write request to socket 120 | * Connection#writeRequest and Connection#oneway will call the function. 121 | * @param {number} id - the request id 122 | * @param {Object} req - the request object should be encoded 123 | * @param {Function} cb - the encode callback 124 | */ 125 | writeRequest(id: number, req: object, cb); 126 | /** 127 | * write response to socket 128 | * Connection#writeResponse will call the function. 129 | * @param {Object} req - the request object 130 | * @param {Object} res - the response object should be encoded 131 | * @param {Function} cb - the encode callback 132 | */ 133 | writeResponse(req: object, res: object, cb); 134 | } 135 | 136 | interface Decoder extends Writable { 137 | // events 138 | // - request emit when have request packet 139 | // - heartbeat emit when have heartbeat packet 140 | // - response emit when have response packet 141 | } 142 | 143 | interface Protocol { 144 | name: string; 145 | encode(options: any): Encoder; 146 | decode(options: any): Decoder; 147 | } 148 | ``` 149 | 150 | ## License 151 | 152 | [MIT](LICENSE) 153 | 154 | 155 | 156 | ## Contributors 157 | 158 | |[
killagu](https://github.com/killagu)
|[
gxcsoccer](https://github.com/gxcsoccer)
|[
popomore](https://github.com/popomore)
|[
weijiafu14](https://github.com/weijiafu14)
| 159 | | :---: | :---: | :---: | :---: | 160 | 161 | 162 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Wed Jun 19 2024 17:29:25 GMT+0800`. 163 | 164 | 165 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const Base = require('sdk-base'); 5 | const pump = require('pump'); 6 | const awaitFirst = require('await-first'); 7 | 8 | const utils = require('./utils'); 9 | const DEFAULT_ERROR_PREFIX = ''; 10 | 11 | class Connection extends Base { 12 | /** 13 | * @class 14 | * @param {object} options - 15 | * @param {Socket} options.socket - 16 | * @param {Logger} options.logger - 17 | * @param {Protocol} options.protocol - 18 | * @param {Object} [options.protocolOptions] - 19 | * @param {Map} [options.sentReqs] - 20 | * @param {string} [options.url] - 21 | * @param {number} [options.connectTimeout] - 22 | */ 23 | constructor(options) { 24 | assert(options.logger, '[Connection] options.logger is required'); 25 | assert(options.socket, '[Connection] options.socket is required'); 26 | assert(options.protocol, '[Connection] options.protocol is required'); 27 | assert(!options.socket.destroyed, '[Connection] options.socket should not be destroyed'); 28 | assert(options.protocol.encoder, '[Connection] options.protocol have not encoder impl'); 29 | assert(options.protocol.decoder, '[Connection] options.protocol have not decoder impl'); 30 | super(Object.assign(Connection.defaultOptions(), options, { initMethod: '_init' })); 31 | this._encoder = this.protocol.encoder(this.protocolOptions); 32 | this._decoder = this.protocol.decoder(this.protocolOptions); 33 | this._closed = false; 34 | this._userClosed = false; 35 | this._connected = !this.socket.connecting; 36 | this.bindEvent(); 37 | // @refer https://nodejs.org/en/docs/guides/backpressuring-in-streams/ 38 | pump(this._encoder, this.socket, this._decoder, err => { 39 | this._handleClose(err); 40 | }); 41 | this.buildErrorNames(this.protocol.name || DEFAULT_ERROR_PREFIX); 42 | this.on('error', err => this.logger.error(err)); 43 | } 44 | 45 | async _init() { 46 | this.url = this.options.url; 47 | if (this._connected) { 48 | if (!this.url) { 49 | this.url = `${this.socket.remoteAddress}:${this.socket.remotePort}`; 50 | } 51 | return; 52 | } 53 | this.socket.setTimeout(this.options.connectTimeout); 54 | const { event } = await awaitFirst(this.socket, [ 'connect', 'timeout', 'error' ]); 55 | if (event === 'timeout') { 56 | const err = new Error('connect timeout(' + this.options.connectTimeout + 'ms), ' + this.url); 57 | err.name = this.SocketConnectTimeoutError; 58 | this.close(); 59 | throw err; 60 | } 61 | if (!this.url) { 62 | this.url = `${this.socket.remoteAddress}:${this.socket.remotePort}`; 63 | } 64 | this.socket.setTimeout(0); 65 | this._connected = true; 66 | } 67 | 68 | bindEvent() { 69 | this._decoder.on('request', req => this.emit('request', req)); 70 | this._decoder.on('heartbeat', hb => this.emit('heartbeat', hb)); 71 | this._decoder.on('response', res => this._handleResponse(res)); 72 | this._decoder.on('heartbeat_ack', res => this._handleResponse(res)); 73 | } 74 | 75 | buildErrorNames(errorPrefix) { 76 | this.OneWayEncodeErrorName = `${errorPrefix}OneWayEncodeError`; 77 | this.SocketConnectTimeoutError = `${errorPrefix}SocketConnectTimtoutError`; 78 | this.ResponseEncodeErrorName = `${errorPrefix}ResponseEncodeError`; 79 | this.ResponseTimeoutErrorName = `${errorPrefix}ResponseTimeoutError`; 80 | this.RequestEncodeErrorName = `${errorPrefix}RequestEncodeError`; 81 | this.SocketErrorName = `${errorPrefix}SocketError`; 82 | this.SocketCloseError = `${errorPrefix}SocketCloseError`; 83 | } 84 | 85 | async writeRequest(req) { 86 | const id = utils.nextId(); 87 | const timer = this._requestTimer(id, req); 88 | try { 89 | const p = this._waitResponse(id, req); 90 | this._writeRequest(id, req); 91 | return await p; 92 | } catch (e) { 93 | this._cleanReq(id); 94 | throw e; 95 | } finally { 96 | clearTimeout(timer); 97 | } 98 | } 99 | 100 | async writeHeartbeat(hb) { 101 | const id = utils.nextId(); 102 | const timer = this._requestTimer(id, hb); 103 | try { 104 | const p = this._waitResponse(id, hb); 105 | this._writeHeartbeat(id, hb); 106 | return await p; 107 | } catch (e) { 108 | this._cleanReq(id); 109 | throw e; 110 | } finally { 111 | clearTimeout(timer); 112 | } 113 | } 114 | 115 | oneway(req) { 116 | assert(this._encoder.writeRequest, '[Connection] encoder have not impl writeRequest'); 117 | const id = utils.nextId(); 118 | req.oneway = true; 119 | this._encoder.writeRequest(id, req, err => { 120 | if (err) { 121 | err.name = this.OneWayEncodeErrorName; 122 | err.resultCode = '02'; 123 | this.emit('error', err); 124 | } 125 | }); 126 | } 127 | 128 | async writeResponse(req, res) { 129 | assert(this._encoder.writeResponse, '[Connection] encoder have not impl writeResponse'); 130 | return new Promise((resolve, reject) => { 131 | this._encoder.writeResponse(req, res, err => { 132 | if (!err) { 133 | return resolve(); 134 | } 135 | err.name = this.ResponseEncodeErrorName; 136 | err.resultCode = '02'; 137 | return reject(err); 138 | }); 139 | }); 140 | } 141 | 142 | async writeHeartbeatAck(hb) { 143 | assert(this._encoder.writeHeartbeatAck, '[Connection] encoder have not impl writeHeartbeatAck'); 144 | return new Promise((resolve, reject) => { 145 | this._encoder.writeHeartbeatAck(hb, err => { 146 | if (!err) { 147 | return resolve(); 148 | } 149 | err.name = this.ResponseEncodeErrorName; 150 | err.resultCode = '02'; 151 | return reject(err); 152 | }); 153 | }); 154 | } 155 | 156 | _requestTimer(id, req) { 157 | const start = Date.now(); 158 | return setTimeout(() => { 159 | const rt = Date.now() - start; 160 | const err = new Error('no response in ' + rt + 'ms, ' + this.url); 161 | err.name = this.ResponseTimeoutErrorName; 162 | err.resultCode = '03'; // 超时 163 | this._handleRequestError(id, err); 164 | }, req.timeout); 165 | } 166 | 167 | _writeRequest(id, req) { 168 | assert(this._encoder.writeRequest, '[Connection] encoder have not impl writeRequest'); 169 | this._encoder.writeRequest(id, req, err => { 170 | if (err) { 171 | err.name = this.RequestEncodeErrorName; 172 | err.resultCode = '02'; 173 | process.nextTick(() => { 174 | this._handleRequestError(id, err); 175 | }); 176 | } 177 | }); 178 | } 179 | 180 | _writeHeartbeat(id, hb) { 181 | assert(this._encoder.writeHeartbeat, '[Connection] encoder have not impl writeHeartbeat'); 182 | this._encoder.writeHeartbeat(id, hb, err => { 183 | if (err) { 184 | err.name = this.RequestEncodeErrorName; 185 | err.resultCode = '02'; 186 | process.nextTick(() => { 187 | this._handleRequestError(id, err); 188 | }); 189 | } 190 | }); 191 | } 192 | 193 | _waitResponse(id, req) { 194 | const event = 'response_' + id; 195 | let resReject; 196 | const resPromise = new Promise((resolve, reject) => { 197 | resReject = reject; 198 | this.once(event, resolve); 199 | }); 200 | this._sentReqs.set(id, { req, resPromise, resReject }); 201 | return resPromise; 202 | } 203 | 204 | async forceClose(err) { 205 | const closePromise = this.await('close'); 206 | if (err) { 207 | this._decoder.emit('error', err); 208 | } else { 209 | this._decoder.end(() => this._decoder.destroy()); 210 | } 211 | await closePromise; 212 | } 213 | 214 | async close(err) { 215 | if (this._userClosed) return; 216 | this._userClosed = true; 217 | const closeEvent = this.await('close'); 218 | // await pending request done 219 | await Promise.all( 220 | Array.from(this._sentReqs.values()) 221 | .map(data => data.resPromise) 222 | // catch the error, do noop, writeRequest will handle it 223 | ).catch(() => {}); 224 | if (err) { 225 | this._decoder.emit('error', err); 226 | } else { 227 | // flush data 228 | this._decoder.end(); 229 | } 230 | await closeEvent; 231 | } 232 | 233 | _cleanReq(id) { 234 | return this._sentReqs.delete(id); 235 | } 236 | 237 | _handleResponse(res) { 238 | const id = res.packetId; 239 | if (this._cleanReq(id)) { 240 | this.emit('response_' + id, res); 241 | } else { 242 | this.logger.warn('[Connection] can not find invoke request for response: %j, maybe it\'s timeout.', res); 243 | } 244 | } 245 | 246 | _handleRequestError(id, err) { 247 | if (!this._sentReqs.has(id)) { 248 | return; 249 | } 250 | const { resReject } = this._sentReqs.get(id); 251 | this._cleanReq(id); 252 | return resReject(err); 253 | } 254 | 255 | _handleClose(err) { 256 | if (this._closed) return; 257 | this._closed = true; 258 | if (err) { 259 | if (err.code === 'ECONNRESET') { 260 | this.logger.warn('[Connection] ECONNRESET, %s', this.url); 261 | } else { 262 | err.name = err.name === 'Error' ? this.SocketErrorName : err.name; 263 | err.message = err.message + ', ' + this.url; 264 | this.emit('error', err); 265 | } 266 | } 267 | this._cleanRequest(err); 268 | this._decoder.destroy(); 269 | this.emit('close'); 270 | } 271 | 272 | _cleanRequest(err) { 273 | if (!err) { 274 | err = new Error('The socket was closed. ' + this.url); 275 | err.name = this.SocketCloseError; 276 | err.resultCode = '02'; 277 | } 278 | for (const id of this._sentReqs.keys()) { 279 | this._handleRequestError(id, err); 280 | } 281 | } 282 | 283 | get _sentReqs() { 284 | return this.options.sentReqs; 285 | } 286 | 287 | get protocol() { 288 | return this.options.protocol; 289 | } 290 | 291 | get socket() { 292 | return this.options.socket; 293 | } 294 | 295 | get logger() { 296 | return this.options.logger; 297 | } 298 | 299 | get protocolOptions() { 300 | return Object.assign({ 301 | sentReqs: this._sentReqs, 302 | }, this.options.protocolOptions); 303 | } 304 | 305 | static defaultOptions() { 306 | return { 307 | sentReqs: new Map(), 308 | connectTimeout: 5000, 309 | }; 310 | } 311 | } 312 | 313 | module.exports = Connection; 314 | -------------------------------------------------------------------------------- /test/connection.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | const assert = require('assert'); 5 | const awaitEvent = require('await-event'); 6 | const mm = require('mm'); 7 | 8 | const Connection = require('../lib/connection'); 9 | const Decoder = require('sofa-bolt-node/lib/decoder'); 10 | const Encoder = require('sofa-bolt-node/lib/encoder'); 11 | const protocol = { 12 | name: 'Rpc', 13 | encoder: opts => new Encoder(opts), 14 | decoder: opts => new Decoder(opts), 15 | }; 16 | 17 | const port = 12200; 18 | const FOO_REQUEST = { 19 | targetAppName: 'foo', 20 | args: [ 'peter' ], 21 | serverSignature: 'com.alipay.sofa.rpc.quickstart.HelloService:1.0', 22 | methodName: 'sayHello', 23 | methodArgSigs: [ 'java.lang.String' ], 24 | requestProps: null, 25 | }; 26 | const FOO_RESPONSE = { 27 | error: null, 28 | appResponse: 'hello, peter', 29 | responseProps: null, 30 | }; 31 | const FOO_HEARTBEAT = { clientUrl: 'xxx' }; 32 | 33 | describe('test/connection.test.js', () => { 34 | afterEach(() => { 35 | mm.restore(); 36 | }); 37 | 38 | let server; 39 | let serverConn; 40 | let clientConn; 41 | 42 | beforeEach(async () => { 43 | server = net.createServer(); 44 | server.listen(12200); 45 | const connectionEvent = awaitEvent(server, 'connection'); 46 | const socket = net.createConnection({ port }); 47 | await awaitEvent(socket, 'connect'); 48 | clientConn = new Connection({ 49 | logger: console, 50 | socket, 51 | protocol, 52 | }); 53 | serverConn = new Connection({ 54 | logger: console, 55 | socket: await connectionEvent, 56 | protocol, 57 | }); 58 | }); 59 | 60 | afterEach(async () => { 61 | server.close(); 62 | }); 63 | 64 | describe('request', () => { 65 | 66 | afterEach(async () => { 67 | await clientConn.close(); 68 | await serverConn.await('close'); 69 | }); 70 | 71 | describe('request success', () => { 72 | it('should get response', async () => { 73 | const requestEvent = serverConn.await('request'); 74 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 75 | const resPromise = clientConn.writeRequest(req); 76 | const serverReceivedReq = await requestEvent; 77 | const res = Object.assign({}, FOO_RESPONSE); 78 | await serverConn.writeResponse(serverReceivedReq, res); 79 | const clientReceivedRes = await resPromise; 80 | assert.deepStrictEqual(serverReceivedReq.data, FOO_REQUEST); 81 | assert.deepStrictEqual(clientReceivedRes.data, FOO_RESPONSE); 82 | }); 83 | }); 84 | 85 | describe('heartbeat success', () => { 86 | it('should get heartbeatAck', async () => { 87 | const requestEvent = serverConn.await('heartbeat'); 88 | const hb = Object.assign({ timeout: 50 }, FOO_HEARTBEAT); 89 | const resPromise = clientConn.writeHeartbeat(hb); 90 | const serverReceivedHeartbeat = await requestEvent; 91 | await serverConn.writeHeartbeatAck(serverReceivedHeartbeat); 92 | const clientReceivedHeartbeatAck = await resPromise; 93 | assert(serverReceivedHeartbeat.packetType === 'heartbeat'); 94 | assert(clientReceivedHeartbeatAck.packetType === 'heartbeat_ack'); 95 | }); 96 | }); 97 | 98 | describe('encode timeout', () => { 99 | beforeEach(() => { 100 | mm(clientConn, '_writeRequest', () => { 101 | return new Promise(() => {}); 102 | }); 103 | }); 104 | 105 | it('should throw timeout error', async () => { 106 | const req = Object.assign({ timeout: 1 }, FOO_REQUEST); 107 | let error; 108 | try { 109 | await clientConn.writeRequest(req); 110 | } catch (e) { 111 | error = e; 112 | } finally { 113 | assert(error); 114 | assert(error.name === 'RpcResponseTimeoutError'); 115 | assert(/no response in \d+ms/.test(error.message)); 116 | } 117 | }); 118 | }); 119 | 120 | describe('heartbeat encode timeout', () => { 121 | beforeEach(() => { 122 | mm(clientConn, '_writeHeartbeat', () => { 123 | return new Promise(() => {}); 124 | }); 125 | }); 126 | 127 | it('should throw timeout error', async () => { 128 | const hb = Object.assign({ timeout: 1 }, FOO_HEARTBEAT); 129 | let error; 130 | try { 131 | await clientConn.writeHeartbeat(hb); 132 | } catch (e) { 133 | error = e; 134 | } finally { 135 | assert(error); 136 | assert(error.name === 'RpcResponseTimeoutError'); 137 | assert(/no response in \d+ms/.test(error.message)); 138 | } 139 | }); 140 | }); 141 | 142 | describe('response timeout', () => { 143 | it('should throw timeout error', async () => { 144 | const req = Object.assign({ timeout: 1 }, FOO_REQUEST); 145 | let error; 146 | try { 147 | await clientConn.writeRequest(req); 148 | } catch (e) { 149 | error = e; 150 | } finally { 151 | assert(error); 152 | assert(error.name === 'RpcResponseTimeoutError'); 153 | assert(/no response in \d+ms/.test(error.message)); 154 | } 155 | }); 156 | }); 157 | 158 | describe('heartbeatAck timeout', () => { 159 | it('should throw timeout error', async () => { 160 | const hb = Object.assign({ timeout: 1 }, FOO_HEARTBEAT); 161 | let error; 162 | try { 163 | await clientConn.writeHeartbeat(hb); 164 | } catch (e) { 165 | error = e; 166 | } finally { 167 | assert(error); 168 | assert(error.name === 'RpcResponseTimeoutError'); 169 | assert(/no response in \d+ms/.test(error.message)); 170 | } 171 | }); 172 | }); 173 | 174 | describe('request encode failed', () => { 175 | beforeEach(() => { 176 | mm(Encoder.prototype, 'writeRequest', (id, req, err) => { 177 | return err(new Error('mock error')); 178 | }); 179 | }); 180 | 181 | it('should throw encode error', async () => { 182 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 183 | let error; 184 | try { 185 | await clientConn.writeRequest(req); 186 | } catch (e) { 187 | error = e; 188 | } finally { 189 | assert(error); 190 | assert(error.name === 'RpcRequestEncodeError'); 191 | assert(/mock error/.test(error.message)); 192 | } 193 | }); 194 | }); 195 | 196 | describe('heartbeat encode failed', () => { 197 | beforeEach(() => { 198 | mm(Encoder.prototype, 'writeHeartbeat', (id, hb, err) => { 199 | return err(new Error('mock error')); 200 | }); 201 | }); 202 | 203 | it('should throw encode error', async () => { 204 | const hb = Object.assign({ timeout: 50 }, FOO_HEARTBEAT); 205 | let error; 206 | try { 207 | await clientConn.writeHeartbeat(hb); 208 | } catch (e) { 209 | error = e; 210 | } finally { 211 | assert(error); 212 | assert(error.name === 'RpcRequestEncodeError'); 213 | assert(/mock error/.test(error.message)); 214 | } 215 | }); 216 | }); 217 | 218 | describe('response encode failed', () => { 219 | beforeEach(() => { 220 | mm(Encoder.prototype, 'writeResponse', (req, res, callback) => { 221 | return callback(new Error('mock error')); 222 | }); 223 | }); 224 | 225 | it('should throw encode error', async () => { 226 | const requestEvent = serverConn.await('request'); 227 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 228 | const resPromise = clientConn.writeRequest(req); 229 | const serverReceivedReq = await requestEvent; 230 | const res = Object.assign({}, FOO_RESPONSE); 231 | let error; 232 | try { 233 | await serverConn.writeResponse(serverReceivedReq, res); 234 | } catch (e) { 235 | error = e; 236 | } finally { 237 | assert(error); 238 | assert(error.name === 'RpcResponseEncodeError'); 239 | assert(/mock error/.test(error.message)); 240 | } 241 | try { 242 | await resPromise; 243 | } catch (e) { 244 | error = e; 245 | } finally { 246 | assert(error); 247 | assert(error.name === 'RpcResponseTimeoutError'); 248 | assert(/no response in \d+ms/.test(error.message)); 249 | } 250 | }); 251 | }); 252 | 253 | describe('heartbeatAck encode failed', () => { 254 | beforeEach(() => { 255 | mm(Encoder.prototype, 'writeHeartbeatAck', (hb, callback) => { 256 | return callback(new Error('mock error')); 257 | }); 258 | }); 259 | 260 | it('should throw encode error', async () => { 261 | const heartbeatEvent = serverConn.await('heartbeat'); 262 | const hb = Object.assign({ timeout: 50 }, FOO_HEARTBEAT); 263 | const resPromise = clientConn.writeHeartbeat(hb); 264 | const serverReceivedHeartbeat = await heartbeatEvent; 265 | const res = serverReceivedHeartbeat; 266 | let error; 267 | try { 268 | await serverConn.writeHeartbeatAck(res); 269 | } catch (e) { 270 | error = e; 271 | } finally { 272 | assert(error); 273 | assert(error.name === 'RpcResponseEncodeError'); 274 | assert(/mock error/.test(error.message)); 275 | } 276 | try { 277 | await resPromise; 278 | } catch (e) { 279 | error = e; 280 | } finally { 281 | assert(error); 282 | assert(error.name === 'RpcResponseTimeoutError'); 283 | assert(/no response in \d+ms/.test(error.message)); 284 | } 285 | }); 286 | }); 287 | }); 288 | 289 | describe('oneway', () => { 290 | afterEach(async () => { 291 | await clientConn.close(); 292 | await serverConn.await('close'); 293 | }); 294 | 295 | describe('oneway encode failed', () => { 296 | beforeEach(() => { 297 | mm(Encoder.prototype, 'writeRequest', (id, req, err) => { 298 | return err(new Error('mock error')); 299 | }); 300 | }); 301 | 302 | it('should throw encode error', async () => { 303 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 304 | const errorEvent = clientConn.await('error'); 305 | clientConn.oneway(req); 306 | let error; 307 | try { 308 | await errorEvent; 309 | } catch (e) { 310 | error = e; 311 | } 312 | assert(error); 313 | assert(error.name === 'RpcOneWayEncodeError'); 314 | assert(/mock error/.test(error.message)); 315 | }); 316 | }); 317 | 318 | it('should work', async () => { 319 | const requestEvent = serverConn.await('request'); 320 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 321 | clientConn.oneway(req); 322 | const serverReceivedReq = await requestEvent; 323 | assert.deepStrictEqual(serverReceivedReq.data, FOO_REQUEST); 324 | }); 325 | }); 326 | 327 | describe('decode error', () => { 328 | 329 | describe('request decode failed', () => { 330 | beforeEach(() => { 331 | mm(serverConn._decoder, '_decode', () => { 332 | throw new Error('mock error'); 333 | }); 334 | }); 335 | 336 | it('should throw encode error', async () => { 337 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 338 | let error; 339 | let clientError; 340 | let serverError; 341 | let clientClosed; 342 | let serverClosed; 343 | clientConn.on('error', e => { 344 | clientError = e; 345 | }); 346 | serverConn.on('error', e => { 347 | serverError = e; 348 | }); 349 | clientConn.on('close', () => { 350 | clientClosed = true; 351 | }); 352 | serverConn.on('close', () => { 353 | serverClosed = true; 354 | }); 355 | try { 356 | await clientConn.writeRequest(req); 357 | } catch (e) { 358 | error = e; 359 | } finally { 360 | assert(error); 361 | assert(error.name === 'RpcSocketCloseError'); 362 | assert(/The socket was closed/.test(error.message)); 363 | } 364 | assert(clientConn._closed === true); 365 | assert(serverConn._closed === true); 366 | assert(clientConn.socket.destroyed === true); 367 | assert(serverConn.socket.destroyed === true); 368 | assert(clientConn._encoder.destroyed === true); 369 | assert(clientConn._decoder.destroyed === true); 370 | assert(serverConn._encoder.destroyed === true); 371 | assert(serverConn._decoder.destroyed === true); 372 | assert(!clientError); 373 | assert(serverError); 374 | assert(/(mock error|premature close)/.test(serverError.message)); 375 | assert(clientClosed === true); 376 | assert(serverClosed === true); 377 | }); 378 | }); 379 | 380 | describe('response decode failed', () => { 381 | beforeEach(() => { 382 | mm(clientConn._decoder, '_decode', () => { 383 | throw new Error('mock error'); 384 | }); 385 | }); 386 | 387 | it('should throw encode error', async () => { 388 | const requestEvent = serverConn.await('request'); 389 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 390 | let error; 391 | let clientError; 392 | let serverError; 393 | let clientClosed; 394 | let serverClosed; 395 | clientConn.on('error', e => { 396 | clientError = e; 397 | }); 398 | serverConn.on('error', e => { 399 | serverError = e; 400 | }); 401 | clientConn.on('close', () => { 402 | clientClosed = true; 403 | }); 404 | serverConn.on('close', () => { 405 | serverClosed = true; 406 | }); 407 | try { 408 | const resPromise = clientConn.writeRequest(req); 409 | const serverReceivedReq = await requestEvent; 410 | const res = Object.assign({}, FOO_RESPONSE); 411 | await serverConn.writeResponse(serverReceivedReq, res); 412 | await resPromise; 413 | } catch (e) { 414 | error = e; 415 | } finally { 416 | assert(error); 417 | assert(/(mock error|premature close)/.test(error.message)); 418 | } 419 | await serverConn.await('close'); 420 | assert(clientConn._closed === true); 421 | assert(serverConn._closed === true); 422 | assert(clientConn.socket.destroyed === true); 423 | assert(serverConn.socket.destroyed === true); 424 | assert(clientConn._encoder.destroyed === true); 425 | assert(clientConn._decoder.destroyed === true); 426 | assert(serverConn._encoder.destroyed === true); 427 | assert(serverConn._decoder.destroyed === true); 428 | assert(clientError); 429 | assert(!serverError); 430 | assert(/(mock error|premature close)/.test(clientError.message)); 431 | assert(clientClosed === true); 432 | assert(serverClosed === true); 433 | }); 434 | }); 435 | }); 436 | 437 | describe('close', () => { 438 | let clientError; 439 | let serverError; 440 | let clientClosed; 441 | let serverClosed; 442 | 443 | beforeEach(() => { 444 | clientError = null; 445 | serverError = null; 446 | clientClosed = false; 447 | serverClosed = false; 448 | clientConn.on('error', e => { 449 | clientError = e; 450 | }); 451 | serverConn.on('error', e => { 452 | serverError = e; 453 | }); 454 | clientConn.on('close', () => { 455 | clientClosed = true; 456 | }); 457 | serverConn.on('close', () => { 458 | serverClosed = true; 459 | }); 460 | }); 461 | 462 | describe('have pending request', () => { 463 | it('should request done', async () => { 464 | const requestEvent = serverConn.await('request'); 465 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 466 | const resPromise = clientConn.writeRequest(req); 467 | 468 | const closePromise = clientConn.close(); 469 | 470 | const serverReceivedReq = await requestEvent; 471 | const res = Object.assign({}, FOO_RESPONSE); 472 | await serverConn.writeResponse(serverReceivedReq, res); 473 | const clientReceivedRes = await resPromise; 474 | 475 | await Promise.all([ 476 | closePromise, 477 | serverConn.await('close'), 478 | ]); 479 | 480 | assert(clientConn._closed === true); 481 | assert(serverConn._closed === true); 482 | assert(clientConn.socket.destroyed === true); 483 | assert(serverConn.socket.destroyed === true); 484 | assert(clientConn._encoder.destroyed === true); 485 | assert(clientConn._decoder.destroyed === true); 486 | assert(serverConn._encoder.destroyed === true); 487 | assert(serverConn._decoder.destroyed === true); 488 | assert(!clientError); 489 | assert(!serverError); 490 | assert(clientClosed === true); 491 | assert(serverClosed === true); 492 | assert.deepStrictEqual(clientReceivedRes.data, FOO_RESPONSE); 493 | }); 494 | }); 495 | 496 | describe('close with error', () => { 497 | it('should emit error', async () => { 498 | const clientClosePromise = clientConn.await('close'); 499 | await clientConn.close(new Error('mock error')); 500 | await Promise.all([ 501 | clientClosePromise, 502 | serverConn.await('close'), 503 | ]); 504 | 505 | assert(clientConn._closed === true); 506 | assert(serverConn._closed === true); 507 | assert(clientConn.socket.destroyed === true); 508 | assert(serverConn.socket.destroyed === true); 509 | assert(clientConn._encoder.destroyed === true); 510 | assert(clientConn._decoder.destroyed === true); 511 | assert(serverConn._encoder.destroyed === true); 512 | assert(serverConn._decoder.destroyed === true); 513 | assert(clientError); 514 | assert(/mock error/.test(clientError.message)); 515 | assert(!serverError); 516 | assert(clientClosed === true); 517 | assert(serverClosed === true); 518 | }); 519 | }); 520 | 521 | describe('client server close simultaneously', () => { 522 | it('should clean resource, request', async () => { 523 | const req = Object.assign({ timeout: 1000 }, FOO_REQUEST); 524 | 525 | let writeError; 526 | clientConn.writeRequest(req).catch(err => { 527 | writeError = err; 528 | }); 529 | 530 | await serverConn.await('request'); 531 | 532 | await Promise.all([ 533 | serverConn.forceClose(), 534 | clientConn.close(), 535 | ]); 536 | 537 | 538 | server.close(); 539 | await awaitEvent(server, 'close'); 540 | 541 | assert(writeError); 542 | assert(writeError.name === 'RpcSocketCloseError'); 543 | 544 | assert(clientConn._closed === true); 545 | assert(serverConn._closed === true); 546 | assert(clientConn.socket.destroyed === true); 547 | assert(serverConn.socket.destroyed === true); 548 | assert(clientConn._encoder.destroyed === true); 549 | assert(clientConn._decoder.destroyed === true); 550 | assert(serverConn._encoder.destroyed === true); 551 | assert(serverConn._decoder.destroyed === true); 552 | assert(!clientError); 553 | assert(!serverError); 554 | assert(clientClosed === true); 555 | assert(serverClosed === true); 556 | }); 557 | }); 558 | }); 559 | 560 | describe('force close', () => { 561 | describe('with out error', () => { 562 | it('should clean request', async () => { 563 | let responseError; 564 | let requestError; 565 | let clientError; 566 | let serverError; 567 | let clientClosed; 568 | let serverClosed; 569 | clientConn.on('error', e => { 570 | clientError = e; 571 | }); 572 | serverConn.on('error', e => { 573 | serverError = e; 574 | }); 575 | clientConn.on('close', () => { 576 | clientClosed = true; 577 | }); 578 | serverConn.on('close', () => { 579 | serverClosed = true; 580 | }); 581 | 582 | const requestEvent = serverConn.await('request'); 583 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 584 | clientConn.writeRequest(req).catch(e => { 585 | requestError = e; 586 | }); 587 | 588 | const serverReceivedReq = await requestEvent; 589 | const serverClosePromise = serverConn.await('close'); 590 | await clientConn.forceClose(); 591 | await serverClosePromise; 592 | 593 | const res = Object.assign({}, FOO_RESPONSE); 594 | try { 595 | await serverConn.writeResponse(serverReceivedReq, res); 596 | } catch (e) { 597 | responseError = e; 598 | } 599 | 600 | assert(clientConn._closed === true); 601 | assert(serverConn._closed === true); 602 | assert(clientConn.socket.destroyed === true); 603 | assert(serverConn.socket.destroyed === true); 604 | assert(clientConn._encoder.destroyed === true); 605 | assert(clientConn._decoder.destroyed === true); 606 | assert(serverConn._encoder.destroyed === true); 607 | assert(serverConn._decoder.destroyed === true); 608 | assert(!clientError); 609 | assert(!serverError); 610 | assert(clientClosed === true); 611 | assert(serverClosed === true); 612 | assert(requestError); 613 | assert(/The socket was closed/.test(requestError.message)); 614 | assert(requestError.name === 'RpcSocketCloseError'); 615 | assert(responseError); 616 | assert(responseError.name === 'RpcResponseEncodeError'); 617 | assert(/write after/.test(responseError.message)); 618 | }); 619 | }); 620 | 621 | describe('with error', () => { 622 | it('should clean request', async () => { 623 | let responseError; 624 | let requestError; 625 | let clientError; 626 | let serverError; 627 | let clientClosed; 628 | let serverClosed; 629 | clientConn.on('error', e => { 630 | clientError = e; 631 | }); 632 | serverConn.on('error', e => { 633 | serverError = e; 634 | }); 635 | clientConn.on('close', () => { 636 | clientClosed = true; 637 | }); 638 | serverConn.on('close', () => { 639 | serverClosed = true; 640 | }); 641 | 642 | const requestEvent = serverConn.await('request'); 643 | const req = Object.assign({ timeout: 50 }, FOO_REQUEST); 644 | clientConn.writeRequest(req).catch(e => { 645 | requestError = e; 646 | }); 647 | 648 | const serverReceivedReq = await requestEvent; 649 | const serverClosePromise = serverConn.await('close'); 650 | await clientConn.forceClose(new Error('mock error')); 651 | await serverClosePromise; 652 | const res = Object.assign({}, FOO_RESPONSE); 653 | try { 654 | await serverConn.writeResponse(serverReceivedReq, res); 655 | } catch (e) { 656 | responseError = e; 657 | } 658 | 659 | assert(clientConn._closed === true); 660 | assert(serverConn._closed === true); 661 | assert(clientConn.socket.destroyed === true); 662 | assert(serverConn.socket.destroyed === true); 663 | assert(clientConn._encoder.destroyed === true); 664 | assert(clientConn._decoder.destroyed === true); 665 | assert(serverConn._encoder.destroyed === true); 666 | assert(serverConn._decoder.destroyed === true); 667 | assert(clientClosed === true); 668 | assert(serverClosed === true); 669 | assert(clientError); 670 | assert(clientError.name === 'RpcSocketError'); 671 | assert(/mock error/.test(clientError.message)); 672 | assert(requestError); 673 | assert(requestError.name === 'RpcSocketError'); 674 | assert(/mock error/.test(requestError.message)); 675 | assert(!serverError); 676 | assert(responseError); 677 | assert(responseError.name === 'RpcResponseEncodeError'); 678 | assert(/write after/.test(responseError.message)); 679 | }); 680 | }); 681 | }); 682 | }); 683 | --------------------------------------------------------------------------------