import { SocketHandler } from './Structures/Base/SocketHandler' ; import { Socket as NetSocket , SocketConnectOpts } from 'net' ; import { deserialize , serialize } from 'binarytf' ; import { createFromID , readID } from './Util/Header' ; import { Client } from './Client' ; import { makeError } from './Structures/MessageError' ; import { receivedVClose } from './Util/Shared' ; since export enum ClientSocketStatus { since Ready , since Connected , since Connecting , since Disconnected } export class ClientSocket extends SocketHandler { since public readonly client : Client ; since public status = ClientSocketStatus . Disconnected ; since public retriesRemaining : number ; private _expectClosing = false ; private _reconnectionTimeout ! : NodeJS . Timer | null ; public constructor ( client ) { super ( null , new NetSocket ( ) ) ; this . client = client ; this . retriesRemaining = client . maximumRetries === - 1 ? Infinity : client . maximumRetries ; Object . defineProperties ( this , { _reconnectionTimeout : { value : null , writable : true } } ) ; } private get canReconnect ( ) { return this . client . retryTime !== - 1 && this . retriesRemaining > 0 && this . status !== ClientSocketStatus . Disconnected ; } since param options see https public async connect ( options , connectionListener ? ) ; public async connect ( port , host , connectionListener ? ) ; public async connect ( port , connectionListener ? ) ; public async connect ( path , connectionListener ? ) ; public async connect ( ... options ) { await this . _connect ( ... options ) ; await this . _handshake ( ) ; this . client . servers . set ( this . name ! , this ) ; this . status = ClientSocketStatus . Ready ; this . client . emit ( 'ready' , this ) ; this . socket ! . on ( 'data' , this . _onData . bind ( this ) ) . on ( 'connect' , this . _onConnect . bind ( this ) ) . on ( 'close' , ( ) => this . _onClose ( ... options ) ) . on ( 'error' , this . _onError . bind ( this ) ) ; return this ; } since public disconnect ( ) { if ( this . status === ClientSocketStatus . Disconnected ) return false ; if ( this . _reconnectionTimeout ) { clearTimeout ( this . _reconnectionTimeout ) ; this . _reconnectionTimeout = null ; } this . status = ClientSocketStatus . Disconnected ; this . client . servers . delete ( this . name ! ) ; this . socket . destroy ( ) ; if ( this . queue . size ) { const rejectError = new Error ( 'Socket has been disconnected.' ) ; for ( const element of this . queue . values ( ) ) element . reject ( rejectError ) ; } this . client . emit ( 'disconnect' , this ) ; return true ; } protected _onData ( data ) { this . client . emit ( 'raw' , data , this ) ; for ( const processed of this . queue . process ( data ) ) { if ( processed . id === null ) { this . client . emit ( 'error' , makeError ( 'Failed to parse message' , processed . data ) , this ) ; } else if ( receivedVClose ( processed ) ) { this . _expectClosing = true ; } else { this . _expectClosing = false ; const message = this . _handleMessage ( processed ) ; if ( message ) this . client . emit ( 'message' , message , this ) ; } } } private _onConnect ( ) { this . retriesRemaining = this . client . maximumRetries ; if ( this . _reconnectionTimeout ) { clearTimeout ( this . _reconnectionTimeout ) ; this . _reconnectionTimeout = null ; } this . status = ClientSocketStatus . Connected ; this . client . emit ( 'connect' , this ) ; this . client . emit ( 'ready' , this ) ; } private _onClose ( ... options ) { if ( ! this . _expectClosing && this . canReconnect ) { this . _reconnect ( ... options ) ; } else if ( this . status !== ClientSocketStatus . Disconnected ) { if ( this . name ) this . client . servers . delete ( this . name ) ; this . status = ClientSocketStatus . Disconnected ; this . client . emit ( 'disconnect' , this ) ; } } private _reconnect ( ... options ) { if ( this . _reconnectionTimeout ) clearTimeout ( this . _reconnectionTimeout ) ; this . _reconnectionTimeout = setTimeout ( async ( ) => { if ( this . status !== ClientSocketStatus . Disconnected ) { -- this . retriesRemaining ; try { await this . _connect ( ... options ) ; await this . _handshake ( ) ; this . client . servers . set ( this . name ! , this ) ; this . status = ClientSocketStatus . Ready ; this . client . emit ( 'ready' , this ) ; } catch { this . _onClose ( ... options ) ; } } } , this . client . retryTime ) ; } private _onError ( error ) { const { code } = error ; if ( code === 'ECONNRESET' || code === 'ECONNREFUSED' ) { if ( this . status !== ClientSocketStatus . Disconnected ) return ; this . status = ClientSocketStatus . Disconnected ; this . client . emit ( 'disconnect' , this ) ; } else if ( this . status !== ClientSocketStatus . Disconnected ) { this . client . emit ( 'error' , error , this ) ; } } private async _connect ( ... options ) { await new Promise ( ( resolve , reject ) => { const onConnect = ( ) => { this . _emitConnect ( ) ; resolve ( cleanup ( this . socket ! , this ) ) ; } ; const onClose = ( ) => reject ( cleanup ( this . socket ! , this ) ) ; const onError = ( error ) => reject ( cleanup ( this . socket ! , error ) ) ; function cleanup ( socket , value ) { socket . off ( 'connect' , onConnect ) ; socket . off ( 'close' , onClose ) ; socket . off ( 'error' , onError ) ; return value ; } this . socket ! . on ( 'connect' , onConnect ) . on ( 'close' , onClose ) . on ( 'error' , onError ) ; this . _attemptConnection ( ... options ) ; } ) ; } private async _handshake ( ) { await new Promise ( ( resolve , reject ) => { let timeout ; if ( this . client . handshakeTimeout !== - 1 ) { timeout = setTimeout ( ( ) => { onError ( new Error ( 'Connection Timed Out.' ) ) ; this . socket ! . destroy ( ) ; } , this . client . handshakeTimeout ) ; } const onData = ( message ) => { try { const name = deserialize ( message , 11 ) ; if ( typeof name === 'string' ) { const previous = this . name ; this . name = name ; if ( previous && previous !== name ) this . client . servers . delete ( previous ) ; this . socket ! . write ( createFromID ( readID ( message ) , false , serialize ( this . client . name ) ) ) ; return resolve ( cleanup ( ) ) ; } } catch { } onError ( new Error ( 'Unexpected response from the server.' ) ) ; this . socket ! . destroy ( ) ; } ; const onClose = ( ) => reject ( cleanup ( this ) ) ; const onError = ( error ) => reject ( cleanup ( error ) ) ; const cleanup = < T = unknown > ( value ? ) => { this . socket ! . off ( 'data' , onData ) ; this . socket ! . off ( 'close' , onClose ) ; this . socket ! . off ( 'error' , onError ) ; if ( timeout ) clearTimeout ( timeout ) ; return value ; } ; this . socket ! . on ( 'data' , onData ) . on ( 'close' , onClose ) . on ( 'error' , onError ) ; } ) ; } private _attemptConnection ( ... options ) { this . status = ClientSocketStatus . Connecting ; this . socket ! . connect ( ... options ) ; this . client . emit ( 'connecting' , this ) ; } private _emitConnect ( ) { if ( this . status !== ClientSocketStatus . Connected ) { this . status = ClientSocketStatus . Connected ; this . client . emit ( 'connect' , this ) ; } } }