@sansavision/create-pulse 0.1.0-alpha.12 → 0.1.0-alpha.2
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/package.json +1 -1
- package/templates/react-all-features/src/App.tsx +26 -116
- package/templates/react-all-features/src/hooks/usePulse.ts +20 -101
- package/templates/react-watch-together/src/App.tsx +17 -144
- package/templates/react-watch-together/src/components/VideoPlayer.tsx +1 -8
- package/templates/react-watch-together/src/hooks/usePulse.ts +20 -101
- package/templates/react-all-features/src/components/EncryptedChat.tsx +0 -312
- package/templates/react-all-features/src/components/GameSync.tsx +0 -297
- package/templates/react-all-features/src/components/ServerMetrics.tsx +0 -160
package/package.json
CHANGED
|
@@ -1,33 +1,26 @@
|
|
|
1
|
-
import { useState, useCallback
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
2
|
import { usePulse } from './hooks/usePulse';
|
|
3
3
|
import { VideoPlayer } from './components/VideoPlayer';
|
|
4
|
-
import {
|
|
5
|
-
import { EncryptedChat } from './components/EncryptedChat';
|
|
6
|
-
import { ServerMetrics } from './components/ServerMetrics';
|
|
7
|
-
import { Activity, Users, MessageSquare, Video, LineChart, ArrowLeft, Gamepad2 } from 'lucide-react';
|
|
4
|
+
import { Activity, Users, MessageSquare, Video, LineChart, ArrowLeft } from 'lucide-react';
|
|
8
5
|
|
|
9
6
|
const VIDEO_URL = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
|
|
10
7
|
const STREAM_NAME = "watch-together-demo";
|
|
11
8
|
|
|
12
9
|
export default function App() {
|
|
13
|
-
const { isConnected, isConnecting, metrics,
|
|
10
|
+
const { isConnected, isConnecting, metrics, useChannel } = usePulse({
|
|
14
11
|
relayUrl: 'ws://localhost:4001',
|
|
15
12
|
apiKey: 'demo'
|
|
16
13
|
});
|
|
17
14
|
|
|
18
|
-
const [activeFeature, setActiveFeature] = useState<'hub' | 'watch' | 'chat' | 'metrics'
|
|
15
|
+
const [activeFeature, setActiveFeature] = useState<'hub' | 'watch' | 'chat' | 'metrics'>('hub');
|
|
19
16
|
|
|
20
17
|
const [hostTime, setHostTime] = useState(0);
|
|
21
18
|
const [peerTime, setPeerTime] = useState(0);
|
|
22
19
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
23
20
|
const [events, setEvents] = useState<{ id: number, action: string, time: string }[]>([]);
|
|
24
21
|
|
|
25
|
-
// Track relay message delay
|
|
26
|
-
const [lastDelay, setLastDelay] = useState<number | null>(null);
|
|
27
|
-
const delayHistoryRef = useRef<number[]>([]);
|
|
28
|
-
|
|
29
22
|
const logEvent = useCallback((action: string) => {
|
|
30
|
-
setEvents(prev => [...prev.slice(-
|
|
23
|
+
setEvents(prev => [...prev.slice(-4), {
|
|
31
24
|
id: Date.now(),
|
|
32
25
|
action,
|
|
33
26
|
time: new Date().toISOString().substring(11, 23)
|
|
@@ -36,15 +29,7 @@ export default function App() {
|
|
|
36
29
|
|
|
37
30
|
const handleSyncMessage = useCallback((msg: any) => {
|
|
38
31
|
if (activeFeature !== 'watch') return;
|
|
39
|
-
|
|
40
|
-
// Track delivery delay
|
|
41
|
-
if (msg.ts) {
|
|
42
|
-
const delay = Date.now() - msg.ts;
|
|
43
|
-
setLastDelay(delay);
|
|
44
|
-
delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), delay];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
logEvent(`RECV: ${msg.action} @ ${msg.time?.toFixed(2)}s`);
|
|
32
|
+
logEvent(`RECV: ${msg.action}`);
|
|
48
33
|
|
|
49
34
|
switch (msg.action) {
|
|
50
35
|
case 'play':
|
|
@@ -68,47 +53,10 @@ export default function App() {
|
|
|
68
53
|
const { broadcast } = useChannel(STREAM_NAME, handleSyncMessage);
|
|
69
54
|
|
|
70
55
|
const broadcastAction = useCallback((action: string, time: number) => {
|
|
71
|
-
logEvent(`SEND: ${action}
|
|
72
|
-
|
|
73
|
-
const msg = { action, time, ts: sendTs };
|
|
74
|
-
broadcast(msg);
|
|
75
|
-
|
|
76
|
-
// Single-tab simulation: defer peerTime update by the measured relay
|
|
77
|
-
// round-trip so that drift is visible and realistic. In a real
|
|
78
|
-
// multi-tab setup the relay echo handler (handleSyncMessage) updates
|
|
79
|
-
// peerTime — but since the relay does not echo back to the sender,
|
|
80
|
-
// we simulate it here with the actual measured delay.
|
|
81
|
-
const simulatedDelay = delayHistoryRef.current.length > 0
|
|
82
|
-
? delayHistoryRef.current[delayHistoryRef.current.length - 1]
|
|
83
|
-
: 4; // sensible default ~4ms first message
|
|
84
|
-
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
if (action === 'tick') {
|
|
87
|
-
setPeerTime(time);
|
|
88
|
-
} else if (action === 'play') {
|
|
89
|
-
setIsPlaying(true);
|
|
90
|
-
setPeerTime(time);
|
|
91
|
-
} else if (action === 'pause') {
|
|
92
|
-
setIsPlaying(false);
|
|
93
|
-
setPeerTime(time);
|
|
94
|
-
} else if (action === 'seek') {
|
|
95
|
-
setPeerTime(time);
|
|
96
|
-
}
|
|
97
|
-
}, simulatedDelay);
|
|
98
|
-
|
|
99
|
-
// Measure delay after the event loop processes the send
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
const delay = Date.now() - sendTs;
|
|
102
|
-
setLastDelay(delay);
|
|
103
|
-
delayHistoryRef.current = [...delayHistoryRef.current.slice(-29), delay];
|
|
104
|
-
}, 0);
|
|
56
|
+
logEvent(`SEND: ${action}`);
|
|
57
|
+
broadcast({ action, time, ts: Date.now() });
|
|
105
58
|
}, [broadcast, logEvent]);
|
|
106
59
|
|
|
107
|
-
// Computed delay stats
|
|
108
|
-
const avgDelay = delayHistoryRef.current.length > 0
|
|
109
|
-
? Math.round(delayHistoryRef.current.reduce((a: number, b: number) => a + b, 0) / delayHistoryRef.current.length)
|
|
110
|
-
: null;
|
|
111
|
-
|
|
112
60
|
return (
|
|
113
61
|
<div className="min-h-screen">
|
|
114
62
|
{/* Header */}
|
|
@@ -138,7 +86,7 @@ export default function App() {
|
|
|
138
86
|
<div className="flex items-center gap-2">
|
|
139
87
|
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-brandEmerald shadow-[0_0_8px_var(--tw-shadow-color)] shadow-brandEmerald' : isConnecting ? 'bg-brandAmber shadow-[0_0_8px_var(--tw-shadow-color)] shadow-brandAmber' : 'bg-brandRose'}`} />
|
|
140
88
|
<span className={isConnected ? 'text-brandEmerald' : 'text-textMuted'}>
|
|
141
|
-
{isConnected ? 'Relay Connected' : isConnecting
|
|
89
|
+
{isConnected ? 'Relay Connected' : isConnecting ? 'Connecting...' : 'Disconnected'}
|
|
142
90
|
</span>
|
|
143
91
|
</div>
|
|
144
92
|
{metrics && (
|
|
@@ -191,26 +139,6 @@ export default function App() {
|
|
|
191
139
|
</div>
|
|
192
140
|
</div>
|
|
193
141
|
|
|
194
|
-
{/* Game Sync Card */}
|
|
195
|
-
<div onClick={() => setActiveFeature('game')} className="glass-panel p-7 cursor-pointer hover:-translate-y-1 transition-all duration-300 group relative overflow-hidden">
|
|
196
|
-
<div className="absolute -top-16 -right-16 w-48 h-48 bg-purple-500/20 rounded-full blur-[40px] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
|
197
|
-
<div className="flex items-center gap-4 mb-4">
|
|
198
|
-
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/10 text-purple-400">
|
|
199
|
-
<Gamepad2 className="w-6 h-6" />
|
|
200
|
-
</div>
|
|
201
|
-
<div className="flex flex-col gap-1">
|
|
202
|
-
<span className="text-[10px] font-bold uppercase tracking-wider bg-purple-500/20 text-purple-400 px-2.5 py-1 rounded-full w-fit">Realtime</span>
|
|
203
|
-
<span className="text-xs text-textDim font-mono">StreamMode::Realtime</span>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
<h3 className="text-lg font-bold mb-2">Game State Sync</h3>
|
|
207
|
-
<p className="text-sm text-textMuted leading-relaxed mb-6">60Hz realtime game loop syncing player positions. Sub-10ms latency updates over Pulse.</p>
|
|
208
|
-
<div className="flex gap-2 relative z-10">
|
|
209
|
-
<span className="px-2.5 py-1 rounded-full text-[10px] border border-pulseBorder text-textMuted bg-white/5">React</span>
|
|
210
|
-
<span className="px-2.5 py-1 rounded-full text-[10px] border border-pulseBorder text-textMuted bg-white/5">Canvas</span>
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
142
|
{/* Chat Card */}
|
|
215
143
|
<div onClick={() => setActiveFeature('chat')} className="glass-panel p-7 cursor-pointer hover:-translate-y-1 transition-all duration-300 group relative overflow-hidden">
|
|
216
144
|
<div className="absolute -top-16 -right-16 w-48 h-48 bg-brandEmerald/20 rounded-full blur-[40px] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
|
@@ -223,7 +151,7 @@ export default function App() {
|
|
|
223
151
|
<span className="text-xs text-textDim font-mono">StreamMode::P2P</span>
|
|
224
152
|
</div>
|
|
225
153
|
</div>
|
|
226
|
-
<h3 className="text-lg font-bold mb-2">Encrypted Chat</h3>
|
|
154
|
+
<h3 className="text-lg font-bold mb-2">Encrypted Chat (Stub)</h3>
|
|
227
155
|
<p className="text-sm text-textMuted leading-relaxed mb-6">Two browser tabs exchanging messages over X25519 double-ratchet encryption. The relay routes opaque ciphertexts.</p>
|
|
228
156
|
<div className="flex gap-2 relative z-10">
|
|
229
157
|
<span className="px-2.5 py-1 rounded-full text-[10px] border border-pulseBorder text-textMuted bg-white/5">React</span>
|
|
@@ -243,7 +171,7 @@ export default function App() {
|
|
|
243
171
|
<span className="text-xs text-textDim font-mono">StreamMode::Realtime</span>
|
|
244
172
|
</div>
|
|
245
173
|
</div>
|
|
246
|
-
<h3 className="text-lg font-bold mb-2">Server Metrics</h3>
|
|
174
|
+
<h3 className="text-lg font-bold mb-2">Server Metrics (Stub)</h3>
|
|
247
175
|
<p className="text-sm text-textMuted leading-relaxed mb-6">Server pushes live system metrics to a React dashboard over Pulse Realtime stream for zero-config observability.</p>
|
|
248
176
|
<div className="flex gap-2 relative z-10">
|
|
249
177
|
<span className="px-2.5 py-1 rounded-full text-[10px] border border-pulseBorder text-textMuted bg-white/5">React</span>
|
|
@@ -309,20 +237,7 @@ export default function App() {
|
|
|
309
237
|
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
310
238
|
<span className="text-textMuted">Drift</span>
|
|
311
239
|
<span className={`font-semibold tracking-wider ${Math.abs(hostTime - peerTime) > 0.15 ? 'text-brandAmber' : 'text-brandEmerald'}`}>
|
|
312
|
-
{Math.abs(hostTime - peerTime)
|
|
313
|
-
</span>
|
|
314
|
-
</div>
|
|
315
|
-
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
316
|
-
<span className="text-textMuted">Relay RTT</span>
|
|
317
|
-
<span className="text-textMain font-semibold tracking-wider">
|
|
318
|
-
{metrics ? `${Math.round(metrics.rtt)}ms` : avgDelay !== null ? `~${avgDelay * 2}ms` : '—'}
|
|
319
|
-
</span>
|
|
320
|
-
</div>
|
|
321
|
-
<div className="flex justify-between items-center p-3 rounded-lg bg-black/30 border border-pulseBorder">
|
|
322
|
-
<span className="text-textMuted">Msg Delay</span>
|
|
323
|
-
<span className={`font-semibold tracking-wider ${lastDelay !== null && lastDelay > 50 ? 'text-brandAmber' : 'text-brandEmerald'}`}>
|
|
324
|
-
{lastDelay !== null ? `${lastDelay}ms` : '—'}
|
|
325
|
-
{avgDelay !== null && <span className="text-textDim text-[10px] ml-1">avg {avgDelay}ms</span>}
|
|
240
|
+
{Math.abs(hostTime - peerTime).toFixed(3)}s
|
|
326
241
|
</span>
|
|
327
242
|
</div>
|
|
328
243
|
</div>
|
|
@@ -362,9 +277,14 @@ export default function App() {
|
|
|
362
277
|
</div>
|
|
363
278
|
|
|
364
279
|
<h2 className="text-3xl font-bold tracking-tight mb-2">Encrypted Chat</h2>
|
|
365
|
-
<div className="text-textMuted mb-6 text-sm">Real Web Crypto API (ECDH P-256 + AES-GCM) powering E2E encryption over Pulse Relay.</div>
|
|
366
280
|
|
|
367
|
-
<
|
|
281
|
+
<div className="flex flex-col items-center justify-center py-24 glass-panel mt-12 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-green-900/10 to-transparent">
|
|
282
|
+
<MessageSquare className="w-16 h-16 text-brandEmerald mb-6 opacity-80" />
|
|
283
|
+
<h3 className="text-2xl font-bold mb-3">Chat Components Setup Here</h3>
|
|
284
|
+
<p className="text-textMuted text-center max-w-md mx-auto leading-relaxed">
|
|
285
|
+
Implementation for the E2E encrypted React UI chat components go here. Refer to the base HTML demo for logical structure.
|
|
286
|
+
</p>
|
|
287
|
+
</div>
|
|
368
288
|
</div>
|
|
369
289
|
)}
|
|
370
290
|
|
|
@@ -373,28 +293,18 @@ export default function App() {
|
|
|
373
293
|
<div className="flex items-center gap-3 text-sm text-textDim mb-6">
|
|
374
294
|
<span className="text-brandPurple cursor-pointer hover:text-brandCyan transition-colors" onClick={() => setActiveFeature('hub')}>Hub</span>
|
|
375
295
|
<span>/</span>
|
|
376
|
-
<span className="text-textMain">Server Metrics
|
|
296
|
+
<span className="text-textMain">Server Metrics</span>
|
|
377
297
|
</div>
|
|
378
298
|
|
|
379
299
|
<h2 className="text-3xl font-bold tracking-tight mb-2">Server Metrics Dashboard</h2>
|
|
380
|
-
<div className="text-textMuted mb-6 text-sm">Receiving live server telemetry via Pulse STREAM_DATA and rendering realtime charts.</div>
|
|
381
300
|
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
<div className="flex items-center gap-3 text-sm text-textDim mb-6">
|
|
389
|
-
<span className="text-brandPurple cursor-pointer hover:text-brandCyan transition-colors" onClick={() => setActiveFeature('hub')}>Hub</span>
|
|
390
|
-
<span>/</span>
|
|
391
|
-
<span className="text-textMain">Game State Sync</span>
|
|
301
|
+
<div className="flex flex-col items-center justify-center py-24 glass-panel mt-12 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-cyan-900/10 to-transparent">
|
|
302
|
+
<LineChart className="w-16 h-16 text-brandCyan mb-6 opacity-80" />
|
|
303
|
+
<h3 className="text-2xl font-bold mb-3">Dashboard Charts Setup Here</h3>
|
|
304
|
+
<p className="text-textMuted text-center max-w-md mt-2">
|
|
305
|
+
Bind <code className="bg-black/50 text-brandCyan px-2 py-1 rounded-md border border-pulseBorder font-mono text-xs shadow-inner">useChannel('server-metrics')</code> to Recharts or Chart.js here.
|
|
306
|
+
</p>
|
|
392
307
|
</div>
|
|
393
|
-
|
|
394
|
-
<h2 className="text-3xl font-bold tracking-tight mb-2">Game State Sync</h2>
|
|
395
|
-
<div className="text-textMuted mb-6 text-sm">Sending 60Hz tick payloads over Pulse relay and interpolating remote peer states smoothly via Canvas API.</div>
|
|
396
|
-
|
|
397
|
-
<GameSync useChannel={useChannel} logEvent={logEvent} />
|
|
398
308
|
</div>
|
|
399
309
|
)}
|
|
400
310
|
</main>
|
|
@@ -4,153 +4,73 @@ import { Pulse, PulseConnection, PulseStream, ConnectionMetrics } from '@sansavi
|
|
|
4
4
|
interface UsePulseOptions {
|
|
5
5
|
relayUrl?: string;
|
|
6
6
|
apiKey?: string;
|
|
7
|
-
/** Max reconnect attempts before giving up (0 = infinite). Default: 0 */
|
|
8
|
-
maxReconnectAttempts?: number;
|
|
9
|
-
/** Initial delay between reconnect attempts in ms. Default: 500 */
|
|
10
|
-
reconnectBaseDelay?: number;
|
|
11
|
-
/** Maximum delay between reconnect attempts in ms. Default: 8000 */
|
|
12
|
-
reconnectMaxDelay?: number;
|
|
13
7
|
}
|
|
14
8
|
|
|
15
9
|
export function usePulse(options: UsePulseOptions = {}) {
|
|
16
10
|
const {
|
|
17
11
|
relayUrl = 'ws://localhost:4001',
|
|
18
|
-
apiKey = 'demo'
|
|
19
|
-
maxReconnectAttempts = 0, // 0 = infinite
|
|
20
|
-
reconnectBaseDelay = 500,
|
|
21
|
-
reconnectMaxDelay = 8000,
|
|
12
|
+
apiKey = 'demo'
|
|
22
13
|
} = options;
|
|
23
14
|
|
|
24
15
|
const [isConnected, setIsConnected] = useState(false);
|
|
25
16
|
const [metrics, setMetrics] = useState<ConnectionMetrics | null>(null);
|
|
26
17
|
const [error, setError] = useState<Error | null>(null);
|
|
27
18
|
const [isConnecting, setIsConnecting] = useState(true);
|
|
28
|
-
const [reconnectAttempt, setReconnectAttempt] = useState(0);
|
|
29
19
|
|
|
30
20
|
const connectionRef = useRef<PulseConnection | null>(null);
|
|
31
|
-
const mountedRef = useRef(true);
|
|
32
|
-
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
33
|
-
const attemptRef = useRef(0);
|
|
34
21
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
// Establish connection on mount
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let mounted = true;
|
|
25
|
+
const pulse = new Pulse({ apiKey });
|
|
38
26
|
|
|
39
27
|
setIsConnecting(true);
|
|
40
|
-
setError(null);
|
|
41
|
-
|
|
42
|
-
const pulse = new Pulse({ apiKey });
|
|
43
28
|
|
|
44
29
|
pulse.connect(relayUrl)
|
|
45
30
|
.then(conn => {
|
|
46
|
-
if (!
|
|
31
|
+
if (!mounted) {
|
|
47
32
|
conn.disconnect();
|
|
48
33
|
return;
|
|
49
34
|
}
|
|
50
35
|
|
|
51
|
-
// Success — reset backoff state
|
|
52
|
-
attemptRef.current = 0;
|
|
53
|
-
setReconnectAttempt(0);
|
|
54
|
-
|
|
55
36
|
connectionRef.current = conn;
|
|
56
37
|
setIsConnected(true);
|
|
57
38
|
setIsConnecting(false);
|
|
58
39
|
|
|
59
|
-
conn.on('metrics', (m
|
|
60
|
-
if (
|
|
40
|
+
conn.on('metrics', (m) => {
|
|
41
|
+
if (mounted) setMetrics(m);
|
|
61
42
|
});
|
|
62
43
|
|
|
63
44
|
conn.on('disconnect', () => {
|
|
64
|
-
if (
|
|
65
|
-
connectionRef.current = null;
|
|
66
|
-
setIsConnected(false);
|
|
67
|
-
setMetrics(null);
|
|
68
|
-
|
|
69
|
-
// Schedule reconnect with exponential backoff
|
|
70
|
-
scheduleReconnect();
|
|
45
|
+
if (mounted) setIsConnected(false);
|
|
71
46
|
});
|
|
72
47
|
})
|
|
73
48
|
.catch(err => {
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Schedule reconnect on failed connect too
|
|
80
|
-
scheduleReconnect();
|
|
49
|
+
if (mounted) {
|
|
50
|
+
setError(err);
|
|
51
|
+
setIsConnecting(false);
|
|
52
|
+
}
|
|
81
53
|
});
|
|
82
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
-
}, [relayUrl, apiKey]);
|
|
84
|
-
|
|
85
|
-
const scheduleReconnect = useCallback(() => {
|
|
86
|
-
if (!mountedRef.current) return;
|
|
87
|
-
|
|
88
|
-
const attempt = attemptRef.current;
|
|
89
|
-
if (maxReconnectAttempts > 0 && attempt >= maxReconnectAttempts) {
|
|
90
|
-
setError(new Error(`Failed to reconnect after ${maxReconnectAttempts} attempts`));
|
|
91
|
-
setIsConnecting(false);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
attemptRef.current = attempt + 1;
|
|
96
|
-
setReconnectAttempt(attempt + 1);
|
|
97
|
-
|
|
98
|
-
// Exponential backoff with jitter: base * 2^attempt + random jitter
|
|
99
|
-
const delay = Math.min(
|
|
100
|
-
reconnectBaseDelay * Math.pow(2, attempt) + Math.random() * 200,
|
|
101
|
-
reconnectMaxDelay
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
console.log(`[Pulse] Reconnecting in ${Math.round(delay)}ms (attempt ${attempt + 1})...`);
|
|
105
|
-
|
|
106
|
-
reconnectTimerRef.current = setTimeout(() => {
|
|
107
|
-
if (mountedRef.current) {
|
|
108
|
-
connect();
|
|
109
|
-
}
|
|
110
|
-
}, delay);
|
|
111
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
112
|
-
}, [reconnectBaseDelay, reconnectMaxDelay, maxReconnectAttempts, connect]);
|
|
113
|
-
|
|
114
|
-
// Initial connection on mount
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
mountedRef.current = true;
|
|
117
|
-
connect();
|
|
118
54
|
|
|
119
55
|
return () => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// Clear any pending reconnect timer
|
|
123
|
-
if (reconnectTimerRef.current) {
|
|
124
|
-
clearTimeout(reconnectTimerRef.current);
|
|
125
|
-
reconnectTimerRef.current = null;
|
|
126
|
-
}
|
|
127
|
-
|
|
56
|
+
mounted = false;
|
|
128
57
|
if (connectionRef.current) {
|
|
129
58
|
connectionRef.current.disconnect();
|
|
130
59
|
connectionRef.current = null;
|
|
131
60
|
}
|
|
132
61
|
};
|
|
133
|
-
}, [
|
|
62
|
+
}, [relayUrl, apiKey]);
|
|
134
63
|
|
|
135
|
-
// Helper to open a pub/sub stream
|
|
64
|
+
// Helper to open a pub/sub or RPC stream and manage its lifecycle
|
|
136
65
|
const useChannel = useCallback((channelName: string, onMessage?: (data: any) => void) => {
|
|
137
66
|
const [stream, setStream] = useState<PulseStream | null>(null);
|
|
138
67
|
|
|
139
68
|
useEffect(() => {
|
|
140
|
-
if (!isConnected || !connectionRef.current)
|
|
141
|
-
setStream(null);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
69
|
+
if (!isConnected || !connectionRef.current) return;
|
|
144
70
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
qos: 'interactive-data'
|
|
149
|
-
});
|
|
150
|
-
} catch (err) {
|
|
151
|
-
console.error('[Pulse] Failed to open stream:', err);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
71
|
+
const newStream = connectionRef.current.stream(channelName, {
|
|
72
|
+
qos: 'interactive-data' // Best for low-latency sync
|
|
73
|
+
});
|
|
154
74
|
|
|
155
75
|
setStream(newStream);
|
|
156
76
|
|
|
@@ -187,7 +107,6 @@ export function usePulse(options: UsePulseOptions = {}) {
|
|
|
187
107
|
isConnecting,
|
|
188
108
|
metrics,
|
|
189
109
|
error,
|
|
190
|
-
reconnectAttempt,
|
|
191
110
|
connection: connectionRef.current,
|
|
192
111
|
useChannel
|
|
193
112
|
};
|