UNPKG

@libp2p/websockets

Version:

JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec

110 lines (89 loc) 3.26 kB
import { AbortError } from '@libp2p/interface' import { CLOSE_TIMEOUT } from './constants.js' import type { AbortOptions, ComponentLogger, CounterGroup, MultiaddrConnection } from '@libp2p/interface' import type { Multiaddr } from '@multiformats/multiaddr' import type { DuplexWebSocket } from 'it-ws/duplex' export interface SocketToConnOptions { localAddr?: Multiaddr logger: ComponentLogger metrics?: CounterGroup metricPrefix?: string } // Convert a stream into a MultiaddrConnection // https://github.com/libp2p/interface-transport#multiaddrconnection export function socketToMaConn (stream: DuplexWebSocket, remoteAddr: Multiaddr, options: SocketToConnOptions): MultiaddrConnection { const log = options.logger.forComponent('libp2p:websockets:maconn') const metrics = options.metrics const metricPrefix = options.metricPrefix ?? '' const maConn: MultiaddrConnection = { log, async sink (source) { try { await stream.sink((async function * () { for await (const buf of source) { if (buf instanceof Uint8Array) { yield buf } else { yield buf.subarray() } } })()) } catch (err: any) { if (err.type !== 'aborted') { log.error(err) } } }, source: stream.source, remoteAddr, timeline: { open: Date.now() }, async close (options: AbortOptions = {}) { const start = Date.now() if (options.signal == null) { const signal = AbortSignal.timeout(CLOSE_TIMEOUT) options = { ...options, signal } } const listener = (): void => { const { host, port } = maConn.remoteAddr.toOptions() log('timeout closing stream to %s:%s after %dms, destroying it manually', host, port, Date.now() - start) this.abort(new AbortError('Socket close timeout')) } options.signal?.addEventListener('abort', listener) try { await stream.close() } catch (err: any) { log.error('error closing WebSocket gracefully', err) this.abort(err) } finally { options.signal?.removeEventListener('abort', listener) maConn.timeline.close = Date.now() } }, abort (err: Error): void { const { host, port } = maConn.remoteAddr.toOptions() log('timeout closing stream to %s:%s due to error', host, port, err) stream.destroy() maConn.timeline.close = Date.now() // ws WebSocket.terminate does not accept an Error arg to emit an 'error' // event on destroy like other node streams so we can't update a metric // with an event listener // https://github.com/websockets/ws/issues/1752#issuecomment-622380981 metrics?.increment({ [`${metricPrefix}error`]: true }) } } stream.socket.addEventListener('close', () => { metrics?.increment({ [`${metricPrefix}close`]: true }) // In instances where `close` was not explicitly called, // such as an iterable stream ending, ensure we have set the close // timeline if (maConn.timeline.close == null) { maConn.timeline.close = Date.now() } }, { once: true }) return maConn }