From e5cb09f3bf8ac60ab675f31e88f7966009694050 Mon Sep 17 00:00:00 2001 From: shanejonas Date: Mon, 13 May 2019 19:08:03 -0700 Subject: [PATCH] fix: initial transport implementation --- package-lock.json | 94 +++++++++++++++++--- package.json | 7 ++ src/RequestManager.test.ts | 36 ++++++++ src/RequestManager.ts | 61 +++++++++++++ src/example.ts | 12 +++ src/index.test.ts | 8 +- src/index.ts | 7 +- src/transports/EventEmitterTransport.test.ts | 21 +++++ src/transports/EventEmitterTransport.ts | 36 ++++++++ src/transports/HTTPTransport.ts | 37 ++++++++ src/transports/Transport.ts | 7 ++ src/transports/WebSocketTransport.ts | 42 +++++++++ 12 files changed, 353 insertions(+), 15 deletions(-) create mode 100644 src/RequestManager.test.ts create mode 100644 src/RequestManager.ts create mode 100644 src/example.ts create mode 100644 src/transports/EventEmitterTransport.test.ts create mode 100644 src/transports/EventEmitterTransport.ts create mode 100644 src/transports/HTTPTransport.ts create mode 100644 src/transports/Transport.ts create mode 100644 src/transports/WebSocketTransport.ts diff --git a/package-lock.json b/package-lock.json index da85def..d3b5f8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -432,6 +432,18 @@ "@babel/types": "^7.3.0" } }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/isomorphic-fetch": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.35.tgz", + "integrity": "sha512-DaZNUvLDCAnCTjgwxgiL1eQdxIKEpNLOlTNtAgnZc50bG2copGhRrFN9/PxPBuJe+tZVLCbQ7ls0xveXVRPkvw==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -472,12 +484,28 @@ "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", "dev": true }, + "@types/node": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.1.tgz", + "integrity": "sha512-7sy7DKVJrCTbaAERJZq/CU12bzdmpjRr321/Ne9QmzhB3iZ//L16Cizcni5hHNbANxDbxwMb9EFoWkM8KPkp0A==", + "dev": true + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "dev": true }, + "@types/websocket": { + "version": "0.0.40", + "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-0.0.40.tgz", + "integrity": "sha512-ldteZwWIgl9cOy7FyvYn+39Ah4+PfpVE72eYKw75iy2L0zTbhbcwvzeJ5IOu6DQP93bjfXq0NGHY6FYtmYoqFQ==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, "@types/yargs": { "version": "12.0.12", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", @@ -632,8 +660,7 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, "asynckit": { "version": "0.4.0", @@ -1244,6 +1271,14 @@ "safer-buffer": "^2.1.0" } }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -2311,7 +2346,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -2514,8 +2548,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-symbol": { "version": "1.0.2", @@ -2562,6 +2595,20 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, + "isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -3136,6 +3183,17 @@ "whatwg-url": "^6.4.1", "ws": "^5.2.0", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + } } }, "jsesc": { @@ -3497,6 +3555,15 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4129,8 +4196,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sane": { "version": "4.1.0", @@ -4944,6 +5010,11 @@ "iconv-lite": "0.4.24" } }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + }, "whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", @@ -5047,12 +5118,11 @@ } }, "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", - "dev": true, + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.0.tgz", + "integrity": "sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==", "requires": { - "async-limiter": "~1.0.0" + "async-limiter": "^1.0.0" } }, "xml-name-validator": { diff --git a/package.json b/package.json index 5745fb0..ae0a6b9 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,17 @@ }, "homepage": "https://github.com/open-rpc/client-js#readme", "devDependencies": { + "@types/isomorphic-fetch": "0.0.35", "@types/jest": "^24.0.12", + "@types/websocket": "0.0.40", "jest": "^24.8.0", "ts-jest": "^24.0.2", "tslint": "^5.16.0", "typescript": "^3.4.5" + }, + "dependencies": { + "isomorphic-fetch": "^2.2.1", + "isomorphic-ws": "^4.0.1", + "ws": "^7.0.0" } } diff --git a/src/RequestManager.test.ts b/src/RequestManager.test.ts new file mode 100644 index 0000000..a33eef2 --- /dev/null +++ b/src/RequestManager.test.ts @@ -0,0 +1,36 @@ +import RequestManager from "./RequestManager"; +import EventEmitterTransport from "./transports/EventEmitterTransport"; + +describe("client-js", () => { + it("can be constructed", () => { + const transport = new EventEmitterTransport("foo://unique-uri"); + const c = new RequestManager([transport]); + expect(!!c).toEqual(true); + }); + + it("has a request method that returns a promise", () => { + const transport = new EventEmitterTransport("foo://unique-uri"); + const c = new RequestManager([transport]); + expect(typeof c.request).toEqual("function"); + expect(typeof c.request("my_method", null).then).toEqual("function"); + }); + + it("can connect", () => { + const transport = new EventEmitterTransport("foo://unique-uri"); + const c = new RequestManager([transport]); + return c.connect(); + }); + + it("can send a request", (done) => { + const transport = new EventEmitterTransport("foo://unique-uri"); + const c = new RequestManager([transport]); + c.connect(); + transport.onData((data: any) => { + const d = JSON.parse(data); + expect(d.method).toEqual("foo"); + done(); + }); + c.request("foo", []); + }); + +}); diff --git a/src/RequestManager.ts b/src/RequestManager.ts new file mode 100644 index 0000000..7df1644 --- /dev/null +++ b/src/RequestManager.ts @@ -0,0 +1,61 @@ +import ITransport from "./transports/Transport"; +let id = 1; + +/* +** Naive Request Manager, only use 1st transport. + * A more complex request manager could try each transport. + * If a transport fails, or times out, move on to the next. + */ +class RequestManager { + public transports: ITransport[]; + private requests: any; + private connectPromise: Promise; + constructor(transports: ITransport[]) { + this.transports = transports; + this.requests = {}; + this.connectPromise = this.connect(); + } + public connect() { + const promises = this.transports.map((transport) => { + return new Promise(async (resolve, reject) => { + await transport.connect(); + transport.onData((data: any) => { + this.onData(data); + }); + resolve(); + }); + }); + return Promise.all(promises); + } + public onData(data: string) { + const parsedData = JSON.parse(data); + if (typeof parsedData.result === "undefined") { + return; + } + // call request callback for id + this.requests[parsedData.id](parsedData); + delete this.requests[parsedData.id]; + } + public async request(method: string, params: any): Promise { + await this.connectPromise; + return new Promise((resolve, reject) => { + const i = id++; + // naively grab first transport and use it + const transport = this.transports[0]; + this.requests[i] = resolve; + transport.sendData(JSON.stringify({ + jsonrpc: "2.0", + id: i, + method, + params, + })); + }); + } + public close() { + this.transports.forEach((transport) => { + transport.close(); + }); + } +} + +export default RequestManager; diff --git a/src/example.ts b/src/example.ts new file mode 100644 index 0000000..2b12b11 --- /dev/null +++ b/src/example.ts @@ -0,0 +1,12 @@ +import Client from "."; +import RequestManager from "./RequestManager"; +import Transport from "./transports/HTTPTransport"; + +const t = new Transport("http://localhost:8545"); + +const c = new Client(new RequestManager([t])); + +// make request for eth_blockNumber +c.request("eth_blockNumber", []).then((b: any) => { + console.log('in then', b); //tslint:disable-line +}); diff --git a/src/index.test.ts b/src/index.test.ts index 874528d..48d42b9 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,14 +1,18 @@ import Client from "."; +import RequestManager from "./RequestManager"; +import EventEmitterTransport from "./transports/EventEmitterTransport"; describe("client-js", () => { it("can be constructed", () => { - const c = new Client(); + const c = new Client(new RequestManager([new EventEmitterTransport("foo://unique")])); expect(!!c).toEqual(true); }); it("has a request method that returns a promise", () => { - const c = new Client(); + const c = new Client(new RequestManager([new EventEmitterTransport("foo://unique")])); + console.log("test city before"); // tslint:disable-line expect(typeof c.request).toEqual("function"); expect(typeof c.request("my_method", null).then).toEqual("function"); }); + }); diff --git a/src/index.ts b/src/index.ts index edd5538..08d936b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,16 @@ +import RequestManager from "./RequestManager"; interface IClient { request(method: string, params: any): Promise; } class Client implements IClient { + public requestManager: RequestManager; + constructor(requestManager: RequestManager) { + this.requestManager = requestManager; + } public request(method: string, params: any) { - return new Promise(() => {/* */}); + return this.requestManager.request(method, params); } } diff --git a/src/transports/EventEmitterTransport.test.ts b/src/transports/EventEmitterTransport.test.ts new file mode 100644 index 0000000..5875836 --- /dev/null +++ b/src/transports/EventEmitterTransport.test.ts @@ -0,0 +1,21 @@ +import EventEmitterTransport from "./EventEmitterTransport"; + +describe("EventEmitterTransport", () => { + it("can connect", () => { + const eventEmitterTransport = new EventEmitterTransport("foo://bar"); + eventEmitterTransport.connect(); + }); + it("can close", () => { + const eventEmitterTransport = new EventEmitterTransport("foo://bar"); + eventEmitterTransport.close(); + }); + it("can send and receive data", (done) => { + const eventEmitterTransport = new EventEmitterTransport("foo://bar"); + eventEmitterTransport.onData((data: any) => { + const d = JSON.parse(data); + expect(d.foo).toEqual("bar"); + done(); + }); + eventEmitterTransport.sendData(JSON.stringify({foo: "bar"})); + }); +}); diff --git a/src/transports/EventEmitterTransport.ts b/src/transports/EventEmitterTransport.ts new file mode 100644 index 0000000..4679389 --- /dev/null +++ b/src/transports/EventEmitterTransport.ts @@ -0,0 +1,36 @@ +import { EventEmitter } from "events"; +import ITransport from "./Transport"; + +class EventEmitterTransport implements ITransport { + public connection: EventEmitter | null; + constructor(uri: string) { + this.connection = new EventEmitter(); + } + public connect(): Promise { + // noop + return Promise.resolve(); + } + public onData(callback: any) { + if (!this.connection) { + return; + } + this.connection.addListener("message", (data: any) => { + callback(data); + }); + } + public sendData(data: any) { + if (!this.connection) { + return; + } + this.connection.emit("message", data); + } + public close() { + if (!this.connection) { + return; + } + this.connection.removeAllListeners(); + this.connection = null; + } +} + +export default EventEmitterTransport; diff --git a/src/transports/HTTPTransport.ts b/src/transports/HTTPTransport.ts new file mode 100644 index 0000000..9c2779f --- /dev/null +++ b/src/transports/HTTPTransport.ts @@ -0,0 +1,37 @@ +import fetch from "isomorphic-fetch"; +import ITransport from "./Transport"; + +class HTTPTransport implements ITransport { + private uri: string; + private onDataCallbacks: any[]; + constructor(uri: string) { + this.onDataCallbacks = []; + this.uri = uri; + } + public connect(): Promise { + return Promise.resolve(); + } + public onData(callback: any) { + this.onDataCallbacks.push(callback); + } + public sendData(data: string) { + fetch(this.uri, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: data, + }).then((result) => { + return result.text(); + }).then((result) => { + this.onDataCallbacks.map((cb) => { + cb(result); + }); + }); + } + public close() { + this.onDataCallbacks = []; + } +} + +export default HTTPTransport; diff --git a/src/transports/Transport.ts b/src/transports/Transport.ts new file mode 100644 index 0000000..a008250 --- /dev/null +++ b/src/transports/Transport.ts @@ -0,0 +1,7 @@ + +export default interface ITransport { + connect(): Promise; + close(): void; + onData(callback: (data: string) => any): void; + sendData(data: string): void; +} diff --git a/src/transports/WebSocketTransport.ts b/src/transports/WebSocketTransport.ts new file mode 100644 index 0000000..01760a1 --- /dev/null +++ b/src/transports/WebSocketTransport.ts @@ -0,0 +1,42 @@ +import WebSocket from "isomorphic-ws"; +import ITransport from "./Transport"; + +class WebSocketTransport implements ITransport { + public connection: WebSocket | undefined; + constructor(uri: string) { + this.connection = new WebSocket(uri); + } + public connect(): Promise { + return new Promise((resolve, reject) => { + if (!this.connection) { return; } + const cb = () => { + if (!this.connection) { return; } + this.connection.removeEventListener("open", cb); + resolve(); + }; + this.connection.addEventListener("open", cb); + }); + } + public onData(callback: any) { + if (!this.connection) { + return; + } + this.connection.addEventListener("message", (ev: MessageEvent) => { + callback(ev.data); + }); + } + public sendData(data: any) { + if (!this.connection) { + return; + } + this.connection.send(data); + } + public close() { + if (!this.connection) { + return; + } + this.connection.close(); + } +} + +export default WebSocketTransport;