UNPKG

@dougrich/json-rpc-commonjs-client

Version:

x-json-rpc js client

226 lines (206 loc) 5.42 kB
class RPCError extends Error { constructor ({ code, message, data }) { super(`[${code}] ${message}`) this.name = 'RPCError' this.data = data this.isRPCError = true Error.captureStackTrace(this, RPCError) } } const HeaderContentType = 'Content-Type' const DefaultFetch = (typeof window !== 'undefined') ? window.fetch : global.fetch const DefaultWebSocket = (typeof window !== 'undefined') ? window.WebSocket : global.WebSocket const DefaultContentType = 'application/json' const DefaultMethod = 'POST' const JSONRPCVersion = '2.0-x' class BaseJSONRPCClient { constructor () { this.id = 0 this.pending = {} this.cachedNamespaces = {} this.debounceCalls = [] } _handle (response) { if (!Array.isArray(response)) { response = [response] } for (const each of response) { if (each.jsonrpc !== JSONRPCVersion) { throw new Error('RPC Error: missing jsonrpc value in JSON response') } } // this is broken apart to ensure that we don't partially resolve for (const each of response) { if (this.pending[each.id]) { this.pending[each.id](each) delete this.pending[each.id] } } } _resolve (ids, err) { for (const id of ids) { if (this.pending[id]) { this.pending[id]({ error: err }) delete this.pending[id] } } } _newPending (id) { return new Promise((resolve, reject) => { this.pending[id] = ({ result, error }) => { if (error != null) { if (error.code) { reject(new RPCError(error)) } else { reject(error) } } else { resolve(result) } } }) } _proxy (parts) { const fullyQualifedName = parts.join('.') if (!this.cachedNamespaces[fullyQualifedName]) { this.cachedNamespaces[fullyQualifedName] = new Proxy(function () {}, { get: (_, method) => { return this._proxy([...parts, method]) }, apply: (target, thisArg, argumentsList) => { const payload = { jsonrpc: JSONRPCVersion, method: fullyQualifedName, params: argumentsList, id: this.id++ } return this._enqueue(payload) } }) } return this.cachedNamespaces[fullyQualifedName] } api (namespace) { const parts = [] if (namespace) { parts.push(namespace) } return this._proxy(parts) } } class JSONRPCClient extends BaseJSONRPCClient { constructor ( endpoint, { fetch, fetchOptions, debounceWindow } = {} ) { super() this.endpoint = endpoint this.fetch = fetch || DefaultFetch this.fetchOptions = fetchOptions || {} this.debounceWindow = debounceWindow || 0 } _request (payload, ids) { return this.fetch(this.endpoint, { method: DefaultMethod, ...this.fetchOptions, headers: { [HeaderContentType]: DefaultContentType, ...(this.fetchOptions.headers || {}) }, body: JSON.stringify(payload) }).then(res => res.json()).then(response => { this._handle(response) return undefined }).catch(err => { return err }).then(err => { this._resolve(ids, err) }) } _enqueue (payload) { const p = this._newPending(payload.id) if (this.debounceWindow) { this.debounceCalls.push(payload) if (this.debounceTimeout == null) { this.debounceTimeout = setTimeout(() => { const calls = this.debounceCalls this.debounceTimeout = null this.debounceCalls = [] this._request(calls, calls.map(x => x.id)) }, this.debounceWindow) } } else { this._request(payload, [payload.id]) } return p } } class PersistentJSONRPCClient extends BaseJSONRPCClient { constructor ( endpoint, { websocket } = {} ) { super() this.endpoint = endpoint this.connection = null this.WS = websocket || DefaultWebSocket this.connecting = null } _connect () { if (!this.connecting) { this.connecting = new Promise((resolve, reject) => { this.connection = new this.WS(this.endpoint) this.connection.addEventListener('open', e => { resolve() }) this.connection.addEventListener('message', e => { let data = null try { data = JSON.parse(e.data) } catch (err) { // error occured parsing json console.error('unknown error occured - panic') this.close() } this._handle(data) }) this.connection.addEventListener('error', e => { console.error('connection error') this.close() }) this.connection.addEventListener('close', e => { // close this.close() }) }) } return this.connecting } async _enqueue (payload) { await this._connect() const p = this._newPending(payload.id) this.connection.send(JSON.stringify(payload)) return p } close () { if (this.connection) { this.connection.close(1000) } this.connection = null this.connecting = null } } module.exports = { PersistentJSONRPCClient, JSONRPCClient, HeaderContentType, DefaultContentType, DefaultMethod, JSONRPCVersion }