From 9e665708f3f134cfea3edcd5922737bb03022858 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Wed, 6 Jun 2018 20:02:43 +0800 Subject: [PATCH 01/16] convert the project from JavaScript to TypeScript --- src/{index.spec.js => index.spec.ts} | 0 src/{index.js => index.ts} | 0 src/typings.d.ts | 6 ++ tsconfig.json | 44 ++++++++++ tslint.json | 115 +++++++++++++++++++++++++++ 5 files changed, 165 insertions(+) rename src/{index.spec.js => index.spec.ts} (100%) rename src/{index.js => index.ts} (100%) create mode 100644 src/typings.d.ts create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/src/index.spec.js b/src/index.spec.ts similarity index 100% rename from src/index.spec.js rename to src/index.spec.ts diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 0000000..7edf0f2 --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1,6 @@ +declare const describe: any +declare const it: any +declare const expect: any +declare const beforeAll: any +declare const afterEach: any +declare const jest: any diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e476add --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "es5" + , "module": "commonjs" + , "outDir": "dist" + , "strict": true + + , "traceResolution": false + , "noLib": false + , "esModuleInterop": true + + , "experimentalDecorators": true + , "emitDecoratorMetadata": true + , "declaration": true + , "sourceMap": true + , "noEmitOnError": true + + , "noUnusedLocals": true + , "noImplicitReturns": true + , "noFallthroughCasesInSwitch": true + , "strictNullChecks": true + + , "noImplicitAny": true + , "noUnusedParameters": true + , "noImplicitThis": true + + , "skipLibCheck": true + , "lib": [ + "esnext" + ] + } + , "exclude": [ + "node_modules/" + , "dist/" + , "tests/fixtures/" + ] + , "include": [ + "bin/*.ts" + , "examples/**/*.ts" + , "scripts/**/*.ts" + , "src/**/*.ts" + , "tests/**/*.ts" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..f4416ee --- /dev/null +++ b/tslint.json @@ -0,0 +1,115 @@ +{ + "rulesDirectory": [ + ], + "rules": { + "align": [ + true, + "parameters", + // "arguments", + "statements" + ], + "jsdoc-require": [ + false + ], + "ban": false, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": false, + "eofline": true, + "forin": false, + "indent": [ + true, + "spaces" + ], + "interface-name": false, + "jsdoc-format": true, + "label-position": true, + "max-line-length": [ + true, + 180 + ], + "callable-types": true, + "import-blacklist": [true, "rxjs"], + "interface-over-type-literal": true, + "no-empty-interface": true, + "no-string-throw": true, + "prefer-const": true, + "unified-signatures": false, + "no-inferrable-types": [true, "ignore-params"], + "member-access": false, + "member-ordering": [false], + "no-any": false, + "no-arg": true, + "no-bitwise": true, + "no-conditional-assignment": true, + "no-consecutive-blank-lines": true, + "no-console": [false], + "no-construct": false, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": true, + "no-eval": true, + "no-internal-module": true, + "no-require-imports": false, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-unused-variable": [true], + "no-use-before-declare": true, + "no-var-keyword": true, + "no-var-requires": false, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-whitespace" + ], + "quotemark": [ + true, + "single", + "avoid-escape" + ], + "radix": false, + "semicolon": [true, "never"], + "switch-default": false, + "trailing-comma": [ + false, + { + "multiline": "always", + "singleline": "never", + "esSpecCompliant": true + } + ], + "triple-equals": [true], + "typedef": [false], + "typedef-whitespace": [ + false, + { + "call-signature": "space", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": [ + true, + "check-format", + "allow-leading-underscore", + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} From 984bef9b079f771e8f8eee7c20c561352f66b215 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Wed, 6 Jun 2018 20:03:22 +0800 Subject: [PATCH 02/16] convert the project from JavaScript to TypeScript --- package.json | 30 +++++-- src/index.spec.ts | 41 ++++++--- src/index.ts | 223 ++++++++++++++++++++++++++++++++-------------- 3 files changed, 206 insertions(+), 88 deletions(-) diff --git a/package.json b/package.json index b9b3690..41052d8 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ }, "dependencies": { "@babel/runtime": "^7.0.0-beta.39", + "@types/cuid": "^1.3.0", + "cuid": "^2.1.1", "json-rpc-protocol": "^0.11.3", "lodash": "^4.17.4" }, @@ -39,6 +41,8 @@ "@babel/core": "7.0.0-beta.39", "@babel/plugin-transform-runtime": "7.0.0-beta.39", "@babel/preset-env": "7.0.0-beta.39", + "@types/lodash": "^4.14.109", + "@types/node": "^10.3.1", "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^8.0.3", "babel-jest": "^22.1.0", @@ -52,18 +56,25 @@ "eslint-plugin-standard": "^3.0.1", "husky": "^0.14.3", "jest": "^22.0.4", - "rimraf": "^2.6.2" + "rimraf": "^2.6.2", + "ts-jest": "^22.4.6", + "tslint": "^5.10.0", + "typescript": "^2.9.1" }, "scripts": { - "build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/", + "build:babel": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/", + "build": "cross-env NODE_ENV=production tsc", "clean": "rimraf dist/", "commitmsg": "npm test", - "dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/", + "dev:babel": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/", + "dev": "cross-env NODE_ENV=development tsc --watch", "dev-test": "jest --bail --watch", "prebuild": "yarn run clean", "predev": "npm run prebuild", "prepublishOnly": "npm run build", - "pretest": "eslint --ignore-path .gitignore --fix .", + "pretest": "npm run lint:ts", + "lint:es": "eslint --ignore-path .gitignore --fix .", + "lint:ts": "tslint --project tsconfig.json && tsc --noEmit", "test": "jest" }, "jest": { @@ -72,6 +83,15 @@ "roots": [ "/src" ], - "testRegex": "\\.spec\\.js$" + "transform": { + "^.+\\.ts?$": "ts-jest" + }, + "testRegex": "\\.spec\\.ts$", + "moduleFileExtensions": [ + "ts", + "js", + "json", + "node" + ] } } diff --git a/src/index.spec.ts b/src/index.spec.ts index fa0d95d..cfa34a6 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,14 +1,24 @@ /* eslint-env jest */ -import { format } from 'json-rpc-protocol' - -import Peer, {MethodNotFound} from './' +import { + format, + JsonRpcPayload, + JsonRpcPayloadRequest, + JsonRpcParamsSchemaByPositional, +} from 'json-rpc-protocol' + +import { + Peer, + MethodNotFound, +} from './' // =================================================================== describe('Peer', () => { - let server, client - const messages = [] + let server: Peer + let client: Peer + + const messages: JsonRpcPayload[] = [] beforeAll(() => { server = new Peer(message => { @@ -18,22 +28,25 @@ describe('Peer', () => { return } - const {method} = message + const {method} = message as JsonRpcPayloadRequest if (method === 'circular value') { - const a = [] + const a: any[] = [] a.push(a) return a } + const requestPayload = message as JsonRpcPayloadRequest + const params = requestPayload.params as JsonRpcParamsSchemaByPositional + if (method === 'identity') { - return message.params[0] + return params[0] } if (method === 'wait') { return new Promise(resolve => { - setTimeout(resolve, message.params[0]) + setTimeout(resolve, params[0]) }) } @@ -55,7 +68,7 @@ describe('Peer', () => { client.notify('foo') expect(messages.length).toBe(1) - expect(messages[0].method).toBe('foo') + expect((messages[0] as JsonRpcPayloadRequest).method).toBe('foo') expect(messages[0].type).toBe('notification') }) @@ -63,11 +76,11 @@ describe('Peer', () => { const result = client.request('identity', [42]) expect(messages.length).toBe(1) - expect(messages[0].method).toBe('identity') + expect((messages[0] as JsonRpcPayloadRequest).method).toBe('identity') expect(messages[0].type).toBe('request') - return result.then(result => { - expect(result).toBe(42) + return result.then(ret => { + expect(ret).toBe(42) }) }) @@ -95,7 +108,7 @@ describe('Peer', () => { }) describe('#write()', function () { - it('emits an error event if the response message cannot be formatted', function (done) { + it('emits an error event if the response message cannot be formatted', function (done: Function) { server.on('error', () => done()) client.request('circular value') diff --git a/src/index.ts b/src/index.ts index da5c76c..4d5bc6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,23 @@ import { EventEmitter } from 'events' -import { - forEach, - isArray, - map, -} from 'lodash' +// import { +// forEach, +// isArray, +// map, +// } from 'lodash' +import cuid from 'cuid' + import { format, JsonRpcError, MethodNotFound, parse, -} from 'json-rpc-protocol' + + JsonRpcPayload, + JsonRpcPayloadRequest, + JsonRpcPayloadError, + JsonRpcPayloadResponse, + JsonRpcParamsSchema, +} from 'json-rpc-protocol' // =================================================================== @@ -18,13 +26,13 @@ export * from 'json-rpc-protocol' // =================================================================== -function makeAsync (fn) { - return function () { - return new Promise(resolve => resolve(fn.apply(this, arguments))) +function makeAsync (fn: Function): (...args: any[]) => Promise { + return function (this: any, ...args: any[]) { + return new Promise(resolve => resolve(fn.apply(this, args))) } } -const parseMessage = message => { +const parseMessage = (message: string | Object) => { try { return parse(message) } catch (error) { @@ -36,22 +44,33 @@ const parseMessage = message => { // // - ignores notifications // - throw MethodNotFound for all requests -function defaultOnMessage (message) { +function defaultOnMessage (message: JsonRpcPayload) { if (message.type === 'request') { - throw new MethodNotFound(message.method) + throw new MethodNotFound((message as JsonRpcPayloadRequest).method) } } -function noop () {} +function noop () { + // noop +} // Starts the autoincrement id with the JavaScript minimal safe integer to have // more room before running out of integers (it's very far fetched but a very // long running process with a LOT of messages could run out). -let nextRequestId = -9007199254740991 +// let nextRequestId = -9007199254740991 +// use cuid() instead // =================================================================== -export default class Peer extends EventEmitter { +export class Peer extends EventEmitter { + private _handle: (payload: JsonRpcPayload, data: any) => Promise + private _deferreds: { + [idx: string]: { + resolve: Function, + reject: Function, + } + } + constructor (onMessage = defaultOnMessage) { super() @@ -59,74 +78,100 @@ export default class Peer extends EventEmitter { this._deferreds = Object.create(null) } - _getDeferred (id) { + _getDeferred (id: number | string) { const deferred = this._deferreds[id] delete this._deferreds[id] return deferred } - async exec (message, data) { - message = parseMessage(message) - - if (isArray(message)) { - const results = [] + async exec ( + message: string | Object, + data?: any, + ): Promise { + const messagePayload = parseMessage(message) + if (Array.isArray(messagePayload)) { // Only returns non empty results. - await Promise.all(map(message, message => { - return this.exec(message, data).then(result => { - if (result !== undefined) { - results.push(result) - } - }) - })) + const results = ( + await Promise.all( + messagePayload.map( + payload => this.exec(payload, data) + ) + ) + ).filter(result => result !== undefined) as JsonRpcPayload[] return results } - const {type} = message + const {type} = messagePayload if (type === 'error') { - const {id} = message + const {id} = messagePayload as JsonRpcPayloadError // Some errors do not have an identifier, simply discard them. - if (id === null) { + if (id === undefined || id === null) { return } - const {error} = message + const {error} = messagePayload as JsonRpcPayloadError this._getDeferred(id).reject( // TODO: it would be great if we could return an error with of // a more specific type (and custom types with registration). new JsonRpcError(error.message, error.code, error.data) ) + return + } else if (type === 'response') { - this._getDeferred(message.id).resolve(message.result) - } else if (type === 'notification') { - this._handle(message, data).catch(noop) - } else { - return this._handle(message, data).then( - (result) => format.response(message.id, result === undefined ? null : result), - (error) => format.error( - message.id, + const responsePayload = messagePayload as JsonRpcPayloadResponse + this._getDeferred( + responsePayload.id + ).resolve(responsePayload.result) + return + } else if (type === 'notification') { + this._handle(messagePayload, data).catch(noop) + return + + } else { // type === 'request' + const requestPayload = messagePayload as JsonRpcPayloadRequest + let result + try { + result = await this._handle(messagePayload, data) + } catch (error) { + return format.error( + requestPayload.id, // If the method name is not defined, default to the method passed // in the request. (error instanceof MethodNotFound && !error.data) - ? new MethodNotFound(message.method) + ? new MethodNotFound(requestPayload.method) : error ) - ) + } + return format.response(requestPayload.id, result === undefined ? null : result) + + // return this._handle(messagePayload, data).then( + // (result) => format.response(messagePayload.id, result === undefined ? null : result), + // (error) => format.error( + // messagePayload.id, + + // // If the method name is not defined, default to the method passed + // // in the request. + // (error instanceof MethodNotFound && !error.data) + // ? new MethodNotFound(messagePayload.method) + // : error + // ) + // ) } } // Fails all pending requests. - failPendingRequests (reason) { + failPendingRequests (reason?: string) { const {_deferreds: deferreds} = this - forEach(deferreds, ({reject}, id) => { - reject(reason) + for (const id in deferreds) { + deferreds[id].reject(reason) delete deferreds[id] - }) + } } /** @@ -134,13 +179,13 @@ export default class Peer extends EventEmitter { * * TODO: handle multi-requests. */ - request (method, params) { + request (method: string, params?: JsonRpcParamsSchema): Promise { return new Promise((resolve, reject) => { - const requestId = nextRequestId++ - - this.push(format.request(requestId, method, params)) + const requestId = cuid() this._deferreds[requestId] = {resolve, reject} + + this.push(format.request(requestId, method, params)) }) } @@ -149,47 +194,87 @@ export default class Peer extends EventEmitter { * * TODO: handle multi-notifications. */ - async notify (method, params) { + async notify (method: string, params?: JsonRpcParamsSchema) { this.push(format.notification(method, params)) } // minimal stream interface - pipe (writable) { + pipe(writable: T): T { + let clean: () => void + const listeners = { - data: data => writable.write(data), + data: (data: any) => { + if (writable instanceof Peer) { + writable.write(data) + } else { + // TypeScript bug? can not identify type at here, have to do a casting. + (writable as NodeJS.WritableStream).write(data) + } + }, end: () => { writable.end() clean() }, + } as { + [event: string]: any, } - const clean = () => forEach(listeners, (listener, event) => { - this.removeListener(event, listener) - }) - forEach(listeners, (listener, event) => { + clean = () => { + // forEach(listeners, (listener, event) => { + for (const event in listeners) { + const listener = listeners[event] + this.removeListener(event, listener) + } + } + + for (const event in listeners) { + const listener = listeners[event] this.on(event, listener) - }) + } + // forEach(listeners, (listener, event) => { + // this.on(event, listener) + // }) return writable } - push (data) { + push (data: any) { return data === null ? this.emit('end') : this.emit('data', data) } - write (message) { - this.exec(String(message)).then( - response => { - if (response !== undefined) { - this.push(response) - } - }, - error => { - this.emit('error', error) + async write (message: any) { + try { + const response = await this.exec(String(message)) + if (response !== undefined) { + this.push(response) } - ) + } catch (error) { + this.emit('error', error) + } + } + + // write(message: any): boolean { + // this.exec(String(message)).then( + // response => { + // if (response !== undefined) { + // this.push(response) + // } + // }, + // ).catch( + // error => this.emit('error', error) + // ) + // return true + // } + + end(): void { + // if (message) { + // this.write(message) + // } + // noop. just for pretend I'm a stream... } } + +export default Peer From 0e4442f18c5c54c807796875a5567656336b7c16 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Wed, 6 Jun 2018 22:03:20 +0800 Subject: [PATCH 03/16] add npm package typing entrience --- package.json | 3 ++- src/index.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 41052d8..47f3a9b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "email": "julien.fontanet@isonoe.net" }, "preferGlobal": false, - "main": "dist/", + "main": "dist/index.js", + "typings": "dist/index.d.ts", "bin": {}, "files": [ "dist/" diff --git a/src/index.ts b/src/index.ts index 4d5bc6c..357281c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -213,8 +213,8 @@ export class Peer extends EventEmitter { } }, end: () => { - writable.end() clean() + writable.end() }, } as { [event: string]: any, @@ -240,6 +240,7 @@ export class Peer extends EventEmitter { } push (data: any) { + // TODO: make sure data is a JsonRpcPayload, or convert it to be. return data === null ? this.emit('end') : this.emit('data', data) @@ -270,6 +271,7 @@ export class Peer extends EventEmitter { // } end(): void { + this.emit('end') // if (message) { // this.write(message) // } From a1d89b5aa9dfc935e972fac909c0567f722fe322 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Sat, 9 Jun 2018 00:02:58 +0800 Subject: [PATCH 04/16] better var name --- src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 357281c..a4183c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -239,16 +239,16 @@ export class Peer extends EventEmitter { return writable } - push (data: any) { - // TODO: make sure data is a JsonRpcPayload, or convert it to be. - return data === null + push (chunk: any, encoding?: string) { + // TODO: does convert the chunk to a JsonRpcPayload is better? + return chunk === null ? this.emit('end') - : this.emit('data', data) + : this.emit('data', chunk, encoding) } + async write (buffer: Buffer | string) { - async write (message: any) { try { - const response = await this.exec(String(message)) + const response = await this.exec(String(buffer)) if (response !== undefined) { this.push(response) } From e613ae1a8571605ce185009a12caf1172e92c027 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 21:39:40 +0800 Subject: [PATCH 05/16] Upgrade json-rpc-protocol to TypeScript version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 47f3a9b..10e7257 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@babel/runtime": "^7.0.0-beta.39", "@types/cuid": "^1.3.0", "cuid": "^2.1.1", - "json-rpc-protocol": "^0.11.3", + "json-rpc-protocol": "^0.12.0", "lodash": "^4.17.4" }, "devDependencies": { From e61af6d87d81d56b2011f4cc1c55c0a6ceb578cf Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 21:55:02 +0800 Subject: [PATCH 06/16] make tslint config clean --- tslint.json | 113 +--------------------------------------------------- 1 file changed, 2 insertions(+), 111 deletions(-) diff --git a/tslint.json b/tslint.json index f4416ee..b6293de 100644 --- a/tslint.json +++ b/tslint.json @@ -1,115 +1,6 @@ { - "rulesDirectory": [ - ], + "extends": ["tslint:recommended", "tslint-config-standard"], "rules": { - "align": [ - true, - "parameters", - // "arguments", - "statements" - ], - "jsdoc-require": [ - false - ], - "ban": false, - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "curly": false, - "eofline": true, - "forin": false, - "indent": [ - true, - "spaces" - ], - "interface-name": false, - "jsdoc-format": true, - "label-position": true, - "max-line-length": [ - true, - 180 - ], - "callable-types": true, - "import-blacklist": [true, "rxjs"], - "interface-over-type-literal": true, - "no-empty-interface": true, - "no-string-throw": true, - "prefer-const": true, - "unified-signatures": false, - "no-inferrable-types": [true, "ignore-params"], - "member-access": false, - "member-ordering": [false], - "no-any": false, - "no-arg": true, - "no-bitwise": true, - "no-conditional-assignment": true, - "no-consecutive-blank-lines": true, - "no-console": [false], - "no-construct": false, - "no-debugger": true, - "no-duplicate-variable": true, - "no-empty": true, - "no-eval": true, - "no-internal-module": true, - "no-require-imports": false, - "no-shadowed-variable": true, - "no-string-literal": false, - "no-switch-case-fall-through": true, - "no-trailing-whitespace": true, - "no-unused-expression": true, - "no-unused-variable": [true], - "no-use-before-declare": true, - "no-var-keyword": true, - "no-var-requires": false, - "object-literal-sort-keys": false, - "one-line": [ - true, - "check-open-brace", - "check-whitespace" - ], - "quotemark": [ - true, - "single", - "avoid-escape" - ], - "radix": false, - "semicolon": [true, "never"], - "switch-default": false, - "trailing-comma": [ - false, - { - "multiline": "always", - "singleline": "never", - "esSpecCompliant": true - } - ], - "triple-equals": [true], - "typedef": [false], - "typedef-whitespace": [ - false, - { - "call-signature": "space", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "variable-name": [ - true, - "check-format", - "allow-leading-underscore", - "ban-keywords" - ], - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ] + "interface-name": [true, "never-prefix"] } } From f7b4dc667ec44272de3d80f790c2d499e872b54b Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 21:55:23 +0800 Subject: [PATCH 07/16] lint --- src/index.ts | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index a4183c9..eb77da5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,23 +1,17 @@ -import { EventEmitter } from 'events' -// import { -// forEach, -// isArray, -// map, -// } from 'lodash' import cuid from 'cuid' +import { EventEmitter } from 'events' import { format, JsonRpcError, - MethodNotFound, - parse, - + JsonRpcParamsSchema, JsonRpcPayload, - JsonRpcPayloadRequest, JsonRpcPayloadError, + JsonRpcPayloadRequest, JsonRpcPayloadResponse, - JsonRpcParamsSchema, -} from 'json-rpc-protocol' + MethodNotFound, + parse +} from 'json-rpc-protocol' // =================================================================== @@ -26,13 +20,13 @@ export * from 'json-rpc-protocol' // =================================================================== -function makeAsync (fn: Function): (...args: any[]) => Promise { +function makeAsync (fn: (...args: any[]) => any): (...args: any[]) => Promise { return function (this: any, ...args: any[]) { - return new Promise(resolve => resolve(fn.apply(this, args))) + return new Promise(((resolve) => resolve(fn.apply(this, args))) } } -const parseMessage = (message: string | Object) => { +const parseMessage = (message: string | object) => { try { return parse(message) } catch (error) { @@ -66,8 +60,8 @@ export class Peer extends EventEmitter { private _handle: (payload: JsonRpcPayload, data: any) => Promise private _deferreds: { [idx: string]: { - resolve: Function, - reject: Function, + resolve: (...args: any[]) => any, + reject: (...args: any[]) => any } } @@ -78,12 +72,6 @@ export class Peer extends EventEmitter { this._deferreds = Object.create(null) } - _getDeferred (id: number | string) { - const deferred = this._deferreds[id] - delete this._deferreds[id] - return deferred - } - async exec ( message: string | Object, data?: any, @@ -277,6 +265,13 @@ export class Peer extends EventEmitter { // } // noop. just for pretend I'm a stream... } + + private _getDeferred (id: number | string) { + const deferred = this._deferreds[id] + delete this._deferreds[id] + return deferred + } + } export default Peer From 3f7fa720503cfbea2294e5621a55675ce8e009e0 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 21:55:41 +0800 Subject: [PATCH 08/16] temp package --- package.json | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 10e7257..8104180 100644 --- a/package.json +++ b/package.json @@ -31,50 +31,30 @@ "node": ">=4" }, "dependencies": { - "@babel/runtime": "^7.0.0-beta.39", - "@types/cuid": "^1.3.0", "cuid": "^2.1.1", - "json-rpc-protocol": "^0.12.0", - "lodash": "^4.17.4" + "json-rpc-protocol": "^0.12.0" }, "devDependencies": { - "@babel/cli": "7.0.0-beta.39", - "@babel/core": "7.0.0-beta.39", - "@babel/plugin-transform-runtime": "7.0.0-beta.39", - "@babel/preset-env": "7.0.0-beta.39", - "@types/lodash": "^4.14.109", + "@types/cuid": "^1.3.0", "@types/node": "^10.3.1", - "babel-core": "^7.0.0-bridge.0", - "babel-eslint": "^8.0.3", - "babel-jest": "^22.1.0", - "babel-plugin-lodash": "^3.3.2", "cross-env": "^5.1.1", - "eslint": "^4.13.1", - "eslint-config-standard": "^11.0.0-beta.0", - "eslint-plugin-import": "^2.8.0", - "eslint-plugin-node": "^6.0.0", - "eslint-plugin-promise": "^3.6.0", - "eslint-plugin-standard": "^3.0.1", "husky": "^0.14.3", "jest": "^22.0.4", "rimraf": "^2.6.2", "ts-jest": "^22.4.6", "tslint": "^5.10.0", + "tslint-config-standard": "^7.0.0", "typescript": "^2.9.1" }, "scripts": { - "build:babel": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/", "build": "cross-env NODE_ENV=production tsc", "clean": "rimraf dist/", - "commitmsg": "npm test", - "dev:babel": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/", "dev": "cross-env NODE_ENV=development tsc --watch", "dev-test": "jest --bail --watch", "prebuild": "yarn run clean", "predev": "npm run prebuild", "prepublishOnly": "npm run build", "pretest": "npm run lint:ts", - "lint:es": "eslint --ignore-path .gitignore --fix .", "lint:ts": "tslint --project tsconfig.json && tsc --noEmit", "test": "jest" }, @@ -90,9 +70,7 @@ "testRegex": "\\.spec\\.ts$", "moduleFileExtensions": [ "ts", - "js", - "json", - "node" + "js" ] } } From ab9d5dc8c8c9d7346b685f46b6d4ae5520179e53 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 22:51:05 +0800 Subject: [PATCH 09/16] make TypeScript passing lint & unit tests --- package.json | 3 +- src/index.spec.ts | 41 ++++++++------ src/index.ts | 136 +++++++++++++++++++++------------------------- 3 files changed, 86 insertions(+), 94 deletions(-) diff --git a/package.json b/package.json index 6217c43..27ff714 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,9 @@ "node": ">=4" }, "dependencies": { - "cuid": "^2.1.1", "json-rpc-protocol": "^0.12.0" }, "devDependencies": { - "@types/cuid": "^1.3.0", "@types/node": "^10.3.1", "cross-env": "^5.1.1", "husky": "^0.14.3", @@ -49,6 +47,7 @@ "scripts": { "build": "cross-env NODE_ENV=production tsc", "clean": "rimraf dist/", + "commitmsg": "npm test", "dev": "cross-env NODE_ENV=development tsc --watch", "dev-test": "jest --bail --watch", "prebuild": "yarn run clean", diff --git a/src/index.spec.ts b/src/index.spec.ts index cfa34a6..6455657 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -2,15 +2,16 @@ import { format, - JsonRpcPayload, - JsonRpcPayloadRequest, JsonRpcParamsSchemaByPositional, -} from 'json-rpc-protocol' + JsonRpcPayload, + JsonRpcPayloadRequest +} from 'json-rpc-protocol' import { - Peer, + AnyFunction, MethodNotFound, -} from './' + Peer +} from './' // =================================================================== @@ -21,14 +22,14 @@ describe('Peer', () => { const messages: JsonRpcPayload[] = [] beforeAll(() => { - server = new Peer(message => { + server = new Peer((message) => { messages.push(message) if (message.type === 'notification') { return } - const {method} = message as JsonRpcPayloadRequest + const { method } = message as JsonRpcPayloadRequest if (method === 'circular value') { const a: any[] = [] @@ -38,14 +39,14 @@ describe('Peer', () => { } const requestPayload = message as JsonRpcPayloadRequest - const params = requestPayload.params as JsonRpcParamsSchemaByPositional + const params = requestPayload.params as any as JsonRpcParamsSchemaByPositional if (method === 'identity') { return params[0] } if (method === 'wait') { - return new Promise(resolve => { + return new Promise((resolve) => { setTimeout(resolve, params[0]) }) } @@ -68,7 +69,7 @@ describe('Peer', () => { client.notify('foo') expect(messages.length).toBe(1) - expect((messages[0] as JsonRpcPayloadRequest).method).toBe('foo') + expect((messages[0] as any as JsonRpcPayloadRequest).method).toBe('foo') expect(messages[0].type).toBe('notification') }) @@ -76,10 +77,10 @@ describe('Peer', () => { const result = client.request('identity', [42]) expect(messages.length).toBe(1) - expect((messages[0] as JsonRpcPayloadRequest).method).toBe('identity') + expect((messages[0] as any as JsonRpcPayloadRequest).method).toBe('identity') expect(messages[0].type).toBe('request') - return result.then(ret => { + return result.then((ret) => { expect(ret).toBe(42) }) }) @@ -89,29 +90,32 @@ describe('Peer', () => { () => { expect('should have been rejected').toBeFalsy() }, - error => { + (error) => { expect(error.code).toBe(-32601) expect(error.data).toBe('foo') } ) }) - it('#request() in parallel', function () { + it('#request() in parallel', () => { const start = Date.now() return Promise.all([ client.request('wait', [25]), - client.request('wait', [25]), + client.request('wait', [25]) ]).then(() => { expect(Date.now() - start).toBeLessThan(40) }) }) - describe('#write()', function () { - it('emits an error event if the response message cannot be formatted', function (done: Function) { + describe('#write()', () => { + it('emits an error event if the response message cannot be formatted', (done: AnyFunction) => { server.on('error', () => done()) client.request('circular value') + .catch(() => { + // noop + }) }) }) @@ -121,6 +125,9 @@ describe('Peer', () => { const onMessage = jest.fn() const peer = new Peer(onMessage) peer.exec(format.notification('foo'), data) + .catch(() => { + // noop + }) expect(onMessage.mock.calls[0][1]).toBe(data) }) }) diff --git a/src/index.ts b/src/index.ts index a59dfff..7173361 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -import cuid from 'cuid' import { EventEmitter } from 'events' import { @@ -20,9 +19,15 @@ export * from 'json-rpc-protocol' // =================================================================== -function makeAsync (fn: (...args: any[]) => any): (...args: any[]) => Promise { +export type AnyFunction = (...args: any[]) => any + +function makeAsync (fn: AnyFunction): AnyFunction { return function (this: any, ...args: any[]) { - return new Promise(((resolve) => resolve(fn.apply(this, args))) + return new Promise( + (resolve) => resolve( + fn.apply(this, args) + ) + ) } } @@ -51,12 +56,13 @@ function noop () { // Starts the autoincrement id with the JavaScript minimal safe integer to have // more room before running out of integers (it's very far fetched but a very // long running process with a LOT of messages could run out). -// let nextRequestId = -9007199254740991 -// use cuid() instead +let nextRequestId = -9007199254740991 // =================================================================== -export class Peer extends EventEmitter { +export class Peer extends EventEmitter implements NodeJS.WritableStream { + public writable = true + private _handle: (payload: JsonRpcPayload, data: any) => Promise private _deferreds: { [idx: string]: { @@ -72,9 +78,9 @@ export class Peer extends EventEmitter { this._deferreds = Object.create(null) } - async exec ( - message: string | Object, - data?: any, + public async exec ( + message: string | object, + data?: any ): Promise { const messagePayload = parseMessage(message) @@ -83,42 +89,42 @@ export class Peer extends EventEmitter { const results = ( await Promise.all( messagePayload.map( - payload => this.exec(payload, data) + (payload) => this.exec(payload, data) ) ) - ).filter(result => result !== undefined) as JsonRpcPayload[] + ).filter((result) => result !== undefined) as JsonRpcPayload[] return results } - const {type} = messagePayload + const { type } = messagePayload if (type === 'error') { - const {id} = messagePayload as JsonRpcPayloadError + const { id } = messagePayload as JsonRpcPayloadError // Some errors do not have an identifier, simply discard them. if (id === undefined || id === null) { - return + return undefined } - const {error} = messagePayload as JsonRpcPayloadError + const { error } = messagePayload as JsonRpcPayloadError this._getDeferred(id).reject( // TODO: it would be great if we could return an error with of // a more specific type (and custom types with registration). new JsonRpcError(error.message, error.code, error.data) ) - return + return undefined } else if (type === 'response') { const responsePayload = messagePayload as JsonRpcPayloadResponse this._getDeferred( responsePayload.id ).resolve(responsePayload.result) - return + return undefined } else if (type === 'notification') { this._handle(messagePayload, data).catch(noop) - return + return undefined } else { // type === 'request' const requestPayload = messagePayload as JsonRpcPayloadRequest @@ -136,27 +142,15 @@ export class Peer extends EventEmitter { ) } return format.response(requestPayload.id, result === undefined ? null : result) - - // return this._handle(messagePayload, data).then( - // (result) => format.response(messagePayload.id, result === undefined ? null : result), - // (error) => format.error( - // messagePayload.id, - - // // If the method name is not defined, default to the method passed - // // in the request. - // (error instanceof MethodNotFound && !error.data) - // ? new MethodNotFound(messagePayload.method) - // : error - // ) - // ) } } // Fails all pending requests. - failPendingRequests (reason?: string) { - const {_deferreds: deferreds} = this + public failPendingRequests (reason?: string) { + const { _deferreds: deferreds } = this - for (const id in deferreds) { + // https://stackoverflow.com/a/45959874/1123955 + for (const id of Object.keys(deferreds)) { deferreds[id].reject(reason) delete deferreds[id] } @@ -167,11 +161,11 @@ export class Peer extends EventEmitter { * * TODO: handle multi-requests. */ - request (method: string, params?: JsonRpcParamsSchema): Promise { + public request (method: string, params?: JsonRpcParamsSchema): Promise { return new Promise((resolve, reject) => { - const requestId = cuid() + const requestId = nextRequestId++ - this._deferreds[requestId] = {resolve, reject} + this._deferreds[requestId] = { resolve, reject } this.push(format.request(requestId, method, params)) }) @@ -182,13 +176,13 @@ export class Peer extends EventEmitter { * * TODO: handle multi-notifications. */ - async notify (method: string, params?: JsonRpcParamsSchema) { + public notify (method: string, params?: JsonRpcParamsSchema) { this.push(format.notification(method, params)) } // minimal stream interface - pipe(writable: T): T { + public pipe (writable: T): T { let clean: () => void const listeners = { @@ -203,51 +197,51 @@ export class Peer extends EventEmitter { end: () => { clean() writable.end() - }, + } } as { - [event: string]: any, + [event: string]: any } clean = () => { // forEach(listeners, (listener, event) => { - for (const event in listeners) { + for (const event of Object.keys(listeners)) { const listener = listeners[event] this.removeListener(event, listener) } } - for (const event in listeners) { + for (const event of Object.keys(listeners)) { const listener = listeners[event] this.on(event, listener) } - // forEach(listeners, (listener, event) => { - // this.on(event, listener) - // }) return writable } - push (chunk: any, encoding?: string) { + public push (chunk: any, encoding?: string) { // TODO: does convert the chunk to a JsonRpcPayload is better? return chunk === null ? this.emit('end') : this.emit('data', chunk, encoding) } - write (message) { + public write (buffer: string | Buffer, cb?: AnyFunction): boolean + public write (str: string, encoding?: string, cb?: AnyFunction): boolean + + public write (...args: any[]): boolean { let cb - const n = arguments.length - if (n > 1 && typeof (cb = arguments[n - 1]) === 'function') { + const n = args.length + if (n > 1 && typeof (cb = args[n - 1]) === 'function') { process.nextTick(cb) } - this.exec(String(message)).then( - response => { + this.exec(String(args[0])).then( + (response) => { if (response !== undefined) { this.push(response) } }, - error => { + (error) => { this.emit('error', error) } ) @@ -256,31 +250,23 @@ export class Peer extends EventEmitter { return true } - // write(message: any): boolean { - // this.exec(String(message)).then( - // response => { - // if (response !== undefined) { - // this.push(response) - // } - // }, - // ).catch( - // error => this.emit('error', error) - // ) - // return true - // } - - end (data, encoding, cb) { - if (typeof data === 'function') { - process.nextTick(data) + public end (cb?: (...args: any[]) => any): void + public end (buffer: string | Buffer, cb?: AnyFunction): void + public end (str: string, encoding?: string, cb?: AnyFunction): void + + // end (data, encoding, cb) { + public end (...args: any[]): void { + if (typeof args[0] === 'function') { + process.nextTick(args[0]) } else { - if (typeof encoding === 'function') { - process.nextTick(encoding) - } else if (typeof cb === 'function') { - process.nextTick(cb) + if (typeof args[1] === 'function') { + process.nextTick(args[1]) + } else if (typeof args[2] === 'function') { + process.nextTick(args[2]) } - if (data !== undefined) { - this.write(data) + if (args[0] !== undefined) { + this.write(args[0]) } } } From da8148a125f1588cd2780f8784ff6a7f5211b335 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 22:56:36 +0800 Subject: [PATCH 10/16] clean tsconfig.json --- tsconfig.json | 59 +++++++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index e476add..5e1ca80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,44 +1,25 @@ { "compilerOptions": { - "target": "es5" - , "module": "commonjs" - , "outDir": "dist" - , "strict": true - - , "traceResolution": false - , "noLib": false - , "esModuleInterop": true - - , "experimentalDecorators": true - , "emitDecoratorMetadata": true - , "declaration": true - , "sourceMap": true - , "noEmitOnError": true - - , "noUnusedLocals": true - , "noImplicitReturns": true - , "noFallthroughCasesInSwitch": true - , "strictNullChecks": true - - , "noImplicitAny": true - , "noUnusedParameters": true - , "noImplicitThis": true - - , "skipLibCheck": true - , "lib": [ + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": true, + "module": "commonjs", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "./dist", + "rootDir": "./src", + "sourceMap": true, + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "lib": [ "esnext" - ] + ], + "target": "es5" } - , "exclude": [ - "node_modules/" - , "dist/" - , "tests/fixtures/" - ] - , "include": [ - "bin/*.ts" - , "examples/**/*.ts" - , "scripts/**/*.ts" - , "src/**/*.ts" - , "tests/**/*.ts" - ] } From f8255feaf691bf71dcfb536df850c3c3cc2826d6 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 22:59:50 +0800 Subject: [PATCH 11/16] ts-node need more time to do this test --- src/index.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 6455657..981f31c 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -101,10 +101,11 @@ describe('Peer', () => { const start = Date.now() return Promise.all([ - client.request('wait', [25]), - client.request('wait', [25]) + client.request('wait', [100]), + client.request('wait', [100]), + client.request('wait', [100]) ]).then(() => { - expect(Date.now() - start).toBeLessThan(40) + expect(Date.now() - start).toBeLessThan(200) }) }) From 02c57ddff933a070a3ad414fd924f70db3553e29 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 23:01:08 +0800 Subject: [PATCH 12/16] remove yarn change --- yarn.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 4b770d8..3c2bc05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4468,4 +4468,5 @@ yargs@~3.10.0: camelcase "^1.0.2" cliui "^2.1.0" decamelize "^1.0.0" - window-size "0.1.0" \ No newline at end of file + window-size "0.1.0" + From 6f792c48f295d3079f3870e3292087dd87619b72 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Mon, 18 Jun 2018 23:37:24 +0800 Subject: [PATCH 13/16] delete index.js --- src/index.js | 220 --------------------------------------------------- 1 file changed, 220 deletions(-) delete mode 100644 src/index.js diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 68456bb..0000000 --- a/src/index.js +++ /dev/null @@ -1,220 +0,0 @@ -import { EventEmitter } from 'events' -import { - forEach, - isArray, - map, -} from 'lodash' -import { - format, - JsonRpcError, - MethodNotFound, - parse, -} from 'json-rpc-protocol' - -// =================================================================== - -// Give access to low level interface. -export * from 'json-rpc-protocol' - -// =================================================================== - -function makeAsync (fn) { - return function () { - return new Promise(resolve => resolve(fn.apply(this, arguments))) - } -} - -const parseMessage = message => { - try { - return parse(message) - } catch (error) { - throw format.error(null, error) - } -} - -// Default onMessage implementation: -// -// - ignores notifications -// - throw MethodNotFound for all requests -function defaultOnMessage (message) { - if (message.type === 'request') { - throw new MethodNotFound(message.method) - } -} - -function noop () {} - -// Starts the autoincrement id with the JavaScript minimal safe integer to have -// more room before running out of integers (it's very far fetched but a very -// long running process with a LOT of messages could run out). -let nextRequestId = -9007199254740991 - -// =================================================================== - -export default class Peer extends EventEmitter { - constructor (onMessage = defaultOnMessage) { - super() - - this._handle = makeAsync(onMessage) - this._deferreds = Object.create(null) - } - - _getDeferred (id) { - const deferred = this._deferreds[id] - delete this._deferreds[id] - return deferred - } - - async exec (message, data) { - message = parseMessage(message) - - if (isArray(message)) { - const results = [] - - // Only returns non empty results. - await Promise.all(map(message, message => { - return this.exec(message, data).then(result => { - if (result !== undefined) { - results.push(result) - } - }) - })) - - return results - } - - const {type} = message - - if (type === 'error') { - const {id} = message - - // Some errors do not have an identifier, simply discard them. - if (id === null) { - return - } - - const {error} = message - this._getDeferred(id).reject( - // TODO: it would be great if we could return an error with of - // a more specific type (and custom types with registration). - new JsonRpcError(error.message, error.code, error.data) - ) - } else if (type === 'response') { - this._getDeferred(message.id).resolve(message.result) - } else if (type === 'notification') { - this._handle(message, data).catch(noop) - } else { - return this._handle(message, data).then( - (result) => format.response(message.id, result === undefined ? null : result), - (error) => format.error( - message.id, - - // If the method name is not defined, default to the method passed - // in the request. - (error instanceof MethodNotFound && !error.data) - ? new MethodNotFound(message.method) - : error - ) - ) - } - } - - // Fails all pending requests. - failPendingRequests (reason) { - const {_deferreds: deferreds} = this - - forEach(deferreds, ({reject}, id) => { - reject(reason) - delete deferreds[id] - }) - } - - /** - * This function should be called to send a request to the other end. - * - * TODO: handle multi-requests. - */ - request (method, params) { - return new Promise((resolve, reject) => { - const requestId = nextRequestId++ - - this.push(format.request(requestId, method, params)) - - this._deferreds[requestId] = {resolve, reject} - }) - } - - /** - * This function should be called to send a notification to the other end. - * - * TODO: handle multi-notifications. - */ - async notify (method, params) { - this.push(format.notification(method, params)) - } - - // minimal stream interface - - end (data, encoding, cb) { - if (typeof data === 'function') { - process.nextTick(data) - } else { - if (typeof encoding === 'function') { - process.nextTick(encoding) - } else if (typeof cb === 'function') { - process.nextTick(cb) - } - - if (data !== undefined) { - this.write(data) - } - } - } - - pipe (writable) { - const listeners = { - data: data => writable.write(data), - end: () => { - writable.end() - clean() - }, - } - - const clean = () => forEach(listeners, (listener, event) => { - this.removeListener(event, listener) - }) - forEach(listeners, (listener, event) => { - this.on(event, listener) - }) - - return writable - } - - push (data) { - return data === null - ? this.emit('end') - : this.emit('data', data) - } - - write (message) { - let cb - const n = arguments.length - if (n > 1 && typeof (cb = arguments[n - 1]) === 'function') { - process.nextTick(cb) - } - - this.exec(String(message)).then( - response => { - if (response !== undefined) { - this.push(response) - } - }, - error => { - this.emit('error', error) - } - ) - - // indicates that other calls to `write` are allowed - return true - } -} From 1f41843fb8a4acb32d76bb26e2fbcdac3515eaa3 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Tue, 19 Jun 2018 09:42:42 +0800 Subject: [PATCH 14/16] merge --- src/index.js | 219 --------------------------------------------------- 1 file changed, 219 deletions(-) delete mode 100644 src/index.js diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 580480c..0000000 --- a/src/index.js +++ /dev/null @@ -1,219 +0,0 @@ -import { EventEmitter } from 'events' -import { - forEach, - isArray, - map, -} from 'lodash' -import { - format, - JsonRpcError, - MethodNotFound, - parse, -} from 'json-rpc-protocol' - -// =================================================================== - -// Give access to low level interface. -export * from 'json-rpc-protocol' - -// =================================================================== - -function makeAsync (fn) { - return function () { - return new Promise(resolve => resolve(fn.apply(this, arguments))) - } -} - -const parseMessage = message => { - try { - return parse(message) - } catch (error) { - throw format.error(null, error) - } -} - -// Default onMessage implementation: -// -// - ignores notifications -// - throw MethodNotFound for all requests -function defaultOnMessage (message) { - if (message.type === 'request') { - throw new MethodNotFound(message.method) - } -} - -function noop () {} - -// Starts the autoincrement id with the JavaScript minimal safe integer to have -// more room before running out of integers (it's very far fetched but a very -// long running process with a LOT of messages could run out). -let nextRequestId = -9007199254740991 - -// =================================================================== - -export default class Peer extends EventEmitter { - constructor (onMessage = defaultOnMessage) { - super() - - this._asyncEmitError = process.nextTick.bind(process, this.emit.bind(this), 'error') - this._handle = makeAsync(onMessage) - this._deferreds = Object.create(null) - } - - _getDeferred (id) { - const deferred = this._deferreds[id] - delete this._deferreds[id] - return deferred - } - - async exec (message, data) { - message = parseMessage(message) - - if (isArray(message)) { - const results = [] - - // Only returns non empty results. - await Promise.all(map(message, message => { - return this.exec(message, data).then(result => { - if (result !== undefined) { - results.push(result) - } - }) - })) - - return results - } - - const {type} = message - - if (type === 'error') { - const {id} = message - - // Some errors do not have an identifier, simply discard them. - if (id === null) { - return - } - - const {error} = message - this._getDeferred(id).reject( - // TODO: it would be great if we could return an error with of - // a more specific type (and custom types with registration). - new JsonRpcError(error.message, error.code, error.data) - ) - } else if (type === 'response') { - this._getDeferred(message.id).resolve(message.result) - } else if (type === 'notification') { - this._handle(message, data).catch(noop) - } else { - return this._handle(message, data).then( - (result) => format.response(message.id, result === undefined ? null : result), - (error) => format.error( - message.id, - - // If the method name is not defined, default to the method passed - // in the request. - (error instanceof MethodNotFound && !error.data) - ? new MethodNotFound(message.method) - : error - ) - ) - } - } - - // Fails all pending requests. - failPendingRequests (reason) { - const {_deferreds: deferreds} = this - - forEach(deferreds, ({reject}, id) => { - reject(reason) - delete deferreds[id] - }) - } - - /** - * This function should be called to send a request to the other end. - * - * TODO: handle multi-requests. - */ - request (method, params) { - return new Promise((resolve, reject) => { - const requestId = nextRequestId++ - - this.push(format.request(requestId, method, params)) - - this._deferreds[requestId] = {resolve, reject} - }) - } - - /** - * This function should be called to send a notification to the other end. - * - * TODO: handle multi-notifications. - */ - async notify (method, params) { - this.push(format.notification(method, params)) - } - - // minimal stream interface - - end (data, encoding, cb) { - if (typeof data === 'function') { - process.nextTick(data) - } else { - if (typeof encoding === 'function') { - process.nextTick(encoding) - } else if (typeof cb === 'function') { - process.nextTick(cb) - } - - if (data !== undefined) { - this.write(data) - } - } - } - - pipe (writable) { - const listeners = { - data: data => writable.write(data), - end: () => { - writable.end() - clean() - }, - } - - const clean = () => forEach(listeners, (listener, event) => { - this.removeListener(event, listener) - }) - forEach(listeners, (listener, event) => { - this.on(event, listener) - }) - - return writable - } - - push (data) { - return data === null - ? this.emit('end') - : this.emit('data', data) - } - - write (message) { - let cb - const n = arguments.length - if (n > 1 && typeof (cb = arguments[n - 1]) === 'function') { - process.nextTick(cb) - } - - this.exec(String(message)).then( - response => { - if (response !== undefined) { - this.push(response) - } - }, - this._asyncEmitError - ) - - // indicates that other calls to `write` are allowed - return true - } -} From 0d6b33bead65601e91328cd1c0b2aac3cd119696 Mon Sep 17 00:00:00 2001 From: Huan LI Date: Tue, 5 Feb 2019 22:30:24 +0800 Subject: [PATCH 15/16] fix typing issues --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9c36ae5..83e1980 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,7 @@ const parseMessage = (message: string | object) => { // - throw MethodNotFound for all requests function defaultOnMessage (message: JsonRpcPayload) { if (message.type === 'request') { - throw new MethodNotFound((message as JsonRpcPayloadRequest).method) + throw new MethodNotFound(message.method) } } @@ -106,7 +106,7 @@ export class Peer extends EventEmitter implements NodeJS.WritableStream { const { id } = messagePayload as JsonRpcPayloadError // Some errors do not have an identifier, simply discard them. - if (id === undefined || id === null) { + if (id === null) { return undefined } From 2a08074ecaead6d07fd3020dce68da255e3d41bf Mon Sep 17 00:00:00 2001 From: Huan LI Date: Tue, 5 Feb 2019 22:30:52 +0800 Subject: [PATCH 16/16] ignore package-lock.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 44c2154..6f9ca41 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ npm-debug.log.* pnpm-debug.log pnpm-debug.log.* yarn-error.log +package-lock.json