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.
- package/LLMD/resources/live-components.md +103 -57
- package/LLMD/resources/live-rooms.md +187 -88
- package/README.md +27 -25
- package/app/client/.live-stubs/LiveCounter.js +4 -4
- package/app/client/src/App.tsx +11 -12
- package/app/client/src/components/AppLayout.tsx +290 -252
- package/app/client/src/components/BackButton.tsx +16 -13
- package/app/client/src/components/DemoPage.tsx +135 -22
- package/app/client/src/index.css +21 -11
- package/app/client/src/live/AuthDemo.tsx +270 -333
- package/app/client/src/live/CounterDemo.tsx +151 -206
- package/app/client/src/live/FormDemo.tsx +140 -119
- package/app/client/src/live/PingPongDemo.tsx +180 -202
- package/app/client/src/live/RoomChatDemo.tsx +397 -374
- package/app/client/src/pages/HomePage.tsx +170 -104
- package/app/server/live/LiveCounter.ts +71 -68
- package/app/server/live/LiveSharedCounter.ts +18 -12
- package/app/server/live/auto-generated-components.ts +1 -3
- package/app/server/live/rooms/CounterRoom.ts +15 -10
- package/core/client/index.ts +0 -3
- package/core/client/state/createStore.ts +88 -88
- package/core/client/state/index.ts +5 -5
- package/core/server/live/auto-generated-components.ts +1 -3
- package/core/utils/version.ts +1 -1
- package/package.json +1 -1
- package/tsconfig.json +7 -6
- package/app/client/src/components/LiveUploadWidget.tsx +0 -200
- package/app/client/src/live/UploadDemo.tsx +0 -21
- package/app/server/live/LiveUpload.ts +0 -96
- package/core/client/hooks/useLiveUpload.ts +0 -70
|
@@ -1,202 +1,180 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
className="
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
}
|