183 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			183 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { useEffect } from 'react'
 | 
						|
import ReconnectingWebSocket, { CloseEvent } from 'reconnecting-websocket'
 | 
						|
import { proxy, ref, useSnapshot } from 'valtio'
 | 
						|
import { subscribeKey } from 'valtio/utils'
 | 
						|
import { Select } from '.'
 | 
						|
import state, { ILog, transactionsState } from '../state'
 | 
						|
import { extractJSON } from '../utils/json'
 | 
						|
import EnrichLog from './EnrichLog'
 | 
						|
import LogBox from './LogBox'
 | 
						|
 | 
						|
interface ISelect<T = string> {
 | 
						|
  label: string
 | 
						|
  value: T
 | 
						|
}
 | 
						|
 | 
						|
export interface IStreamState {
 | 
						|
  selectedAccount: ISelect | null
 | 
						|
  status: 'idle' | 'opened' | 'closed'
 | 
						|
  statusChangeTimestamp?: number
 | 
						|
  logs: ILog[]
 | 
						|
  socket?: ReconnectingWebSocket
 | 
						|
}
 | 
						|
 | 
						|
export const streamState = proxy<IStreamState>({
 | 
						|
  selectedAccount: null as ISelect | null,
 | 
						|
  status: 'idle',
 | 
						|
  logs: [] as ILog[]
 | 
						|
})
 | 
						|
 | 
						|
const onOpen = (account: ISelect | null) => {
 | 
						|
  if (!account) {
 | 
						|
    return
 | 
						|
  }
 | 
						|
  // streamState.logs = [];
 | 
						|
  streamState.status = 'opened'
 | 
						|
  streamState.statusChangeTimestamp = Date.now()
 | 
						|
 | 
						|
  pushLog(`Debug stream opened for account ${account?.value}`, {
 | 
						|
    type: 'success'
 | 
						|
  })
 | 
						|
}
 | 
						|
const onError = () => {
 | 
						|
  pushLog('Something went wrong! Check your connection and try again.', {
 | 
						|
    type: 'error'
 | 
						|
  })
 | 
						|
}
 | 
						|
const onClose = (e: CloseEvent) => {
 | 
						|
  // 999 = closed websocket connection by switching account
 | 
						|
  if (e.code !== 4999) {
 | 
						|
    pushLog(`Connection was closed. [code: ${e.code}]`, {
 | 
						|
      type: 'error'
 | 
						|
    })
 | 
						|
  }
 | 
						|
  streamState.status = 'closed'
 | 
						|
  streamState.statusChangeTimestamp = Date.now()
 | 
						|
}
 | 
						|
const onMessage = (event: any) => {
 | 
						|
  // Ping returns just account address, if we get that
 | 
						|
  // response we don't need to log anything
 | 
						|
  if (event.data !== streamState.selectedAccount?.value) {
 | 
						|
    pushLog(event.data)
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
let interval: NodeJS.Timer | null = null
 | 
						|
 | 
						|
const addListeners = (account: ISelect | null) => {
 | 
						|
  if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
 | 
						|
    return
 | 
						|
  }
 | 
						|
  streamState.logs = []
 | 
						|
  if (account?.value) {
 | 
						|
    if (interval) {
 | 
						|
      clearInterval(interval)
 | 
						|
    }
 | 
						|
    if (streamState.socket) {
 | 
						|
      streamState.socket?.removeEventListener('open', () => onOpen(account))
 | 
						|
      streamState.socket?.removeEventListener('close', onClose)
 | 
						|
      streamState.socket?.removeEventListener('error', onError)
 | 
						|
      streamState.socket?.removeEventListener('message', onMessage)
 | 
						|
    }
 | 
						|
 | 
						|
    streamState.socket = ref(
 | 
						|
      new ReconnectingWebSocket(
 | 
						|
        `wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
 | 
						|
      )
 | 
						|
    )
 | 
						|
    if (streamState.socket) {
 | 
						|
      interval = setInterval(() => {
 | 
						|
        streamState.socket?.send('')
 | 
						|
      }, 45000)
 | 
						|
    }
 | 
						|
 | 
						|
    streamState.socket.addEventListener('open', () => onOpen(account))
 | 
						|
    streamState.socket.addEventListener('close', onClose)
 | 
						|
    streamState.socket.addEventListener('error', onError)
 | 
						|
    streamState.socket.addEventListener('message', onMessage)
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
subscribeKey(streamState, 'selectedAccount', addListeners)
 | 
						|
 | 
						|
const clearLog = () => {
 | 
						|
  streamState.logs = []
 | 
						|
  streamState.statusChangeTimestamp = Date.now()
 | 
						|
}
 | 
						|
 | 
						|
const DebugStream = () => {
 | 
						|
  const { selectedAccount, logs } = useSnapshot(streamState)
 | 
						|
  const { activeHeader: activeTxTab } = useSnapshot(transactionsState)
 | 
						|
  const { accounts } = useSnapshot(state)
 | 
						|
 | 
						|
  const accountOptions = accounts.map(acc => ({
 | 
						|
    label: acc.name,
 | 
						|
    value: acc.address
 | 
						|
  }))
 | 
						|
 | 
						|
  const renderNav = () => (
 | 
						|
    <>
 | 
						|
      <Select
 | 
						|
        instanceId="DSAccount"
 | 
						|
        placeholder="Select account"
 | 
						|
        options={accountOptions}
 | 
						|
        hideSelectedOptions
 | 
						|
        value={selectedAccount}
 | 
						|
        onChange={acc => {
 | 
						|
          streamState.socket?.close(4999, 'Old connection closed because user switched account')
 | 
						|
          streamState.selectedAccount = acc as any
 | 
						|
        }}
 | 
						|
        css={{ width: '100%' }}
 | 
						|
      />
 | 
						|
    </>
 | 
						|
  )
 | 
						|
 | 
						|
  useEffect(() => {
 | 
						|
    const account = transactionsState.transactions.find(tx => tx.header === activeTxTab)?.state
 | 
						|
      .selectedAccount
 | 
						|
 | 
						|
    if (account && account.value !== streamState.selectedAccount?.value)
 | 
						|
      streamState.selectedAccount = account
 | 
						|
  }, [activeTxTab])
 | 
						|
 | 
						|
  return (
 | 
						|
    <LogBox enhanced renderNav={renderNav} title="Debug stream" logs={logs} clearLog={clearLog} />
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
export default DebugStream
 | 
						|
 | 
						|
export const pushLog = (str: any, opts: Partial<Pick<ILog, 'type'>> = {}): ILog | undefined => {
 | 
						|
  if (!str) return
 | 
						|
  if (typeof str !== 'string') throw Error('Unrecognized debug log stream!')
 | 
						|
 | 
						|
  const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m)
 | 
						|
  const [_, tm, msg] = match || []
 | 
						|
 | 
						|
  const timestamp = Date.parse(tm || '') || undefined
 | 
						|
  const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString()
 | 
						|
 | 
						|
  const extracted = extractJSON(msg)
 | 
						|
  const _message = !extracted ? msg : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1)
 | 
						|
  const message = ref(<EnrichLog str={_message} />)
 | 
						|
 | 
						|
  const _jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined
 | 
						|
  const jsonData = _jsonData ? ref(<EnrichLog str={_jsonData} />) : undefined
 | 
						|
 | 
						|
  if (extracted?.result?.id?._Request?.includes('hooks-builder-req')) {
 | 
						|
    return
 | 
						|
  }
 | 
						|
 | 
						|
  const { type = 'log' } = opts
 | 
						|
  const log: ILog = {
 | 
						|
    type,
 | 
						|
    message,
 | 
						|
    timestring,
 | 
						|
    jsonData,
 | 
						|
    defaultCollapsed: true
 | 
						|
  }
 | 
						|
 | 
						|
  if (log) streamState.logs.push(log)
 | 
						|
  return log
 | 
						|
}
 |