UNPKG

opendb_test_rpc

Version:

general purpose library for OpenDB blockchain

403 lines (339 loc) 11.2 kB
import { v1 } from 'uuid' import WebSocket from 'isomorphic-ws' export type RpcEventFunction = (e: Event) => void export type RpcMessageEventFunction = (e: MessageEvent) => void export type RpcCloseEventFunction = (e: CloseEvent) => void export type RpcNotificationEvent = (data: IRpcNotification) => void export type RpcRequestEvent = (data: IRpcRequest) => void export type RpcSuccessResponseEvent = (data: IRpcSuccessResponse) => void export type RpcErrorResponseEvent = (data: IRpcErrorResponse) => void export enum RpcVersions { RPC_VERSION = '2.0', } export type RpcId = string | number export interface IRpcData { method: string // TSES-lint: (any - unknown) Unexpected any. Specify a different type params?: unknown } export interface IRpcNotification extends IRpcData { jsonrpc: RpcVersions.RPC_VERSION } export interface IRpcRequest extends IRpcNotification { // if not included its notification id: RpcId } export interface IRpcResponse { id: RpcId jsonrpc: RpcVersions.RPC_VERSION } export interface IRpcSuccessResponse extends IRpcResponse { // if not included its notification // TSES-lint: (any - unknown) Unexpected any. Specify a different type result: unknown } export interface IRpcError { code: number message: string // TSES-lint: (any - unknown) Unexpected any. Specify a different type data?: unknown } export interface IRpcErrorResponse extends IRpcResponse { error: IRpcError } export interface IRpcWebSocketConfig { responseTimeout: number } export type RpcUnidentifiedMessage = | IRpcRequest | IRpcNotification | IRpcSuccessResponse | IRpcErrorResponse export class RpcWebSocketClient { // native websocket public ws: WebSocket private idAwaiter: { // TSES-lint: (any - unknown) Unexpected any. Specify a different type [id: string]: (data?: unknown) => void } = {} private onOpenHandlers: RpcEventFunction[] = [] private onAnyMessageHandlers: RpcMessageEventFunction[] = [] private onNotification: RpcNotificationEvent[] = [] private onRequest: RpcRequestEvent[] = [] private onSuccessResponse: RpcSuccessResponseEvent[] = [] private onErrorResponse: RpcErrorResponseEvent[] = [] private onErrorHandlers: RpcEventFunction[] = [] private onCloseHandlers: RpcCloseEventFunction[] = [] private config = { responseTimeout: 10000, } // constructor /** * Does not start WebSocket connection! * You need to call connect() method first. * @memberof RpcWebSocketClient */ public constructor() { // TSES-lint: (any - unknown) Unexpected any. Specify a different type this.ws = undefined as unknown } // public /** * Starts WebSocket connection. Returns Promise when connection is established. * @param {string} url * @param {(string | string[])} [protocols] * @memberof RpcWebSocketClient */ public async connect(url: string, protocols?: string | string[]) { this.ws = new WebSocket(url, protocols) await this.listen() } // events public onOpen(fn: RpcEventFunction) { this.onOpenHandlers.push(fn) } /** * Native onMessage event. DO NOT USE THIS unless you really have to or for debugging purposes. * Proper RPC events are onRequest, onNotification, onSuccessResponse and onErrorResponse (or just awaiting response). * @param {RpcMessageEventFunction} fn * @memberof RpcWebSocketClient */ public onAnyMessage(fn: RpcMessageEventFunction) { this.onAnyMessageHandlers.push(fn) } public onError(fn: RpcEventFunction) { this.onErrorHandlers.push(fn) } public onClose(fn: RpcCloseEventFunction) { this.onCloseHandlers.push(fn) } /** * Appends onmessage listener on native websocket with RPC handlers. * If onmessage function was already there, it will call it on beggining. * Useful if you want to use RPC WebSocket Client on already established WebSocket along with function changeSocket(). * @memberof RpcWebSocketClient */ public listenMessages() { let previousOnMessage: RpcEventFunction | undefined if (this.ws.onmessage) { previousOnMessage = this.ws.onmessage.bind(this.ws) } this.ws.onmessage = (e: MessageEvent) => { if (previousOnMessage) { previousOnMessage(e) } for (const handler of this.onAnyMessageHandlers) { handler(e) } const data: RpcUnidentifiedMessage = JSON.parse(e.data) if (this.isNotification(data)) { // notification for (const handler of this.onNotification) { handler(data) } } else if (this.isRequest(data)) { // request for (const handler of this.onRequest) { handler(data) } // responses } else if (this.isSuccessResponse(data)) { // success for (const handler of this.onSuccessResponse) { handler(data) } // resolve awaiting function this.idAwaiter[data.id](data.result) } else if (this.isErrorResponse(data)) { // error for (const handler of this.onErrorResponse) { handler(data) } // resolve awaiting function this.idAwaiter[data.id](data.error) } } } // communication /** * Creates and sends RPC request. Resolves when appropirate response is returned from server or after config.responseTimeout. * @param {string} method * @param {*} [params] * @returns * @memberof RpcWebSocketClient */ // TSES-lint: (any - unknown) Unexpected any. Specify a different type public call(method: string, params?: unknown) { return new Promise((resolve, reject) => { const data = this.buildRequest(method, params) // give limited time for response let timeout: NodeJS.Timeout if (this.config.responseTimeout) { timeout = setTimeout(() => { // stop waiting for response delete this.idAwaiter[data.id] reject( `Awaiting response to: ${method} with id: ${data.id} timed out.` ) }, this.config.responseTimeout) } // TSES-lint: (any - unknown) Unexpected any. Specify a different type // expect response this.idAwaiter[data.id] = (responseData?: unknown) => { // stop timeout clearInterval(timeout) // stop waiting for response delete this.idAwaiter[data.id] if (this.isRpcError(responseData)) { reject(responseData) return } resolve(responseData) } this.ws.send(JSON.stringify(data)) }) } // -------TODO: Unused Function Notify /** * Creates and sends RPC Notification. * @param {string} method * @param {*} [params] * @memberof RpcWebSocketClient */ // public notify(method: string, params?: any) { // this.ws.send(JSON.stringify(this.buildNotification(method, params))) // } // setup /** * You can provide custom id generation function to replace default uuid/v1. * @param {() => string} idFn * @memberof RpcWebSocketClient */ public customId(idFn: () => string) { this.idFn = idFn } /** * Allows modifying configuration. * @param {RpcWebSocketConfig} options * @memberof RpcWebSocketClient */ // TSES-lint: (any - unknown) Unexpected any. Specify a different type public configure(options: unknown) { Object.assign(this.config, options) } /** * Allows you to change used native WebSocket client to another one. * If you have already-connected WebSocket, use this with listenMessages(). * @param {WebSocket} ws * @memberof RpcWebSocketClient */ public changeSocket(ws: WebSocket) { this.ws = ws } // private // events private listen() { return new Promise<void>((resolve, reject) => { this.ws.onopen = (e: Event) => { for (const handler of this.onOpenHandlers) { handler(e) } resolve(void 0) } // listen for messages this.listenMessages() // called before onclose this.ws.onerror = (e: Event) => { for (const handler of this.onErrorHandlers) { handler(e) } } this.ws.onclose = (e: CloseEvent) => { for (const handler of this.onCloseHandlers) { handler(e) } reject() } }) } // TSES-lint: (any - unknown) Unexpected any. Specify a different type // request private buildRequest(method: string, params?: unknown): IRpcRequest { const data = this.buildRequestBase(method, params) data.jsonrpc = RpcVersions.RPC_VERSION return data } // TSES-lint: (any - unknown) Unexpected any. Specify a different type // private buildRequestBase(method: string, params?: unknown): IRpcRequest { // const data: IRpcRequest = {} as unknown // data.id = this.idFn() // data.method = method // if (params) { // data.params = params // } // return data // } private buildRequestBase(method: string, params?: unknown): IRpcRequest { const data: IRpcRequest = { id: this.idFn(), jsonrpc: RpcVersions.RPC_VERSION, method: method, } if (params) { data.params = params } return data } // -------TODO: Unused Function Notify // notification // private buildNotification(method: string, params?: any): IRpcNotification { // const data = this.buildNotificationBase(method, params) // data.jsonrpc = RpcVersions.RPC_VERSION // return data // } // private buildNotificationBase( // method: string, // params?: any // ): IRpcNotification { // const data: IRpcNotification = {} as any // data.method = method // if (params) { // data.params = params // } // return data // } private idFn(): RpcId { return v1() } // tests private isNotification( data: RpcUnidentifiedMessage ): data is IRpcNotification { return !(data as any).id // eslint-disable-line @typescript-eslint/no-explicit-any } private isRequest(data: RpcUnidentifiedMessage): data is IRpcRequest { // TSES-lint: (any - unknown) Unexpected any. Specify a different type return (data as IRpcRequest).method !== undefined // return (data as any).method } private isSuccessResponse( data: RpcUnidentifiedMessage ): data is IRpcSuccessResponse { // TSES_lint : error Do not access Object.prototype method 'hasOwnProperty' from target object // return data.hasOwnProperty(`result`) return Object.prototype.hasOwnProperty.call(data, 'result') } private isErrorResponse( data: RpcUnidentifiedMessage ): data is IRpcErrorResponse { // TSES_lint : error Do not access Object.prototype method 'hasOwnProperty' from target object // return data.hasOwnProperty(`error`) return Object.prototype.hasOwnProperty.call(data, 'error') } // TSES-lint: (any - unknown) Unexpected any. Specify a different type private isRpcError(data: unknown): data is IRpcError { // return typeof (data as any).code !== 'undefined' return typeof (data as IRpcError).code !== 'undefined' } } export default RpcWebSocketClient