import { RecognizedString , TemplatedApp , WebSocket } from "@trufflesuite/uws-js-unofficial" ; import WebSocketCloseCodes from "./utils/websocket-close-codes" ; import { InternalOptions } from "../options" ; import * as Flavors from "@ganache/flavors" ; import { hasOwn , PromiEvent } from "@ganache/utils" ; import { isGeneratorFunction , isGeneratorObject } from "util/types" ; import { types } from "util" ; type MergePromiseT < Type > = Promise < Type extends Promise < infer X > ? X : never > ; type HandlesWebSocketSignature = ( payload : any , connection : WebSocket ) => any ; type WebSocketCapableFlavorMap = { [ k in keyof Flavors . ConnectorsByName ] : Flavors . ConnectorsByName [ k ] [ "handle" ] extends HandlesWebSocketSignature ? Flavors . ConnectorsByName [ k ] : never ; } ; export type WebSocketCapableFlavor = { [ k in keyof WebSocketCapableFlavorMap ] : WebSocketCapableFlavorMap [ k ] ; } [ keyof WebSocketCapableFlavorMap ] ; export type GanacheWebSocket = WebSocket & { closed ? : boolean ; } ; export type WebsocketServerOptions = Pick < InternalOptions [ "server" ] , "wsBinary" | "rpcEndpoint" > ; export const MAX_PAYLOAD_SIZE = 15 * 1024 * 1024 ; export default class WebsocketServer { #connections = new Map < WebSocket , Set < ( ) => void > > ( ) ; constructor ( app , connector , options ) { const connections = this . #connections ; const wsBinary = options . wsBinary ; const autoBinary = wsBinary === "auto" ; app . ws ( options . rpcEndpoint , { maxPayloadLength : MAX_PAYLOAD_SIZE , idleTimeout : 120 , open : ( ws ) => { ws . closed = false ; connections . set ( ws , new Set ( ) ) ; } , message : async ( ws , message , isBinary ) => { let payload ; const useBinary = autoBinary ? isBinary : ( wsBinary as boolean ) ; try { payload = connector . parse ( Buffer . from ( message ) ) ; } catch ( err ) { const response = connector . formatError ( err , payload ) ; ws . send ( response , useBinary ) ; return ; } let data ; try { const { value } = await connector . handle ( payload , ws ) ; if ( ws . closed ) return ; const resultEmitter = value as MergePromiseT < typeof value > ; const result = await resultEmitter ; if ( ws . closed ) return ; data = connector . format ( result , payload ) ; if ( typeof resultEmitter [ "on" ] === "function" ) { const resultEmitterPromiEvent = resultEmitter as PromiEvent < any > ; resultEmitterPromiEvent . on ( "message" , ( result ) => { const message = JSON . stringify ( { jsonrpc : "2.0" , method : result . type , params : result . data } ) ; ws . send ( message , isBinary ) ; } ) ; connections . get ( ws ) . add ( resultEmitterPromiEvent . dispose ) ; } } catch ( err ) { if ( ws . closed ) return ; data = connector . formatError ( err , payload ) ; } if ( types . isGeneratorObject ( data ) ) { const localData = data ; ws . cork ( ( ) => { const { value : first } = localData . next ( ) ; let { value : next , done } = localData . next ( ) ; if ( done ) { ws . send ( first , useBinary ) ; } else { const shouldCompress = false ; ws . sendFirstFragment ( first , useBinary , shouldCompress ) ; let prev = next ; for ( next of localData ) { ws . sendFragment ( prev , shouldCompress ) ; prev = next ; } ws . sendLastFragment ( next , shouldCompress ) ; } } ) ; } else { ws . send ( data as RecognizedString , useBinary ) ; } } , drain : ( ws ) => { } , close : ( ws ) => { ws . closed = true ; connections . get ( ws ) . forEach ( dispose => dispose ( ) ) ; connections . delete ( ws ) ; } } ) ; } close ( ) { this . #connections . forEach ( ( _ , ws ) => ws . end ( WebSocketCloseCodes . CLOSE_NORMAL , "Server closed by client" ) ) ; } }