UNPKG

@akala/json-rpc-ws

Version:

json-rpc websocket transport

162 lines (145 loc) 5.73 kB
import { Base } from './base.js'; import debug from 'debug'; const logger = debug('akala:json-rpc-ws'); import { type PayloadDataType, Connection, Payload } from './shared-connection.js'; import { type Error as MyError } from './errors.js' import { ErrorWithStatus, HttpStatusCode, IncompleteMessageError, IsomorphicBuffer, SocketProtocolTransformer, type SocketAdapter } from '@akala/core'; /** * json-rpc-ws connection * * @constructor * @param {Socket} socket - web socket for this connection * @param {Object} parent - parent that controls this connection */ export function JsonNDRpcTransformer<T>(): SocketProtocolTransformer<any, string[]> { return { receive(chunks: string[] | IsomorphicBuffer[]) { if (typeof chunks !== 'object' || !Array.isArray(chunks)) if (typeof chunks == 'string') chunks = [chunks]; else if (typeof chunks == 'object' && (chunks as any) instanceof IsomorphicBuffer) chunks = [chunks]; let stringCount: number = 0; let bufferCount: number = 0; for (const chunk of chunks) { if (typeof chunk == 'string') stringCount++; else if (chunk instanceof IsomorphicBuffer) bufferCount++; else throw new ErrorWithStatus(HttpStatusCode.BadRequest, 'Expected a string or IsomorphicBuffer, but got ' + typeof chunk); } let data: string; if (stringCount > bufferCount) data = chunks.reduce((previous, current) => previous + (typeof current == 'string' ? current : current.toString('utf8')), '') as string; else if (bufferCount == chunks.length) data = IsomorphicBuffer.concat(chunks as IsomorphicBuffer[]).toString('utf-8'); else data = chunks.reduce((previous, chunk) => previous + (typeof chunk == 'string' ? chunk : chunk.toString('utf8')), '') as string; let messages = data.split('\n'); if (messages.length == 1) throw new IncompleteMessageError([], messages); if (messages[messages.length - 1] != '') throw new IncompleteMessageError( messages.slice(0, messages.length - 1).map(message => JSON.parse(message)), [messages[messages.length - 1]]); return messages.slice(0, messages.length - 1).map(data => JSON.parse(data)); }, send(data: T) { return [JSON.stringify(data) + '\n']; } } } export default abstract class Client<TStreamable, TConnectOptions> extends Base<TStreamable> { constructor(private socketConstructor: (address: string, options?: TConnectOptions) => SocketAdapter<Payload<TStreamable>>, private options?: TConnectOptions) { super('client'); logger('new Client'); } public socket?: SocketAdapter<Payload<TStreamable>>; /** * Connect to a json-rpc-ws server * * @param {String} address - url to connect to i.e. `ws://foo.com/`. * @param {function} callback - optional callback to call once socket is connected * @public */ public connect(address: string, callback: (err?: Event) => void): void { logger('Client connect %s', address); if (this.isConnected()) throw new Error('Already connected'); let opened = false; const socket = this.socket = this.socketConstructor(address, this.options); socket.once('open', () => { // The client connected handler runs scoped as the socket so we can pass // it into our connected method like thisk this.connected(socket); opened = true; if (callback) callback.call(this); }); if (callback) this.socket.once('error', function socketError(err) { if (!opened) { callback.call(self, err); } }); } /** * Test whether we have a connection or not * * @returns {Boolean} whether or not we have a connection * @public */ public isConnected(): boolean { return Object.keys(this.connections).length !== 0; } /** * Return the current connection (there can be only one) * * @returns {Object} current connection * @public */ public getConnection(): Connection<TStreamable> { const ids = Object.keys(this.connections); return this.connections[ids[0]]; } /** * Close the current connection */ public disconnect(): Promise<CloseEvent> { if (!this.isConnected()) throw new Error('Not connected'); const connection = this.getConnection(); return connection.hangup(); } /** * Send a method request * * @param {String} method - name of method * @param {Array} params - optional parameters for method * @param {function} callback - optional reply handler * @public * @todo allow for empty params aka arguments.length === 2 */ public send<TParamType extends PayloadDataType<TStreamable>, TReplyType extends PayloadDataType<TStreamable>>(method: string, params: TParamType, callback?: (error?: MyError, result?: TReplyType) => void): void { logger('send %s', method); if (!this.isConnected()) throw new Error('Not connected'); const connection = this.getConnection(); connection.sendMethod(method, params, callback); } }