create-fluxstack 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LLMD/INDEX.md +4 -3
- package/LLMD/resources/live-binary-delta.md +507 -0
- package/LLMD/resources/live-components.md +208 -12
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +11 -0
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +19 -7
- package/app/client/src/components/AppLayout.tsx +18 -10
- package/app/client/src/live/PingPongDemo.tsx +199 -0
- package/app/client/src/live/RoomChatDemo.tsx +187 -22
- package/app/client/src/live/SharedCounterDemo.tsx +142 -0
- package/app/server/auth/DevAuthProvider.ts +2 -2
- package/app/server/auth/JWTAuthProvider.example.ts +2 -2
- package/app/server/index.ts +2 -2
- package/app/server/live/LiveAdminPanel.ts +1 -1
- package/app/server/live/LivePingPong.ts +61 -0
- package/app/server/live/LiveProtectedChat.ts +1 -1
- package/app/server/live/LiveRoomChat.ts +106 -38
- package/app/server/live/LiveSharedCounter.ts +73 -0
- package/app/server/live/rooms/ChatRoom.ts +68 -0
- package/app/server/live/rooms/CounterRoom.ts +51 -0
- package/app/server/live/rooms/DirectoryRoom.ts +42 -0
- package/app/server/live/rooms/PingRoom.ts +40 -0
- package/app/server/routes/room.routes.ts +1 -2
- package/core/build/live-components-generator.ts +11 -2
- package/core/build/vite-plugins.ts +28 -0
- package/core/client/hooks/useLiveUpload.ts +3 -4
- package/core/client/index.ts +25 -35
- package/core/framework/server.ts +1 -1
- package/core/server/index.ts +1 -2
- package/core/server/live/auto-generated-components.ts +5 -8
- package/core/server/live/index.ts +90 -21
- package/core/server/live/websocket-plugin.ts +54 -1079
- package/core/types/types.ts +76 -1025
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +100 -95
- package/plugins/crypto-auth/index.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
- package/tsconfig.json +4 -1
- package/vite.config.ts +40 -12
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/server/live/LiveChat.ts +0 -78
- package/core/client/LiveComponentsProvider.tsx +0 -531
- package/core/client/components/Live.tsx +0 -111
- package/core/client/components/LiveDebugger.tsx +0 -1324
- package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
- package/core/client/hooks/state-validator.ts +0 -130
- package/core/client/hooks/useChunkedUpload.ts +0 -359
- package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
- package/core/client/hooks/useLiveComponent.ts +0 -853
- package/core/client/hooks/useLiveDebugger.ts +0 -392
- package/core/client/hooks/useRoom.ts +0 -409
- package/core/client/hooks/useRoomProxy.ts +0 -382
- package/core/server/live/ComponentRegistry.ts +0 -1128
- package/core/server/live/FileUploadManager.ts +0 -446
- package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
- package/core/server/live/LiveDebugger.ts +0 -462
- package/core/server/live/LiveLogger.ts +0 -144
- package/core/server/live/LiveRoomManager.ts +0 -278
- package/core/server/live/RoomEventBus.ts +0 -234
- package/core/server/live/RoomStateManager.ts +0 -172
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +0 -705
- package/core/server/live/WebSocketConnectionManager.ts +0 -710
- package/core/server/live/auth/LiveAuthContext.ts +0 -71
- package/core/server/live/auth/LiveAuthManager.ts +0 -304
- package/core/server/live/auth/index.ts +0 -19
- package/core/server/live/auth/types.ts +0 -179
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class LiveRoomChat {
|
|
2
|
+
static componentName = 'LiveRoomChat'
|
|
3
|
+
static defaultState = {
|
|
4
|
+
username: '',
|
|
5
|
+
activeRoom: null,
|
|
6
|
+
rooms: [],
|
|
7
|
+
messages: {},
|
|
8
|
+
customRooms: []
|
|
9
|
+
}
|
|
10
|
+
static publicActions = ['createRoom', 'joinRoom', 'leaveRoom', 'switchRoom', 'sendMessage', 'setUsername']
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class LiveUpload {
|
|
2
|
+
static componentName = 'LiveUpload'
|
|
3
|
+
static defaultState = {
|
|
4
|
+
status: 'idle',
|
|
5
|
+
progress: 0,
|
|
6
|
+
fileName: '',
|
|
7
|
+
fileSize: 0,
|
|
8
|
+
fileType: '',
|
|
9
|
+
fileUrl: '',
|
|
10
|
+
bytesUploaded: 0,
|
|
11
|
+
totalBytes: 0,
|
|
12
|
+
error: null
|
|
13
|
+
}
|
|
14
|
+
static publicActions = ['startUpload', 'updateProgress', 'completeUpload', 'failUpload', 'reset']
|
|
15
|
+
}
|
package/app/client/src/App.tsx
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { Routes, Route } from 'react-router'
|
|
3
3
|
import { api } from './lib/eden-api'
|
|
4
|
-
import { LiveComponentsProvider
|
|
4
|
+
import { LiveComponentsProvider } from '@/core/client'
|
|
5
5
|
import { FormDemo } from './live/FormDemo'
|
|
6
6
|
import { CounterDemo } from './live/CounterDemo'
|
|
7
7
|
import { UploadDemo } from './live/UploadDemo'
|
|
8
|
-
import { ChatDemo } from './live/ChatDemo'
|
|
9
8
|
import { RoomChatDemo } from './live/RoomChatDemo'
|
|
9
|
+
import { SharedCounterDemo } from './live/SharedCounterDemo'
|
|
10
10
|
import { AuthDemo } from './live/AuthDemo'
|
|
11
|
+
import { PingPongDemo } from './live/PingPongDemo'
|
|
11
12
|
import { AppLayout } from './components/AppLayout'
|
|
12
13
|
import { DemoPage } from './components/DemoPage'
|
|
13
14
|
import { HomePage } from './pages/HomePage'
|
|
@@ -110,10 +111,12 @@ function AppContent() {
|
|
|
110
111
|
}
|
|
111
112
|
/>
|
|
112
113
|
<Route
|
|
113
|
-
path="/
|
|
114
|
+
path="/shared-counter"
|
|
114
115
|
element={
|
|
115
|
-
<DemoPage
|
|
116
|
-
<
|
|
116
|
+
<DemoPage
|
|
117
|
+
note={<>Contador compartilhado usando <code className="text-purple-400">LiveRoom</code> - abra em varias abas!</>}
|
|
118
|
+
>
|
|
119
|
+
<SharedCounterDemo />
|
|
117
120
|
</DemoPage>
|
|
118
121
|
}
|
|
119
122
|
/>
|
|
@@ -121,7 +124,7 @@ function AppContent() {
|
|
|
121
124
|
path="/room-chat"
|
|
122
125
|
element={
|
|
123
126
|
<DemoPage
|
|
124
|
-
note={
|
|
127
|
+
note={<>Chat com múltiplas salas usando o sistema <code className="text-purple-400">$room</code>.</>}
|
|
125
128
|
>
|
|
126
129
|
<RoomChatDemo />
|
|
127
130
|
</DemoPage>
|
|
@@ -137,6 +140,16 @@ function AppContent() {
|
|
|
137
140
|
</DemoPage>
|
|
138
141
|
}
|
|
139
142
|
/>
|
|
143
|
+
<Route
|
|
144
|
+
path="/ping-pong"
|
|
145
|
+
element={
|
|
146
|
+
<DemoPage
|
|
147
|
+
note={<>Latency demo com <code className="text-cyan-400">msgpack</code> binary codec - mensagens binárias no WebSocket!</>}
|
|
148
|
+
>
|
|
149
|
+
<PingPongDemo />
|
|
150
|
+
</DemoPage>
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
140
153
|
<Route path="*" element={<HomePage apiStatus={apiStatus} />} />
|
|
141
154
|
</Route>
|
|
142
155
|
</Routes>
|
|
@@ -153,7 +166,6 @@ function App() {
|
|
|
153
166
|
debug={false}
|
|
154
167
|
>
|
|
155
168
|
<AppContent />
|
|
156
|
-
{import.meta.env.DEV && <LiveDebugger />}
|
|
157
169
|
</LiveComponentsProvider>
|
|
158
170
|
)
|
|
159
171
|
}
|
|
@@ -9,9 +9,10 @@ const navItems = [
|
|
|
9
9
|
{ to: '/counter', label: 'Counter' },
|
|
10
10
|
{ to: '/form', label: 'Form' },
|
|
11
11
|
{ to: '/upload', label: 'Upload' },
|
|
12
|
-
{ to: '/
|
|
12
|
+
{ to: '/shared-counter', label: 'Shared Counter' },
|
|
13
13
|
{ to: '/room-chat', label: 'Room Chat' },
|
|
14
14
|
{ to: '/auth', label: 'Auth' },
|
|
15
|
+
{ to: '/ping-pong', label: 'Ping Pong' },
|
|
15
16
|
{ to: '/api-test', label: 'API Test' }
|
|
16
17
|
]
|
|
17
18
|
|
|
@@ -20,12 +21,16 @@ const routeFlameHue: Record<string, string> = {
|
|
|
20
21
|
'/counter': '180deg', // ciano
|
|
21
22
|
'/form': '300deg', // rosa
|
|
22
23
|
'/upload': '60deg', // amarelo
|
|
23
|
-
'/
|
|
24
|
+
'/shared-counter': '120deg', // verde
|
|
24
25
|
'/room-chat': '240deg', // azul
|
|
25
26
|
'/auth': '330deg', // vermelho
|
|
27
|
+
'/ping-pong': '200deg', // ciano-azul
|
|
26
28
|
'/api-test': '90deg', // lima
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
// Cache favicon blob URLs by hue to avoid recreating blobs on every navigation
|
|
32
|
+
const faviconUrlCache = new Map<string, string>()
|
|
33
|
+
|
|
29
34
|
export function AppLayout() {
|
|
30
35
|
const location = useLocation()
|
|
31
36
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
@@ -34,14 +39,18 @@ export function AppLayout() {
|
|
|
34
39
|
const current = navItems.find(item => item.to === location.pathname)
|
|
35
40
|
document.title = current ? `${current.label} - FluxStack` : 'FluxStack'
|
|
36
41
|
|
|
37
|
-
// Dynamic favicon with hue-rotate
|
|
42
|
+
// Dynamic favicon with hue-rotate (cached per hue value)
|
|
38
43
|
const hue = routeFlameHue[location.pathname] || '0deg'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
let url = faviconUrlCache.get(hue)
|
|
45
|
+
if (!url) {
|
|
46
|
+
const colored = faviconSvg.replace(
|
|
47
|
+
'<svg ',
|
|
48
|
+
`<svg style="filter: hue-rotate(${hue})" `
|
|
49
|
+
)
|
|
50
|
+
const blob = new Blob([colored], { type: 'image/svg+xml' })
|
|
51
|
+
url = URL.createObjectURL(blob)
|
|
52
|
+
faviconUrlCache.set(hue, url)
|
|
53
|
+
}
|
|
45
54
|
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
|
46
55
|
if (!link) {
|
|
47
56
|
link = document.createElement('link')
|
|
@@ -50,7 +59,6 @@ export function AppLayout() {
|
|
|
50
59
|
}
|
|
51
60
|
link.type = 'image/svg+xml'
|
|
52
61
|
link.href = url
|
|
53
|
-
return () => URL.revokeObjectURL(url)
|
|
54
62
|
}, [location.pathname])
|
|
55
63
|
|
|
56
64
|
return (
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// PingPongDemo - Demo de Binary Codec (msgpack)
|
|
2
|
+
//
|
|
3
|
+
// Mostra latencia round-trip de mensagens binárias.
|
|
4
|
+
// Cada ping viaja como msgpack binário pelo WebSocket.
|
|
5
|
+
// Abra em varias abas para ver o onlineCount e totalPings compartilhados.
|
|
6
|
+
|
|
7
|
+
import { useMemo, useState, useEffect, useRef, useCallback } from 'react'
|
|
8
|
+
import { Live } from '@/core/client'
|
|
9
|
+
import { LivePingPong } from '@server/live/LivePingPong'
|
|
10
|
+
import type { PingRoom } from '@server/live/rooms/PingRoom'
|
|
11
|
+
|
|
12
|
+
interface PingEntry {
|
|
13
|
+
seq: number
|
|
14
|
+
sentAt: number
|
|
15
|
+
rtt: number | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function PingPongDemo() {
|
|
19
|
+
const username = useMemo(() => {
|
|
20
|
+
const adj = ['Swift', 'Rapid', 'Quick', 'Turbo', 'Flash'][Math.floor(Math.random() * 5)]
|
|
21
|
+
const noun = ['Ping', 'Bolt', 'Wave', 'Pulse', 'Beam'][Math.floor(Math.random() * 5)]
|
|
22
|
+
return `${adj}${noun}${Math.floor(Math.random() * 100)}`
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
const live = Live.use(LivePingPong, {
|
|
26
|
+
initialState: { ...LivePingPong.defaultState, username },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const [pings, setPings] = useState<PingEntry[]>([])
|
|
30
|
+
const [avgRtt, setAvgRtt] = useState<number | null>(null)
|
|
31
|
+
const [minRtt, setMinRtt] = useState<number | null>(null)
|
|
32
|
+
const [maxRtt, setMaxRtt] = useState<number | null>(null)
|
|
33
|
+
const seqRef = useRef(0)
|
|
34
|
+
const pendingRef = useRef<Map<number, number>>(new Map())
|
|
35
|
+
|
|
36
|
+
// Listen for pong events (binary msgpack from server)
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const unsub = live.$room<PingRoom>('ping:global').on('pong', (data) => {
|
|
39
|
+
const sentAt = pendingRef.current.get(data.seq)
|
|
40
|
+
if (sentAt == null) return
|
|
41
|
+
pendingRef.current.delete(data.seq)
|
|
42
|
+
|
|
43
|
+
const rtt = Date.now() - sentAt
|
|
44
|
+
|
|
45
|
+
setPings(prev => {
|
|
46
|
+
const updated = [{ seq: data.seq, sentAt, rtt }, ...prev].slice(0, 20)
|
|
47
|
+
// Compute stats
|
|
48
|
+
const rtts = updated.filter(p => p.rtt != null).map(p => p.rtt!)
|
|
49
|
+
if (rtts.length > 0) {
|
|
50
|
+
setAvgRtt(Math.round(rtts.reduce((a, b) => a + b, 0) / rtts.length))
|
|
51
|
+
setMinRtt(Math.min(...rtts))
|
|
52
|
+
setMaxRtt(Math.max(...rtts))
|
|
53
|
+
}
|
|
54
|
+
return updated
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
return unsub
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
const sendPing = useCallback(() => {
|
|
61
|
+
const seq = ++seqRef.current
|
|
62
|
+
pendingRef.current.set(seq, Date.now())
|
|
63
|
+
live.ping({ seq })
|
|
64
|
+
}, [live])
|
|
65
|
+
|
|
66
|
+
// Auto-ping mode
|
|
67
|
+
const [autoPing, setAutoPing] = useState(false)
|
|
68
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (autoPing && live.$connected) {
|
|
72
|
+
intervalRef.current = setInterval(sendPing, 500)
|
|
73
|
+
}
|
|
74
|
+
return () => {
|
|
75
|
+
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
76
|
+
}
|
|
77
|
+
}, [autoPing, live.$connected, sendPing])
|
|
78
|
+
|
|
79
|
+
const onlineCount = live.$state.onlineCount
|
|
80
|
+
const totalPings = live.$state.totalPings
|
|
81
|
+
const lastPingBy = live.$state.lastPingBy
|
|
82
|
+
|
|
83
|
+
const rttColor = (rtt: number) => {
|
|
84
|
+
if (rtt < 10) return 'text-emerald-400'
|
|
85
|
+
if (rtt < 50) return 'text-yellow-400'
|
|
86
|
+
return 'text-red-400'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex flex-col items-center gap-8 w-full max-w-lg mx-auto">
|
|
91
|
+
{/* Header */}
|
|
92
|
+
<div className="text-center">
|
|
93
|
+
<h2 className="text-2xl font-bold text-white mb-2">Ping Pong Binary</h2>
|
|
94
|
+
<p className="text-sm text-gray-400">
|
|
95
|
+
Mensagens binárias via <code className="text-cyan-400">msgpack</code> — round-trip latency demo
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Status bar */}
|
|
100
|
+
<div className="flex items-center gap-4 flex-wrap justify-center">
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
102
|
+
<div className={`w-2 h-2 rounded-full ${live.$connected ? 'bg-emerald-400' : 'bg-red-400'}`} />
|
|
103
|
+
<span className="text-sm text-gray-400">{live.$connected ? 'Conectado' : 'Desconectado'}</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10">
|
|
106
|
+
<span className="text-sm text-gray-400">{onlineCount} online</span>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="px-3 py-1 rounded-full bg-cyan-500/10 border border-cyan-500/20">
|
|
109
|
+
<span className="text-xs text-cyan-300">{username}</span>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Stats */}
|
|
114
|
+
<div className="grid grid-cols-3 gap-4 w-full">
|
|
115
|
+
<div className="bg-gray-800/50 border border-white/10 rounded-xl p-4 text-center">
|
|
116
|
+
<div className="text-2xl font-bold text-white tabular-nums">
|
|
117
|
+
{avgRtt != null ? `${avgRtt}ms` : '--'}
|
|
118
|
+
</div>
|
|
119
|
+
<div className="text-xs text-gray-500 mt-1">AVG RTT</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="bg-gray-800/50 border border-white/10 rounded-xl p-4 text-center">
|
|
122
|
+
<div className="text-2xl font-bold text-emerald-400 tabular-nums">
|
|
123
|
+
{minRtt != null ? `${minRtt}ms` : '--'}
|
|
124
|
+
</div>
|
|
125
|
+
<div className="text-xs text-gray-500 mt-1">MIN RTT</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="bg-gray-800/50 border border-white/10 rounded-xl p-4 text-center">
|
|
128
|
+
<div className="text-2xl font-bold text-red-400 tabular-nums">
|
|
129
|
+
{maxRtt != null ? `${maxRtt}ms` : '--'}
|
|
130
|
+
</div>
|
|
131
|
+
<div className="text-xs text-gray-500 mt-1">MAX RTT</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Controls */}
|
|
136
|
+
<div className="flex items-center gap-3">
|
|
137
|
+
<button
|
|
138
|
+
onClick={sendPing}
|
|
139
|
+
disabled={!live.$connected || live.$loading}
|
|
140
|
+
className="px-8 h-14 rounded-2xl bg-cyan-500/20 border border-cyan-500/30 text-cyan-300 text-lg font-bold hover:bg-cyan-500/30 active:scale-95 disabled:opacity-50 transition-all"
|
|
141
|
+
>
|
|
142
|
+
Ping!
|
|
143
|
+
</button>
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => setAutoPing(!autoPing)}
|
|
146
|
+
disabled={!live.$connected}
|
|
147
|
+
className={`px-6 h-14 rounded-2xl border text-sm font-medium transition-all ${
|
|
148
|
+
autoPing
|
|
149
|
+
? 'bg-yellow-500/20 border-yellow-500/30 text-yellow-300 hover:bg-yellow-500/30'
|
|
150
|
+
: 'bg-white/10 border-white/20 text-gray-300 hover:bg-white/20'
|
|
151
|
+
}`}
|
|
152
|
+
>
|
|
153
|
+
{autoPing ? 'Auto ON' : 'Auto OFF'}
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Global stats */}
|
|
158
|
+
<div className="flex items-center gap-6 text-sm text-gray-500">
|
|
159
|
+
<span>Total pings: <span className="text-white font-mono">{totalPings}</span></span>
|
|
160
|
+
{lastPingBy && <span>Ultimo: <span className="text-gray-300">{lastPingBy}</span></span>}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Ping log */}
|
|
164
|
+
<div className="w-full bg-gray-800/30 border border-white/10 rounded-xl overflow-hidden">
|
|
165
|
+
<div className="px-4 py-2 bg-white/5 border-b border-white/10 flex items-center justify-between">
|
|
166
|
+
<span className="text-xs text-gray-400 font-medium">Ping Log</span>
|
|
167
|
+
<span className="text-xs text-gray-600">
|
|
168
|
+
wire format: <code className="text-cyan-400">msgpack</code> (binary)
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
<div className="max-h-60 overflow-y-auto">
|
|
172
|
+
{pings.length === 0 ? (
|
|
173
|
+
<div className="px-4 py-8 text-center text-gray-600 text-sm">
|
|
174
|
+
Clique Ping! para enviar uma mensagem binaria
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
pings.map((p) => (
|
|
178
|
+
<div
|
|
179
|
+
key={p.seq}
|
|
180
|
+
className="px-4 py-2 border-b border-white/5 flex items-center justify-between text-sm"
|
|
181
|
+
>
|
|
182
|
+
<span className="text-gray-500 font-mono">#{p.seq}</span>
|
|
183
|
+
<span className={`font-mono font-bold ${p.rtt != null ? rttColor(p.rtt) : 'text-gray-600'}`}>
|
|
184
|
+
{p.rtt != null ? `${p.rtt}ms` : 'pending...'}
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
))
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Info */}
|
|
193
|
+
<div className="text-center text-xs text-gray-600 space-y-1">
|
|
194
|
+
<p>Powered by <code className="text-purple-400">LiveRoom</code> + <code className="text-cyan-400">msgpack codec</code></p>
|
|
195
|
+
<p>Wire format: binary frames <code className="text-cyan-400">0x02</code> (event) / <code className="text-cyan-400">0x03</code> (state)</p>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|