relayx-webjs 1.0.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/CHANGELOG.md +6 -0
- package/LICENSE +201 -0
- package/README.md +186 -0
- package/example/README.md +82 -0
- package/example/chat-app/index.html +13 -0
- package/example/chat-app/package-lock.json +3105 -0
- package/example/chat-app/package.json +24 -0
- package/example/chat-app/postcss.config.js +6 -0
- package/example/chat-app/src/App.jsx +193 -0
- package/example/chat-app/src/components/ui/button.jsx +16 -0
- package/example/chat-app/src/components/ui/card.jsx +9 -0
- package/example/chat-app/src/components/ui/input.jsx +11 -0
- package/example/chat-app/src/index.css +3 -0
- package/example/chat-app/src/main.jsx +10 -0
- package/example/chat-app/tailwind.config.js +8 -0
- package/example/chat-app/vite.config.js +12 -0
- package/example/screenshots/screen_1.png +0 -0
- package/example/screenshots/screen_2.png +0 -0
- package/example/screenshots/screen_3.png +0 -0
- package/package.json +34 -0
- package/realtime/realtime.js +936 -0
- package/tests/load.js +101 -0
- package/tests/test.js +514 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "relay-chat-demo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"preview": "vite preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"react": "^18.2.0",
|
|
12
|
+
"react-dom": "^18.2.0",
|
|
13
|
+
"react-router-dom": "^6.23.0",
|
|
14
|
+
"framer-motion": "^11.0.0",
|
|
15
|
+
"lucide-react": "^0.295.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@vitejs/plugin-react": "^4.1.0",
|
|
19
|
+
"tailwindcss": "^3.4.0",
|
|
20
|
+
"autoprefixer": "^10.4.0",
|
|
21
|
+
"postcss": "^8.4.0",
|
|
22
|
+
"vite": "^5.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, createContext, useContext } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BrowserRouter as Router,
|
|
4
|
+
Routes,
|
|
5
|
+
Route,
|
|
6
|
+
useNavigate,
|
|
7
|
+
useParams,
|
|
8
|
+
} from 'react-router-dom';
|
|
9
|
+
import { Realtime, CONNECTED, DISCONNECTED, RECONNECT } from '../../../realtime/realtime.js';
|
|
10
|
+
import { Button } from '@/components/ui/button';
|
|
11
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
12
|
+
import { Input } from '@/components/ui/input';
|
|
13
|
+
import { SendHorizonal } from 'lucide-react';
|
|
14
|
+
import { motion } from 'framer-motion';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Environment vars expected (add to your .env.* file or CI secrets)
|
|
18
|
+
* VITE_RELAY_API_KEY=<your_api_key>
|
|
19
|
+
* VITE_RELAY_SECRET=<your_secret_key>
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/* -------------------------------------------------------------------------------------------------
|
|
23
|
+
* Relay context – one connection app‑wide
|
|
24
|
+
* ------------------------------------------------------------------------------------------------*/
|
|
25
|
+
const RelayContext = createContext(null);
|
|
26
|
+
|
|
27
|
+
function RelayProvider({ children }) {
|
|
28
|
+
const realtimeRef = useRef(null);
|
|
29
|
+
const [connected, setConnected] = useState(false);
|
|
30
|
+
|
|
31
|
+
// Connect exactly once – on mount of the provider
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const rt = new Realtime({
|
|
34
|
+
api_key: import.meta.env.VITE_API_KEY,
|
|
35
|
+
secret: import.meta.env.VITE_SECRET,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
rt.init({
|
|
39
|
+
debug: true
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
rt.on(CONNECTED, () => setConnected(true));
|
|
43
|
+
rt.on(DISCONNECTED, () => setConnected(false));
|
|
44
|
+
rt.on(RECONNECT, () => setConnected(false));
|
|
45
|
+
|
|
46
|
+
rt.connect(); // 🔗 now "on mount" of the provider
|
|
47
|
+
|
|
48
|
+
realtimeRef.current = rt;
|
|
49
|
+
return () => rt.close();
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<RelayContext.Provider value={{ realtime: realtimeRef.current, connected }}>
|
|
54
|
+
{children}
|
|
55
|
+
</RelayContext.Provider>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function useRelay() {
|
|
60
|
+
const ctx = useContext(RelayContext);
|
|
61
|
+
if (!ctx) throw new Error('useRelay must be used inside <RelayProvider>');
|
|
62
|
+
return ctx;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* -------------------------------------------------------------------------------------------------
|
|
66
|
+
* Landing page – pick a room
|
|
67
|
+
* ------------------------------------------------------------------------------------------------*/
|
|
68
|
+
function LandingPage() {
|
|
69
|
+
const [room, setRoom] = useState('');
|
|
70
|
+
const navigate = useNavigate();
|
|
71
|
+
|
|
72
|
+
const joinRoom = () => {
|
|
73
|
+
if (!room.trim()) return;
|
|
74
|
+
navigate(`/chat/${encodeURIComponent(room.trim())}`);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<motion.div
|
|
79
|
+
className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-tr from-indigo-50 to-sky-100 p-4"
|
|
80
|
+
initial={{ opacity: 0 }}
|
|
81
|
+
animate={{ opacity: 1 }}
|
|
82
|
+
>
|
|
83
|
+
<Card className="w-full max-w-md shadow-2xl">
|
|
84
|
+
<CardContent className="flex flex-col gap-4 p-8">
|
|
85
|
+
<h1 className="text-center text-3xl font-bold tracking-tight text-indigo-700">
|
|
86
|
+
Relay Chat Demo
|
|
87
|
+
</h1>
|
|
88
|
+
<Input
|
|
89
|
+
placeholder="Enter room name"
|
|
90
|
+
value={room}
|
|
91
|
+
onChange={(e) => setRoom(e.target.value)}
|
|
92
|
+
className="text-lg"
|
|
93
|
+
/>
|
|
94
|
+
<Button onClick={joinRoom} disabled={!room.trim()} size="lg">
|
|
95
|
+
Join
|
|
96
|
+
</Button>
|
|
97
|
+
</CardContent>
|
|
98
|
+
</Card>
|
|
99
|
+
</motion.div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* -------------------------------------------------------------------------------------------------
|
|
104
|
+
* Chat page – uses the shared connection; subscribes on mount
|
|
105
|
+
* ------------------------------------------------------------------------------------------------*/
|
|
106
|
+
function ChatPage() {
|
|
107
|
+
const { room } = useParams();
|
|
108
|
+
const { realtime, connected } = useRelay();
|
|
109
|
+
const [input, setInput] = useState('');
|
|
110
|
+
const [messages, setMessages] = useState([]);
|
|
111
|
+
|
|
112
|
+
// Subscribe/unsubscribe to this room when component mounts/unmounts
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!realtime) return; // connection not yet established
|
|
115
|
+
|
|
116
|
+
const handler = (data) =>
|
|
117
|
+
setMessages((prev) => [...prev, { from: 'remote', text: data.data }]);
|
|
118
|
+
|
|
119
|
+
realtime.on(room, handler);
|
|
120
|
+
return () => realtime.off(room, handler);
|
|
121
|
+
}, [realtime, room]);
|
|
122
|
+
|
|
123
|
+
const sendMessage = async () => {
|
|
124
|
+
if (!input.trim() || !realtime) return;
|
|
125
|
+
const ok = await realtime.publish(room, input.trim());
|
|
126
|
+
if (ok) {
|
|
127
|
+
setMessages((prev) => [...prev, { from: 'me', text: input.trim() }]);
|
|
128
|
+
setInput('');
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex min-h-screen flex-col bg-slate-50">
|
|
134
|
+
<header className="flex items-center justify-between bg-indigo-600 p-4 text-white shadow-md">
|
|
135
|
+
<h2 className="text-xl font-semibold">Room: {room}</h2>
|
|
136
|
+
<span className={`text-sm ${connected ? 'text-emerald-300' : 'text-red-300'}`}>
|
|
137
|
+
{connected ? 'Connected' : 'Offline'}
|
|
138
|
+
</span>
|
|
139
|
+
</header>
|
|
140
|
+
|
|
141
|
+
<main className="flex-1 overflow-y-auto p-4">
|
|
142
|
+
{messages.length === 0 && (
|
|
143
|
+
<p className="mt-4 text-center text-gray-500">No messages yet – start the conversation!</p>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{messages.map((m, i) => (
|
|
147
|
+
<motion.div
|
|
148
|
+
key={i}
|
|
149
|
+
className={`mb-2 max-w-[75%] rounded-2xl px-4 py-2 text-base shadow ${
|
|
150
|
+
m.from === 'me'
|
|
151
|
+
? 'ml-auto bg-indigo-500 text-white'
|
|
152
|
+
: 'mr-auto bg-white text-gray-900'
|
|
153
|
+
}`}
|
|
154
|
+
initial={{ y: 10, opacity: 0 }}
|
|
155
|
+
animate={{ y: 0, opacity: 1 }}
|
|
156
|
+
>
|
|
157
|
+
{m.text}
|
|
158
|
+
</motion.div>
|
|
159
|
+
))}
|
|
160
|
+
</main>
|
|
161
|
+
|
|
162
|
+
<footer className="flex items-center gap-2 border-t bg-white p-4 shadow-lg">
|
|
163
|
+
<Input
|
|
164
|
+
value={input}
|
|
165
|
+
onChange={(e) => setInput(e.target.value)}
|
|
166
|
+
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
|
|
167
|
+
placeholder={connected ? 'Type a message…' : 'Connecting to Relay…'}
|
|
168
|
+
disabled={!connected}
|
|
169
|
+
className="flex-1"
|
|
170
|
+
/>
|
|
171
|
+
<Button size="icon" onClick={sendMessage} disabled={!connected || !input.trim()}>
|
|
172
|
+
<SendHorizonal className="h-5 w-5" />
|
|
173
|
+
</Button>
|
|
174
|
+
</footer>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* -------------------------------------------------------------------------------------------------
|
|
180
|
+
* Root app
|
|
181
|
+
* ------------------------------------------------------------------------------------------------*/
|
|
182
|
+
export default function App() {
|
|
183
|
+
return (
|
|
184
|
+
<Router>
|
|
185
|
+
<RelayProvider>
|
|
186
|
+
<Routes>
|
|
187
|
+
<Route path="/" element={<LandingPage />} />
|
|
188
|
+
<Route path="/chat/:room" element={<ChatPage />} />
|
|
189
|
+
</Routes>
|
|
190
|
+
</RelayProvider>
|
|
191
|
+
</Router>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export function Button({ children, size = 'md', disabled, className = '', ...props }) {
|
|
4
|
+
const base = 'inline-flex items-center justify-center rounded-2xl shadow font-medium transition';
|
|
5
|
+
const sizes = {
|
|
6
|
+
icon: 'h-10 w-10',
|
|
7
|
+
lg: 'h-11 px-6 text-lg',
|
|
8
|
+
md: 'h-10 px-4',
|
|
9
|
+
};
|
|
10
|
+
const cls = `${base} bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50 ${sizes[size]} ${className}`;
|
|
11
|
+
return (
|
|
12
|
+
<button className={cls} disabled={disabled} {...props}>
|
|
13
|
+
{children}
|
|
14
|
+
</button>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export function Card({ className = '', children }) {
|
|
4
|
+
return <div className={`rounded-2xl bg-white ${className}`}>{children}</div>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function CardContent({ className = '', children }) {
|
|
8
|
+
return <div className={className}>{children}</div>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export const Input = React.forwardRef(function Input({ className = '', ...props }, ref) {
|
|
4
|
+
return (
|
|
5
|
+
<input
|
|
6
|
+
ref={ref}
|
|
7
|
+
className={`rounded-xl border border-slate-300 p-3 focus:border-indigo-500 focus:outline-none ${className}`}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "relayx-webjs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A powerful library for integrating real-time communication into your webapps, powered by the Relay Network.",
|
|
5
|
+
"main": "realtime/realtime.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "NODE_ENV=test node tests/test.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/Realtime-Relay/relayx-webjs.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"realtime",
|
|
16
|
+
"realtime-communication",
|
|
17
|
+
"relay",
|
|
18
|
+
"relay-x",
|
|
19
|
+
"relayx-webjs"
|
|
20
|
+
],
|
|
21
|
+
"author": "Relay",
|
|
22
|
+
"license": "Apache 2.0",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Realtime-Relay/relayx-webjs/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/Realtime-Relay/relayx-webjs#readme",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@msgpack/msgpack": "^3.1.2",
|
|
29
|
+
"@nats-io/jetstream": "3.0.0-35",
|
|
30
|
+
"jest": "30.0.4",
|
|
31
|
+
"nats.ws": "1.30.3",
|
|
32
|
+
"uuid": "11.1.0"
|
|
33
|
+
}
|
|
34
|
+
}
|