@rvettori/elysia-broadcast 0.2.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/README.md ADDED
@@ -0,0 +1,361 @@
1
+ # @rvettori/elysia-broadcast
2
+
3
+ Broadcast and Server-Sent Events (SSE) system for Elysia with multi-channel per-user support.
4
+
5
+ ## 🚀 Features
6
+
7
+ - ✅ **Multiple channels per user** - Each user can connect to multiple channels simultaneously
8
+ - ✅ **Type-safe** - Fully typed with TypeScript
9
+ - ✅ **SSE out-of-the-box** - Ready-to-use Server-Sent Events plugin with client library
10
+ - ✅ **Alpine.js compatible** - Works seamlessly with Alpine.morph and x-sync
11
+ - ✅ **Flexible** - Use `BroadcastManager` standalone or as an Elysia plugin
12
+ - ✅ **Lightweight** - Zero dependencies besides Elysia
13
+ - ✅ **Tested** - Complete test coverage
14
+
15
+ ## 📦 Installation
16
+
17
+ ```bash
18
+ bun add @rvettori/elysia-broadcast
19
+ ```
20
+
21
+ ## 🎯 Basic Usage
22
+
23
+ ### Server Setup
24
+
25
+ ```typescript
26
+ import { Elysia } from 'elysia';
27
+ import { broadcastPlugin, streamPlugin } from '@rvettori/elysia-broadcast';
28
+
29
+ const app = new Elysia()
30
+ .use(broadcastPlugin())
31
+ .use(streamPlugin({
32
+ authMiddleware: yourAuthMiddleware,
33
+ getUserId: (ctx) => ctx.store.user.userId
34
+ }))
35
+ .post('/todos', ({ store }) => {
36
+ // Create todo...
37
+
38
+ // Broadcast to user's 'todos' channel
39
+ store.broadcast.broadcast('todos', userId, {
40
+ type: 'todo.created',
41
+ data: { id: 1, task: 'New task' },
42
+ html: '<div x-sync id="todo-list">...</div>' // Alpine-compatible
43
+ });
44
+
45
+ return { success: true };
46
+ })
47
+ .listen(3000);
48
+ ```
49
+
50
+ ### Client Setup
51
+
52
+ The `streamPlugin` automatically provides a client library at `/vendor/elysia-sse.js`:
53
+
54
+ ```html
55
+ <!-- Load the client library -->
56
+ <script src="/vendor/elysia-sse.js"></script>
57
+
58
+ <!-- Connect to a channel -->
59
+ <script>
60
+ ElysiaSSE.connect('todos');
61
+ </script>
62
+
63
+ <!-- Elements with x-sync and id will be auto-updated -->
64
+ <div x-sync id="todo-list">
65
+ <!-- Content here will be updated via SSE -->
66
+ </div>
67
+ ```
68
+
69
+ ### BroadcastManager Standalone
70
+
71
+ ```typescript
72
+ import { BroadcastManager } from '@rvettori/elysia-broadcast';
73
+
74
+ const manager = new BroadcastManager();
75
+
76
+ // Subscribe
77
+ const unsubscribe = manager.subscribe('notifications', 123, (event) => {
78
+ console.log('Event received:', event.type, event.data);
79
+ });
80
+
81
+ // Broadcast to specific user
82
+ manager.broadcast('notifications', 123, {
83
+ type: 'notification.new',
84
+ data: { message: 'Hello!' }
85
+ });
86
+
87
+ // Broadcast to ALL users in channel
88
+ manager.broadcast('notifications', {
89
+ type: 'system.announcement',
90
+ data: { message: 'System maintenance at 3pm' }
91
+ });
92
+
93
+ // Cleanup
94
+ unsubscribe();
95
+ ```
96
+
97
+ ## 📚 API
98
+
99
+ ### `broadcastPlugin(options?)`
100
+
101
+ Elysia plugin that injects `BroadcastManager` into the store.
102
+
103
+ **Options:**
104
+ - `stateName?: string` - State name (default: `'broadcast'`)
105
+
106
+ **Example:**
107
+ ```typescript
108
+ app.use(broadcastPlugin({ stateName: 'events' }));
109
+
110
+ app.get('/trigger', ({ store }) => {
111
+ store.events.broadcast('channel', userId, { ... });
112
+ });
113
+ ```
114
+
115
+ ### `streamPlugin(options?)`
116
+
117
+ Plugin that creates a generic SSE route.
118
+
119
+ **Options:**
120
+ ```typescript
121
+ interface StreamPluginOptions {
122
+ basePath?: string; // Default: '/stream'
123
+ userStoreName?: string; // Default: 'user'
124
+ userIdField?: string; // Default: 'userId'
125
+ getUserId?: (context) => number | string;
126
+ authMiddleware?: any;
127
+ }
128
+ ```
129
+
130
+ **Example:**
131
+ ```typescript
132
+ app.use(streamPlugin({
133
+ basePath: '/events',
134
+ authMiddleware: sessionAuth,
135
+ getUserId: (ctx) => ctx.store.currentUser.id
136
+ }));
137
+
138
+ // Client: GET /events/todos
139
+ ```
140
+
141
+ ### `BroadcastManager`
142
+
143
+ Main event management class.
144
+
145
+ #### Methods
146
+
147
+ **`subscribe(channel, userId, callback)`**
148
+
149
+ Register a listener.
150
+
151
+ ```typescript
152
+ const unsubscribe = manager.subscribe('todos', 123, (event) => {
153
+ console.log(event.type, event.data);
154
+ });
155
+ ```
156
+
157
+ **`broadcast(channel, userId, event)`** | **`broadcast(channel, event)`**
158
+
159
+ Send event to all listeners on the channel/user. Omit userId to broadcast to all users in the channel.
160
+
161
+ ```typescript
162
+ // Broadcast to specific user
163
+ manager.broadcast('todos', 123, {
164
+ type: 'update',
165
+ data: { id: 1 },
166
+ html: '<div>...</div>' // Optional
167
+ });
168
+
169
+ // Broadcast to ALL users in channel (no userId)
170
+ manager.broadcast('todos', {
171
+ type: 'system.announcement',
172
+ data: { message: 'New feature available!' }
173
+ });
174
+ ```
175
+
176
+ **`getConnectionCount(channel, userId)`**
177
+
178
+ Returns number of active connections for channel/user.
179
+
180
+ **`getTotalConnections()`**
181
+
182
+ Returns total number of connections.
183
+
184
+ **`getActiveChannels()`**
185
+
186
+ Lists all active channels.
187
+
188
+ **`clearChannel(channel, userId)`**
189
+
190
+ Removes all connections from a channel.
191
+
192
+ **`clearAll()`**
193
+
194
+ Removes all connections.
195
+
196
+ ## 🎨 Frontend Integration
197
+
198
+ ### Alpine.js + SSE (Recommended)
199
+
200
+ ```html
201
+ <!-- 1. Load libraries -->
202
+ <script src="/vendor/elysia-sse.js"></script>
203
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
204
+
205
+ <!-- 2. Connect to SSE channel -->
206
+ <script>
207
+ ElysiaSSE.connect('todos');
208
+ </script>
209
+
210
+ <!-- 3. Elements with x-sync and id will be auto-updated -->
211
+ <div x-sync id="todo-list" x-data="{ todos: [] }">
212
+ <template x-for="todo in todos">
213
+ <div x-text="todo.task"></div>
214
+ </template>
215
+ </div>
216
+
217
+ <!-- Multiple elements can be updated simultaneously -->
218
+ <div x-sync id="todo-stats">Total: 5</div>
219
+ <div x-sync id="user-badge">👤 John</div>
220
+ ```
221
+
222
+ ### Vanilla JavaScript
223
+
224
+ ```html
225
+ <script src="/vendor/elysia-sse.js"></script>
226
+ <script>
227
+ // Basic connection
228
+ ElysiaSSE.connect('notifications');
229
+
230
+ // With custom handler
231
+ ElysiaSSE.connect('messages', {
232
+ onUpdate: (data) => {
233
+ console.log('New message:', data);
234
+ // Custom DOM manipulation
235
+ },
236
+ onConnect: () => console.log('Connected!'),
237
+ onError: (err) => console.error('Error:', err)
238
+ });
239
+ </script>
240
+ ```
241
+
242
+ ### HTMX + SSE
243
+
244
+ ```html
245
+ <div x-data="{ todos: [] }" x-sync id="todo-list">
246
+ <template x-for="todo in todos">
247
+ <div x-text="todo.task"></div>
248
+ </template>
249
+ </div>
250
+
251
+ <script>
252
+ const eventSource = new EventSource('/stream/todos');
253
+
254
+ eventSource.onmessage = (event) => {
255
+ const { type, html, data } = JSON.parse(event.data);
256
+
257
+ // With Alpine AJAX: updates elements by ID
258
+ if (html) {
259
+ Alpine.morph(document.getElementById('todo-list'), html);
260
+ }
261
+ };
262
+ </script>
263
+ ```
264
+
265
+ ### HTMX + SSE
266
+
267
+ ```html
268
+ <div hx-ext="sse" sse-connect="/stream/todos">
269
+ <div id="todo-list" sse-swap="message">
270
+ <!-- Todo list -->
271
+ </div>
272
+ </div>
273
+ ```
274
+
275
+ ## 🔧 Use Cases
276
+
277
+ ### Real-time Dashboard
278
+
279
+ ```typescript
280
+ app.post('/analytics/track', ({ store, body }) => {
281
+ // Process event...
282
+
283
+ store.broadcast.broadcast('dashboard', userId, {
284
+ type: 'metric.updated',
285
+ data: { visitors: 1234, sales: 5678 }
286
+ });
287
+ });
288
+ ```
289
+
290
+ ### Notifications
291
+
292
+ ```typescript
293
+ // Send to specific user
294
+ app.post('/notifications/send', ({ store, body }) => {
295
+ store.broadcast.broadcast('notifications', recipientId, {
296
+ type: 'notification.new',
297
+ data: {
298
+ title: 'New comment',
299
+ message: 'Someone commented on your post'
300
+ }
301
+ });
302
+ });
303
+
304
+ // Send to all users (system announcement)
305
+ app.post('/admin/announce', ({ store, body }) => {
306
+ store.broadcast.broadcast('notifications', {
307
+ type: 'system.announcement',
308
+ data: {
309
+ title: 'System Update',
310
+ message: 'New features available!'
311
+ }
312
+ });
313
+ });
314
+ ```
315
+
316
+ ### Chat/Messages
317
+
318
+ ```typescript
319
+ app.post('/messages', ({ store, body }) => {
320
+ // Save message...
321
+
322
+ // Notify recipient
323
+ store.broadcast.broadcast('messages', recipientId, {
324
+ type: 'message.new',
325
+ data: { from: senderId, text: body.text }
326
+ });
327
+ });
328
+ ```
329
+
330
+ ## 🧪 Testing
331
+
332
+ ```bash
333
+ bun test
334
+ ```
335
+
336
+ ## 📝 Types
337
+
338
+ ```typescript
339
+ interface BroadcastEvent {
340
+ type: string;
341
+ data: any;
342
+ html?: string;
343
+ }
344
+
345
+ type BroadcastCallback = (event: BroadcastEvent) => void;
346
+ type UnsubscribeFunction = () => void;
347
+ ```
348
+
349
+ ## 🛠️ Build
350
+
351
+ ```bash
352
+ bun run build
353
+ ```
354
+
355
+ ## 📄 License
356
+
357
+ MIT
358
+
359
+ ## 🤝 Contributing
360
+
361
+ Contributions are welcome! Open an issue or PR.
package/dist/client.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * SSE (Server-Sent Events) client for elysia-broadcast
3
+ *
4
+ * 🎯 FULL COMPATIBILITY WITH ALPINE-AJAX:
5
+ * Processes elements with 'x-sync' attribute and updates using Alpine.morph,
6
+ * maintaining parity with alpine-ajax behavior in synchronous requests.
7
+ *
8
+ * @module elysia-broadcast/client
9
+ */
10
+
11
+ /**
12
+ * Connects to an SSE channel and manages automatic DOM updates
13
+ *
14
+ * @param {string} channel - SSE channel name
15
+ * @param {Object} options - Configuration options
16
+ * @param {string} options.basePath - SSE base path (default: '/stream')
17
+ * @param {Function} options.onUpdate - Custom handler for events
18
+ * @param {Function} options.onConnect - Callback when connected
19
+ * @param {Function} options.onError - Callback when error occurs
20
+ * @param {Function} options.onDisconnect - Callback when disconnected
21
+ * @returns {EventSource} EventSource instance
22
+ *
23
+ * @example
24
+ * // Basic usage
25
+ * ElysiaSSE.connect('todos');
26
+ *
27
+ * @example
28
+ * // With callbacks
29
+ * ElysiaSSE.connect('todos', {
30
+ * onConnect: () => console.log('Connected!'),
31
+ * onUpdate: (data) => console.log('Updated:', data)
32
+ * });
33
+ */
34
+ window.ElysiaSSE = {
35
+ connect: function (channel, options = {}) {
36
+ const {
37
+ basePath = '/stream',
38
+ onUpdate = null,
39
+ onConnect = null,
40
+ onError = null,
41
+ onDisconnect = null
42
+ } = options;
43
+
44
+ const url = basePath + '/' + channel;
45
+ const eventSource = new EventSource(url);
46
+
47
+ eventSource.onopen = function () {
48
+ console.log('🔌 [SSE] Conectado ao canal:', channel);
49
+ if (onConnect) onConnect();
50
+ };
51
+
52
+ eventSource.onmessage = function (event) {
53
+ try {
54
+ const data = JSON.parse(event.data);
55
+
56
+ // Custom handler has priority
57
+ if (onUpdate && typeof onUpdate === 'function') {
58
+ onUpdate(data);
59
+ return;
60
+ }
61
+
62
+ // Default processing: multiple elements with x-sync (like alpine-ajax)
63
+ if (data.html) {
64
+ const parser = new DOMParser();
65
+ const doc = parser.parseFromString(data.html, 'text/html');
66
+
67
+ // Search ALL elements with x-sync attribute (alpine-ajax compatibility)
68
+ const elementsWithSync = doc.querySelectorAll('[x-sync]');
69
+
70
+ if (elementsWithSync.length === 0) {
71
+ console.error('❌ [SSE] Nenhum elemento com atributo x-sync encontrado');
72
+ console.error('💡 [SSE] Adicione x-sync e id="..." nos elementos');
73
+ console.error('💡 [SSE] Exemplo: <div x-sync id="meu-elemento">...</div>');
74
+ return;
75
+ }
76
+
77
+ let updatedCount = 0;
78
+ let errors = [];
79
+
80
+ // Process each element (compatible with alpine-ajax multi-target)
81
+ elementsWithSync.forEach(function (newElement) {
82
+ const targetId = newElement.getAttribute('id');
83
+
84
+ if (!targetId) {
85
+ errors.push('Element ' + newElement.tagName + ' without id');
86
+ return;
87
+ }
88
+
89
+ const existingElement = document.getElementById(targetId);
90
+
91
+ if (!existingElement) {
92
+ errors.push('ID not found: ' + targetId);
93
+ return;
94
+ }
95
+
96
+ // Use Alpine.morph to preserve reactive state
97
+ if (window.Alpine && window.Alpine.morph) {
98
+ Alpine.morph(existingElement, newElement.outerHTML);
99
+ } else {
100
+ // Fallback: direct replacement
101
+ existingElement.outerHTML = newElement.outerHTML;
102
+ }
103
+
104
+ updatedCount++;
105
+ });
106
+
107
+ if (updatedCount > 0) {
108
+ console.log('✨ [SSE] ' + updatedCount + ' element(s) updated - ' + data.type);
109
+ }
110
+
111
+ if (errors.length > 0) {
112
+ console.error('❌ [SSE] Erros:', errors.join(', '));
113
+ }
114
+ }
115
+ } catch (error) {
116
+ console.error('❌ [SSE] Erro ao processar evento:', error);
117
+ }
118
+ };
119
+
120
+ eventSource.onerror = function (error) {
121
+ console.error('❌ [SSE] Connection error:', error);
122
+
123
+ if (eventSource.readyState === EventSource.CLOSED) {
124
+ console.log('🔄 [SSE] Reconnecting in 5 seconds...');
125
+ setTimeout(function () {
126
+ window.location.reload();
127
+ }, 5000);
128
+
129
+ if (onError) onError(error);
130
+ }
131
+ };
132
+
133
+ // Close connection when page is unloaded
134
+ window.addEventListener('beforeunload', function () {
135
+ eventSource.close();
136
+ console.log('👋 [SSE] Desconectado do canal:', channel);
137
+ if (onDisconnect) onDisconnect();
138
+ });
139
+
140
+ return eventSource;
141
+ }
142
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * elysia-broadcast
3
+ *
4
+ * Broadcast and SSE (Server-Sent Events) system for Elysia
5
+ * with support for multiple channels per user
6
+ *
7
+ * @module elysia-broadcast
8
+ */
9
+ export { BroadcastManager } from './manager';
10
+ export { broadcastPlugin, broadcastManager, alpineRequest } from './plugin';
11
+ export { streamPlugin } from './stream';
12
+ export type { BroadcastEvent, BroadcastCallback, UnsubscribeFunction, BroadcastPluginOptions, StreamPluginOptions } from './types';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,SAAS,CAAC"}