first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
import React from 'react'
import classnames from 'classnames'
import type { ConnectionStatus } from './types'
import { Badge, Button } from 'react-bootstrap-5'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import MaterialIcon from '@/shared/components/material-icon'
const variants = {
connected: 'success',
connecting: 'warning',
disconnected: 'danger',
}
export const ConnectionBadge = ({ state }: { state: ConnectionStatus }) => (
<Badge className="px-2 py-1" bg={variants[state]}>
{state}
</Badge>
)
export const DiagnosticItem = ({
icon,
label,
value,
type,
}: {
icon: string
label: string
value: React.ReactNode
type?: 'success' | 'danger'
}) => (
<div
className={classnames(
'py-2',
type === 'success' && 'text-success',
type === 'danger' && 'text-danger'
)}
>
<div className="d-flex gap-2 fw-bold align-items-center">
<MaterialIcon type={icon} />
<span>{label}</span>
</div>
<div>{value}</div>
</div>
)
export function ErrorAlert({ message }: { message: string }) {
return <OLNotification type="error" content={message} className="mt-3" />
}
export function ActionButton({
label,
icon,
onClick,
disabled,
}: {
label: string
icon: string
onClick: () => void
disabled?: boolean
}) {
return (
<Button
onClick={onClick}
disabled={disabled}
className="d-flex align-items-center"
>
<MaterialIcon className="me-2" type={icon} />
<span>{label}</span>
</Button>
)
}

View File

@@ -0,0 +1,230 @@
import React, { useEffect } from 'react'
import type { ConnectionStatus } from './types'
import { useSocketManager } from './use-socket-manager'
import {
ActionButton,
ConnectionBadge,
DiagnosticItem,
ErrorAlert,
} from './diagnostic-component'
import { Col, Container, Row } from 'react-bootstrap-5'
import MaterialIcon from '@/shared/components/material-icon'
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
type NetworkInformation = {
downlink: number
effectiveType: string
rtt: number
saveData: boolean
type: string
}
const navigatorInfo = (): string[] => {
if (!('connection' in navigator)) {
return ['Network Information API not supported']
}
const connection = navigator.connection as NetworkInformation
return [
`Downlink: ${connection.downlink} Mbps`,
`Effective Type: ${connection.effectiveType}`,
`Round Trip Time: ${connection.rtt} ms`,
`Save Data: ${connection.saveData ? 'Enabled' : 'Disabled'}`,
`Platform: ${navigator.platform}`,
// @ts-ignore
`Device Memory: ${navigator.deviceMemory}`,
`Hardware Concurrency: ${navigator.hardwareConcurrency}`,
]
}
const useCurrentTime = () => {
const [, setTime] = React.useState(new Date())
useEffect(() => {
const interval = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(interval)
}, [])
}
type DiagnosticProps = {
icon: string
label: string
text: string[]
type?: 'success' | 'danger'
}
export const SocketDiagnostics = () => {
const {
socketState,
debugInfo,
disconnectSocket,
forceReconnect,
socket,
autoping,
setAutoping,
} = useSocketManager()
useCurrentTime()
const now = new Date()
const getConnectionState = (): ConnectionStatus => {
if (socketState.connected) return 'connected'
if (socketState.connecting) return 'connecting'
return 'disconnected'
}
const lastReceivedS = debugInfo.lastReceived
? Math.round((now.getTime() - debugInfo.lastReceived) / 1000)
: null
const isLate =
!!debugInfo.unansweredSince &&
now.getTime() - debugInfo.unansweredSince >= 3000
const diagnosticProps: DiagnosticProps[] = [
{
icon: 'network_ping',
label: 'Ping Count',
text: [
`${debugInfo.received} / ${debugInfo.sent}`,
lastReceivedS !== null ? `Last received ${lastReceivedS}s ago` : null,
].filter(Boolean) as string[],
type: isLate === null ? undefined : isLate ? 'danger' : 'success',
},
{
icon: 'schedule',
label: 'Latency',
text: [
debugInfo.latency
? `${debugInfo.latency} ms\nMax: ${debugInfo.maxLatency} ms`
: '-',
],
type: debugInfo.latency && debugInfo.latency < 450 ? 'success' : 'danger',
},
{
icon: 'difference',
label: 'Clock Delta',
text: [
debugInfo.clockDelta === null
? '-'
: `${Math.round(debugInfo.clockDelta / 1000)}s`,
],
type:
debugInfo.clockDelta !== null && Math.abs(debugInfo.clockDelta) < 1500
? 'success'
: 'danger',
},
{
icon: 'signal_cellular_alt',
label: 'Online',
text: [debugInfo.onLine?.toString() ?? '-'],
type: debugInfo.onLine ? 'success' : 'danger',
},
{
icon: 'schedule',
label: 'Current time',
text: [now.toUTCString()],
},
{
icon: 'hourglass',
label: 'Connection time',
text: [
debugInfo.client?.connectedAt
? `${new Date(debugInfo.client.connectedAt).toUTCString()} (${Math.round(
(Date.now() - debugInfo.client.connectedAt) / 1000
)}s)`
: '-',
],
},
{
icon: 'local_shipping',
label: 'Transport',
text: [socket?.socket.transport?.name ?? '-'],
},
{
icon: 'badge',
label: 'Client Public ID',
text: [debugInfo.client?.publicId ?? '-'],
},
{
icon: 'pin',
label: 'IP Address',
text: [debugInfo.client?.remoteIp ?? '-'],
},
{
icon: 'web',
label: 'User agent',
text: [debugInfo.client?.userAgent ?? '-'],
},
{
icon: 'directions_boat',
label: 'Navigator info',
text: navigatorInfo(),
},
]
const diagnosticItems = diagnosticProps.map(item => (
<DiagnosticItem
key={item.label}
icon={item.icon}
label={item.label}
value={item.text.map((t, i) => (
<div key={i}>{t}</div>
))}
type={item.type}
/>
))
const cutAtIndex = 7
const leftItems = diagnosticItems.slice(0, cutAtIndex)
const rightItems = diagnosticItems.slice(cutAtIndex)
return (
<Container>
<h1>Socket Diagnostics</h1>
<ConnectionBadge state={getConnectionState()} />
<div className="d-flex flex-wrap gap-4 mt-3 align-items-center">
<ActionButton
label="Reconnect"
icon="refresh"
onClick={forceReconnect}
/>
<ActionButton
label="Disconnect"
icon="close"
onClick={disconnectSocket}
disabled={!socketState.connected}
/>
<OLFormCheckbox
label="Auto ping"
id="autoping"
checked={autoping}
onChange={e => setAutoping(e.target.checked)}
/>
</div>
{socketState.lastError && <ErrorAlert message={socketState.lastError} />}
<div className="card p-4 mt-3">
<div className="d-flex flex-wrap gap-4 row-gap-1 justify-content-between align-items-center">
<h3 className="text-lg">
<MaterialIcon type="speed" /> Connection Stats
</h3>
<div className="ms-auto">
<CopyToClipboard
content={diagnosticProps
.map(item => [`${item.label}:`, ...item.text].join('\n'))
.join('\n\n')}
tooltipId="copy-debug-info"
kind="text"
/>
</div>
</div>
<Row>
<Col md={6}>{leftItems}</Col>
<Col md={6}>{rightItems}</Col>
</Row>
</div>
</Container>
)
}

View File

@@ -0,0 +1,30 @@
export interface SocketState {
connected: boolean
connecting: boolean
lastError: string
}
export interface DebugInfo {
sent: number
received: number
latency: number | null
maxLatency: number | null
clockDelta: number | null
onLine: boolean | null
client: Client | null
unansweredSince: number | null
lastReceived: number | null
}
interface Client {
id: string
publicId: string
remoteIp: string
userAgent: string
connected: boolean
readable: boolean
ackPackets: number
connectedAt: number
}
export type ConnectionStatus = 'connected' | 'connecting' | 'disconnected'

View File

@@ -0,0 +1,170 @@
import { useState, useEffect, useCallback } from 'react'
import SocketIoShim from '@/ide/connection/SocketIoShim'
import type { Socket } from '@/features/ide-react/connection/types/socket'
import type { DebugInfo, SocketState } from './types'
export function useSocketManager() {
const [socket, setSocket] = useState<Socket | null>(null)
const [autoping, setAutoping] = useState<boolean>(false)
const [socketState, setSocketState] = useState<SocketState>({
connected: false,
connecting: false,
lastError: '',
})
const [debugInfo, setDebugInfo] = useState<DebugInfo>({
sent: 0,
received: 0,
latency: null,
maxLatency: null,
onLine: null,
clockDelta: null,
client: null,
unansweredSince: null,
lastReceived: null,
})
const connectSocket = useCallback(() => {
const parsedURL = new URL('/socket.io', window.origin)
setSocketState(prev => ({
...prev,
connecting: true,
lastAttempt: Date.now(),
}))
const newSocket = SocketIoShim.connect(parsedURL.origin, {
resource: parsedURL.pathname.slice(1),
'auto connect': false,
'connect timeout': 30 * 1000,
'force new connection': true,
query: new URLSearchParams({ debugging: 'true' }).toString(),
reconnect: false,
}) as unknown as Socket
setSocket(newSocket)
return newSocket
}, [])
const disconnectSocket = useCallback(() => {
socket?.disconnect()
setSocket(null)
setSocketState(prev => ({
...prev,
connected: false,
connecting: false,
lastError: 'Manually disconnected',
}))
}, [socket])
const forceReconnect = useCallback(() => {
disconnectSocket()
setTimeout(connectSocket, 1000)
}, [disconnectSocket, connectSocket])
useEffect(() => {
connectSocket()
}, [connectSocket])
const sendPing = useCallback(() => {
if (socket?.socket.connected) {
const time = Date.now()
setDebugInfo(prev => ({
...prev,
sent: prev.sent + 1,
unansweredSince: prev.unansweredSince ?? time,
}))
socket.emit('debug', { time }, (info: any) => {
const beforeTime = info.data.time
const now = Date.now()
const latency = now - beforeTime
const clockDelta = (beforeTime + beforeTime) / 2 - info.serverTime
setDebugInfo(prev => ({
...prev,
received: prev.received + 1,
latency,
maxLatency: Math.max(prev.maxLatency ?? 0, latency),
clockDelta,
client: info.client,
lastReceived: now,
unansweredSince: null,
}))
})
}
}, [socket])
useEffect(() => {
if (!socket || !autoping) return
const statsInterval = setInterval(sendPing, 2000)
return () => {
clearInterval(statsInterval)
}
}, [socket, autoping, sendPing])
useEffect(() => {
if (!socket) return
socket.on('connect', () => {
setSocketState(prev => ({
...prev,
connected: true,
connecting: false,
lastSuccess: Date.now(),
lastError: '',
}))
sendPing()
})
socket.on('disconnect', (reason: string) => {
setSocketState(prev => ({
...prev,
connected: false,
connecting: false,
lastError: `Disconnected: ${reason}`,
}))
})
socket.on('connect_error', (error: Error) => {
setSocketState(prev => ({
...prev,
connecting: false,
lastError: `Connection error: ${error?.message || 'Unknown'}`,
}))
})
socket.socket.connect()
return () => {
socket.disconnect()
}
}, [sendPing, socket])
useEffect(() => {
const updateNetworkInfo = () => {
setDebugInfo(prev => ({ ...prev, onLine: navigator.onLine }))
}
window.addEventListener('online', updateNetworkInfo)
window.addEventListener('offline', updateNetworkInfo)
updateNetworkInfo()
return () => {
window.removeEventListener('online', updateNetworkInfo)
window.removeEventListener('offline', updateNetworkInfo)
}
}, [])
return {
socketState,
debugInfo,
connectSocket,
disconnectSocket,
forceReconnect,
socket,
autoping,
setAutoping,
}
}