UNPKG

@akala/json-rpc-ws

Version:

json-rpc websocket transport

153 lines (136 loc) 5.1 kB
'use strict'; import { default as ClientBase } from './shared-client.js'; import { SocketAdapter, SocketAdapterAkalaEventMap } from '../shared-connection.js'; import { Connection } from '../browser.js' import { EventEmitter, AllEventKeys, AllEvents, EventListener, EventOptions, StatefulSubscription, Subscription } from '@akala/core'; import debug from 'debug'; /** * json-rpc-ws connection * * @constructor * @param {Socket} socket - web socket for this connection * @param {Object} parent - parent that controls this connection */ export class WebSocketAdapter extends EventEmitter<SocketAdapterAkalaEventMap> implements SocketAdapter { constructor(private readonly socket: WebSocket) { super(); } pipe(socket: SocketAdapter) { this.on('message', (message) => socket.send(message)); this.on('close', () => socket.close()); } get open(): boolean { return this.socket.readyState == WebSocket.OPEN; } close(): void { this.socket.close(); } send(data: string): void { this.socket.send(data); } private readonly messageListeners: [(ev: unknown) => void, (ev: unknown) => void][] = []; public off<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]> ): boolean { switch (event) { case 'message': { let listeners = this.messageListeners; if (handler) listeners = listeners.filter(f => f[0] == handler); listeners.forEach(l => this.socket.removeEventListener('message', l[1])); } break; case 'close': case 'error': case 'open': //eslint-disable-next-line @typescript-eslint/no-explicit-any this.socket.removeEventListener(event, handler as any); break; default: throw new Error(`Unsupported event ${String(event)}`); } return true; } public on<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>, options?: EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]> ): Subscription { switch (event) { case 'message': { const x = function (ev) { return handler.call(this, ev.data) }; this.messageListeners.push([handler, x]); this.socket.addEventListener('message', x, options); return new StatefulSubscription(() => { this.messageListeners.splice(this.messageListeners.findIndex(x => x[0] === handler), 1); this.socket.removeEventListener('message', x); }).unsubscribe; } break; case 'close': case 'error': case 'open': //eslint-disable-next-line @typescript-eslint/no-explicit-any this.socket.addEventListener(event, handler as any); return new StatefulSubscription(() => { this.socket.removeEventListener(event, handler as any); }).unsubscribe; default: throw new Error(`Unsupported event ${String(event)}`); } } public once<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]> ): Subscription { switch (event) { case 'message': return this.on(event, handler, { once: true } as EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>); case 'close': case 'error': case 'open': return this.on(event, handler, { once: true } as EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>); default: throw new Error(`Unsupported event ${event?.toString()}`); } } } export default class Client extends ClientBase<ReadableStream, { protocols?: string | string[] }> { connection(socket: SocketAdapter): Connection { return new Connection(socket, this); } constructor(options?: { protocols?: string | string[] }) { super(Client.connect, options); } public static connect(address: string, options?: { protocols?: string | string[] }): SocketAdapter { return new WebSocketAdapter(new WebSocket(address.replace(/^http/, 'ws'), options?.protocols)); } } const logger = debug('akala:json-rpc-ws'); export function createClient(options?: { protocols?: string | string[] }): Client { logger('create ws client'); return new Client(options); } export const connect = Client.connect;