create-fluxstack 1.20.1 → 1.21.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.
@@ -1,202 +1,180 @@
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 != null) {
76
- clearInterval(intervalRef.current)
77
- intervalRef.current = null
78
- }
79
- }
80
- }, [autoPing, live.$connected, sendPing])
81
-
82
- const onlineCount = live.$state.onlineCount
83
- const totalPings = live.$state.totalPings
84
- const lastPingBy = live.$state.lastPingBy
85
-
86
- const rttColor = (rtt: number) => {
87
- if (rtt < 10) return 'text-emerald-400'
88
- if (rtt < 50) return 'text-yellow-400'
89
- return 'text-red-400'
90
- }
91
-
92
- return (
93
- <div className="flex flex-col items-center gap-8 w-full max-w-lg mx-auto">
94
- {/* Header */}
95
- <div className="text-center">
96
- <h2 className="text-2xl font-bold text-white mb-2">Ping Pong Binary</h2>
97
- <p className="text-sm text-gray-400">
98
- Mensagens binárias via <code className="text-theme-secondary">msgpack</code> — round-trip latency demo
99
- </p>
100
- </div>
101
-
102
- {/* Status bar */}
103
- <div className="flex items-center gap-4 flex-wrap justify-center">
104
- <div className="flex items-center gap-2">
105
- <div className={`w-2 h-2 rounded-full ${live.$connected ? 'bg-emerald-400' : 'bg-red-400'}`} />
106
- <span className="text-sm text-gray-400">{live.$connected ? 'Conectado' : 'Desconectado'}</span>
107
- </div>
108
- <div className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10">
109
- <span className="text-sm text-gray-400">{onlineCount} online</span>
110
- </div>
111
- <div className="px-3 py-1 rounded-full bg-theme-accent border border-theme">
112
- <span className="text-xs text-theme">{username}</span>
113
- </div>
114
- </div>
115
-
116
- {/* Stats */}
117
- <div className="grid grid-cols-3 gap-4 w-full">
118
- <div className="card-theme p-4 text-center">
119
- <div className="text-2xl font-bold text-white tabular-nums">
120
- {avgRtt != null ? `${avgRtt}ms` : '--'}
121
- </div>
122
- <div className="text-xs text-gray-500 mt-1">AVG RTT</div>
123
- </div>
124
- <div className="card-theme p-4 text-center">
125
- <div className="text-2xl font-bold text-emerald-400 tabular-nums">
126
- {minRtt != null ? `${minRtt}ms` : '--'}
127
- </div>
128
- <div className="text-xs text-gray-500 mt-1">MIN RTT</div>
129
- </div>
130
- <div className="card-theme p-4 text-center">
131
- <div className="text-2xl font-bold text-red-400 tabular-nums">
132
- {maxRtt != null ? `${maxRtt}ms` : '--'}
133
- </div>
134
- <div className="text-xs text-gray-500 mt-1">MAX RTT</div>
135
- </div>
136
- </div>
137
-
138
- {/* Controls */}
139
- <div className="flex items-center gap-3">
140
- <button
141
- onClick={sendPing}
142
- disabled={!live.$connected || live.$loading}
143
- className="px-8 h-14 rounded-2xl bg-theme-muted border border-theme-active text-theme text-lg font-bold hover:bg-theme-muted active:scale-95 disabled:opacity-50 transition-all"
144
- >
145
- Ping!
146
- </button>
147
- <button
148
- onClick={() => setAutoPing(!autoPing)}
149
- disabled={!live.$connected}
150
- className={`px-6 h-14 rounded-2xl border text-sm font-medium transition-all ${
151
- autoPing
152
- ? 'bg-yellow-500/20 border-yellow-500/30 text-yellow-300 hover:bg-yellow-500/30'
153
- : 'bg-white/10 border-white/20 text-gray-300 hover:bg-white/20'
154
- }`}
155
- >
156
- {autoPing ? 'Auto ON' : 'Auto OFF'}
157
- </button>
158
- </div>
159
-
160
- {/* Global stats */}
161
- <div className="flex items-center gap-6 text-sm text-gray-500">
162
- <span>Total pings: <span className="text-white font-mono">{totalPings}</span></span>
163
- {lastPingBy && <span>Ultimo: <span className="text-gray-300">{lastPingBy}</span></span>}
164
- </div>
165
-
166
- {/* Ping log */}
167
- <div className="w-full bg-gray-800/30 border border-white/10 rounded-xl overflow-hidden">
168
- <div className="px-4 py-2 bg-white/5 border-b border-white/10 flex items-center justify-between">
169
- <span className="text-xs text-gray-400 font-medium">Ping Log</span>
170
- <span className="text-xs text-gray-600">
171
- wire format: <code className="text-theme-secondary">msgpack</code> (binary)
172
- </span>
173
- </div>
174
- <div className="max-h-60 overflow-y-auto">
175
- {pings.length === 0 ? (
176
- <div className="px-4 py-8 text-center text-gray-600 text-sm">
177
- Clique Ping! para enviar uma mensagem binaria
178
- </div>
179
- ) : (
180
- pings.map((p) => (
181
- <div
182
- key={p.seq}
183
- className="px-4 py-2 border-b border-white/5 flex items-center justify-between text-sm"
184
- >
185
- <span className="text-gray-500 font-mono">#{p.seq}</span>
186
- <span className={`font-mono font-bold ${p.rtt != null ? rttColor(p.rtt) : 'text-gray-600'}`}>
187
- {p.rtt != null ? `${p.rtt}ms` : 'pending...'}
188
- </span>
189
- </div>
190
- ))
191
- )}
192
- </div>
193
- </div>
194
-
195
- {/* Info */}
196
- <div className="text-center text-xs text-gray-600 space-y-1">
197
- <p>Powered by <code className="text-theme">LiveRoom</code> + <code className="text-theme-secondary">msgpack codec</code></p>
198
- <p>Wire format: binary frames <code className="text-theme-secondary">0x02</code> (event) / <code className="text-theme-secondary">0x03</code> (state)</p>
199
- </div>
200
- </div>
201
- )
202
- }
1
+ import { useMemo, useState, useEffect, useRef, useCallback } from 'react'
2
+ import { Live } from '@/core/client'
3
+ import { LivePingPong } from '@server/live/LivePingPong'
4
+ import type { PingRoom } from '@server/live/rooms/PingRoom'
5
+ import { FaGaugeHigh, FaPlay, FaSignal, FaStopwatch } from 'react-icons/fa6'
6
+
7
+ interface PingEntry {
8
+ seq: number
9
+ sentAt: number
10
+ rtt: number | null
11
+ }
12
+
13
+ function StatCard({ label, value, tone = 'white' }: { label: string; value: string; tone?: string }) {
14
+ return (
15
+ <div className="rounded-lg border border-white/10 bg-white/[0.025] p-4">
16
+ <div className={`text-2xl font-semibold tabular-nums ${tone}`}>{value}</div>
17
+ <div className="mt-1 text-xs font-medium uppercase tracking-[0.16em] text-gray-600">{label}</div>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ export function PingPongDemo() {
23
+ const username = useMemo(() => {
24
+ const prefix = ['Edge', 'Core', 'Node', 'Wire', 'Frame'][Math.floor(Math.random() * 5)]
25
+ const suffix = Math.floor(Math.random() * 100)
26
+ return `${prefix}-${suffix}`
27
+ }, [])
28
+
29
+ const live = Live.use(LivePingPong, {
30
+ initialState: { ...LivePingPong.defaultState, username },
31
+ })
32
+
33
+ const [pings, setPings] = useState<PingEntry[]>([])
34
+ const [avgRtt, setAvgRtt] = useState<number | null>(null)
35
+ const [minRtt, setMinRtt] = useState<number | null>(null)
36
+ const [maxRtt, setMaxRtt] = useState<number | null>(null)
37
+ const [autoPing, setAutoPing] = useState(false)
38
+ const seqRef = useRef(0)
39
+ const pendingRef = useRef<Map<number, number>>(new Map())
40
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
41
+
42
+ useEffect(() => {
43
+ const unsub = live.$room<PingRoom>('ping:global').on('pong', (data) => {
44
+ const sentAt = pendingRef.current.get(data.seq)
45
+ if (sentAt == null) return
46
+ pendingRef.current.delete(data.seq)
47
+
48
+ const rtt = Date.now() - sentAt
49
+ setPings(prev => {
50
+ const updated = [{ seq: data.seq, sentAt, rtt }, ...prev].slice(0, 18)
51
+ const rtts = updated.filter(p => p.rtt != null).map(p => p.rtt!)
52
+ if (rtts.length > 0) {
53
+ setAvgRtt(Math.round(rtts.reduce((a, b) => a + b, 0) / rtts.length))
54
+ setMinRtt(Math.min(...rtts))
55
+ setMaxRtt(Math.max(...rtts))
56
+ }
57
+ return updated
58
+ })
59
+ })
60
+ return unsub
61
+ }, [live])
62
+
63
+ const sendPing = useCallback(() => {
64
+ const seq = ++seqRef.current
65
+ pendingRef.current.set(seq, Date.now())
66
+ live.ping({ seq })
67
+ }, [live])
68
+
69
+ useEffect(() => {
70
+ if (autoPing && live.$connected) {
71
+ intervalRef.current = setInterval(sendPing, 500)
72
+ }
73
+ return () => {
74
+ if (intervalRef.current != null) {
75
+ clearInterval(intervalRef.current)
76
+ intervalRef.current = null
77
+ }
78
+ }
79
+ }, [autoPing, live.$connected, sendPing])
80
+
81
+ const rttColor = (rtt: number) => {
82
+ if (rtt < 10) return 'text-emerald-300'
83
+ if (rtt < 50) return 'text-amber-300'
84
+ return 'text-red-300'
85
+ }
86
+
87
+ return (
88
+ <div className="grid w-full max-w-5xl gap-4 lg:grid-cols-[380px_1fr]">
89
+ <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
90
+ <div className="mb-6 flex items-start justify-between gap-4">
91
+ <div>
92
+ <h2 className="text-2xl font-semibold tracking-tight text-white">Latency probe</h2>
93
+ <p className="mt-2 text-sm leading-6 text-gray-500">
94
+ Send binary msgpack events and measure round-trip time through the Live room.
95
+ </p>
96
+ </div>
97
+ <span className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs ${
98
+ live.$connected
99
+ ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
100
+ : 'border-red-400/25 bg-red-400/10 text-red-200'
101
+ }`}>
102
+ <span className={`h-1.5 w-1.5 rounded-full ${live.$connected ? 'bg-emerald-300' : 'bg-red-300'}`} />
103
+ {live.$connected ? 'Connected' : 'Offline'}
104
+ </span>
105
+ </div>
106
+
107
+ <div className="grid gap-3">
108
+ <StatCard label="Average" value={avgRtt != null ? `${avgRtt}ms` : '--'} />
109
+ <div className="grid grid-cols-2 gap-3">
110
+ <StatCard label="Min" value={minRtt != null ? `${minRtt}ms` : '--'} tone="text-emerald-300" />
111
+ <StatCard label="Max" value={maxRtt != null ? `${maxRtt}ms` : '--'} tone="text-red-300" />
112
+ </div>
113
+ </div>
114
+
115
+ <div className="mt-6 grid grid-cols-2 gap-3">
116
+ <button
117
+ onClick={sendPing}
118
+ disabled={!live.$connected || live.$loading}
119
+ className="inline-flex h-12 items-center justify-center gap-2 rounded-lg bg-white px-4 text-sm font-semibold text-black transition hover:bg-gray-200 disabled:opacity-50"
120
+ >
121
+ <FaPlay className="h-3.5 w-3.5" />
122
+ Ping
123
+ </button>
124
+ <button
125
+ onClick={() => setAutoPing(!autoPing)}
126
+ disabled={!live.$connected}
127
+ className={`inline-flex h-12 items-center justify-center gap-2 rounded-lg border px-4 text-sm font-semibold transition disabled:opacity-50 ${
128
+ autoPing
129
+ ? 'border-amber-400/25 bg-amber-400/10 text-amber-200'
130
+ : 'border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.06]'
131
+ }`}
132
+ >
133
+ <FaStopwatch className="h-3.5 w-3.5" />
134
+ {autoPing ? 'Auto on' : 'Auto off'}
135
+ </button>
136
+ </div>
137
+
138
+ <div className="mt-6 rounded-lg border border-white/10 bg-white/[0.025] p-4 text-sm text-gray-400">
139
+ <div className="mb-2 flex items-center gap-2 text-white">
140
+ <FaSignal className="text-theme" />
141
+ Session
142
+ </div>
143
+ <div className="grid gap-2 text-xs">
144
+ <div className="flex justify-between"><span>Client</span><span className="font-mono text-gray-200">{username}</span></div>
145
+ <div className="flex justify-between"><span>Online</span><span className="font-mono text-gray-200">{live.$state.onlineCount}</span></div>
146
+ <div className="flex justify-between"><span>Total pings</span><span className="font-mono text-gray-200">{live.$state.totalPings}</span></div>
147
+ </div>
148
+ </div>
149
+ </section>
150
+
151
+ <section className="rounded-lg border border-white/10 bg-black/30">
152
+ <div className="flex items-center justify-between border-b border-white/10 px-4 py-3">
153
+ <div className="inline-flex items-center gap-2 text-sm font-semibold text-white">
154
+ <FaGaugeHigh className="text-theme" />
155
+ Event log
156
+ </div>
157
+ <span className="text-xs text-gray-500">wire: msgpack binary frames</span>
158
+ </div>
159
+
160
+ <div className="max-h-[560px] overflow-auto">
161
+ {pings.length === 0 ? (
162
+ <div className="px-4 py-14 text-center text-sm text-gray-600">
163
+ Send a ping to populate the event log.
164
+ </div>
165
+ ) : (
166
+ pings.map((p) => (
167
+ <div key={p.seq} className="grid grid-cols-[90px_1fr_auto] items-center gap-3 border-b border-white/5 px-4 py-3 text-sm">
168
+ <span className="font-mono text-gray-500">#{p.seq}</span>
169
+ <span className="text-gray-500">{new Date(p.sentAt).toLocaleTimeString()}</span>
170
+ <span className={`font-mono font-semibold ${p.rtt != null ? rttColor(p.rtt) : 'text-gray-600'}`}>
171
+ {p.rtt != null ? `${p.rtt}ms` : 'pending'}
172
+ </span>
173
+ </div>
174
+ ))
175
+ )}
176
+ </div>
177
+ </section>
178
+ </div>
179
+ )
180
+ }