evstream 1.0.1 → 1.0.3

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 CHANGED
@@ -1,674 +1,844 @@
1
- # `evstream`
2
-
3
- A simple, easy, and lightweight Server-Sent Events (SSE) library for Node.js that simplifies managing SSE connections, broadcasting events, and maintaining reactive state. It works out of the box with any backend library that supports native `IncomingMessage` and `ServerResponse` objects for IO.
4
-
5
- ## Features
6
-
7
- - Manage multiple SSE connections with centralized control.
8
- - Broadcast events to channels (event names).
9
- - Built-in support for connection and listener limits.
10
- - Optional token-based authentication per connection.
11
- - Heartbeat support to keep connections alive.
12
- - Reactive state management with automatic broadcasting.
13
-
14
- ## Installation
15
-
16
- ```bash
17
- npm install evstream
18
- ```
19
-
20
- ## Usage
21
-
22
- We used `express.js` to show you the usage. However you can use the library with any backend library or framework supporting `IncomingMessage` and `ServerResponse` objects for IO.
23
-
24
- ### 1. Creating a base SSE Connection
25
-
26
- ```javascript
27
- import { Evstream } from 'evstream'
28
-
29
- app.get("/", (req, res) => {
30
- const stream = new Evstream(req, res, { heartbeat: 5000 })
31
-
32
- stream.message({ event: "connected", data: { userId: "a-user-id" } })
33
-
34
- setTimeout(() => {
35
- stream.close();
36
- }, 5000)
37
- })
38
- ```
39
-
40
- Client Recieves :
41
- ```
42
- event:connected
43
- data:{"userId":"a-user-id"}
44
-
45
- event:heartbeat
46
- data:
47
-
48
- event:end
49
- data:
50
- ```
51
-
52
- ### 2. Creating a SSE Connection with query based authentication
53
-
54
- ```javascript
55
- app.get("/", async (req, res) => {
56
- const stream = new Evstream(req, res, {
57
- heartbeat: 5000, authentication: {
58
- method: "query",
59
- param: "token",
60
- verify: async (token) => false
61
- }
62
- })
63
-
64
- const isAuthenticated = await stream.authenticate();
65
-
66
- if (!isAuthenticated) {
67
- return;
68
- }
69
-
70
- stream.message({ event: "connected", data: { userId: "a-user-id" } })
71
-
72
- setTimeout(() => {
73
- stream.close();
74
- }, 5000)
75
- })
76
- ```
77
-
78
- To test this out URL should be `/?token=<auth-token>`.
79
-
80
- You'll get the query parameter value one the callback function's parameter passed to `verify` field.
81
- You can either return boolean values or `EvMessage`.
82
-
83
- #### Authentication
84
-
85
- To authenticate the incoming request there is a built-in support in `evstream`. You can verify the query based token verification which is generally not recommended.
86
-
87
- - Options :
88
- - `method` : Authentication method to use (`"query"`).
89
- - `param` : Field or parameter in `query` which holds the authentication token.
90
- - `verify` : A callback function to check the token. If `false` returned req will get close.
91
-
92
- `evstream` by default doesn't authenticate the request. You have to call the `authenticate` function from `Evstream` class to verify. If false returned you have to stop processing the request and return immediately.
93
-
94
- ```javascript
95
- const isAuthenticated = await stream.authenticate()
96
- ```
97
-
98
-
99
- ### 3. Creating a stream manager
100
-
101
- Using `EvStreamManager` you can broadcast messages, create channels and manage connections in a much better way.
102
-
103
- ```javascript
104
- const manager = new EvStreamManager();
105
-
106
- app.get("/", (req, res) => {
107
- const stream = manager.createStream(req, res)
108
-
109
- const i = setInterval(() => {
110
- stream.message({ data: { hello: "hii" } })
111
- }, 2000)
112
-
113
- stream.message({ data: { why: "hii" }, event: "hello" })
114
-
115
- setTimeout(() => {
116
- clearTimeout(i);
117
- stream.close();
118
- }, 10000)
119
- })
120
- ```
121
-
122
- ### 4. Using Reactive State
123
-
124
- Reactive states are data which you can shared across multiple clients within the same server. Whenever the data gets updated each client listening to that data get notified with an SSE message.
125
-
126
- - #### Creating a reactive States
127
-
128
- ```javascript
129
- import { EvState, EvStreamManager } from "evstream"
130
-
131
- const manager = new EvStreamManager();
132
- const userCount = new EvState({ channel: "user-count", initialValue: 0, manager: manager })
133
- ```
134
-
135
- To create a reactive value you can use `EvState` class which takes a `channel` which is then listened by the connected client for any update.
136
-
137
- - `channel` : A unique name to which client will listen to for state changes.
138
- - `initialValue` : Default value for the state.
139
- - `manager` : Connection manager for the connected clients.
140
-
141
- **Getting the state data**
142
-
143
- ```javascript
144
- userCount.get();
145
- ```
146
-
147
- **Updating State data**
148
-
149
- ```javascript
150
- userCount.set((prev) => prev += 1);
151
- ```
152
- This will update the values and send the data to all clients which are listening for the state changes.
153
-
154
-
155
- - #### Listening for a reactive state
156
- ```javascript
157
- import { EvState, EvStreamManager } from "evstream"
158
-
159
-
160
- const manager = new EvStreamManager();
161
- const userCount = new EvState({ channel: "user-count", initialValue: 0, manager: manager })
162
-
163
-
164
- app.get("/", (req, res) => {
165
- const stream = manager.createStream(req, res)
166
- stream.listen("user-count")
167
- userCount.set((user) => user + 1);
168
-
169
- const i = setInterval(() => {
170
- stream.message({ data: { hello: "hii" } })
171
- }, 2000)
172
-
173
- stream.message({ data: { why: "hii" }, event: "hello" })
174
-
175
- setTimeout(() => {
176
- clearTimeout(i);
177
- stream.close((channels) => {
178
-
179
- userCount.set((user) => user - 1)
180
-
181
- console.log(channels)
182
- });
183
- }, 10000)
184
- })
185
- ```
186
-
187
- This will now listen for a state change in `userCount` variables and push the update to all the connected client listening for that state.
188
-
189
- **See** `channel` **and the value pass to the** `listen()` **must be the same**
190
-
191
- ### 5. Distributed Reactive State (Redis)
192
-
193
- When running multiple server instances, you can synchronize `EvState` across them using the built-in Redis adapter.
194
-
195
- 1. **Install the peer dependency:**
196
- ```bash
197
- npm install ioredis
198
- ```
199
-
200
- 2. **Use the adapter:**
201
-
202
- ```javascript
203
- import { EvState, EvStreamManager } from "evstream"
204
- import { EvRedisAdapter } from "evstream/adapter/redis"
205
-
206
- const manager = new EvStreamManager();
207
- const redisAdapter = new EvRedisAdapter("redis://localhost:6379");
208
-
209
- const userCount = new EvState({
210
- channel: "user-count",
211
- initialValue: 0,
212
- manager: manager,
213
- adapter: redisAdapter
214
- })
215
- ```
216
-
217
- Updates to `userCount` will now be synchronized across all instances connected to the same Redis.
218
-
219
- ### 6. Sending data to a channel
220
-
221
- To send data to a channel you can use `send()` method from `EvStreamManager` class.
222
-
223
- Example :
224
-
225
- ```javascript
226
- import { EvStreamManager } from "evstream"
227
-
228
- const manager = new EvStreamManager();
229
-
230
- manager.send("<channel-name>", {event: "custom-event", data: {"foo": "bar"}})
231
- ```
232
-
233
- ### 7. Listening for channels
234
-
235
- To listen for data from any channel you can use `listen()` function from `Evstream` class.
236
-
237
- ```javascript
238
- client.listen("<channel-name>")
239
- ```
240
-
241
- ## API Reference
242
-
243
- ## `Evstream`
244
-
245
- Manages a Server-Sent Events (SSE) connection. Handles headers, heartbeat intervals, authentication, sending messages, and closing the stream.
246
-
247
- ### Constructor
248
-
249
- ```js
250
- new Evstream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions)
251
- ```
252
-
253
- #### Parameters:
254
-
255
- * `req`: `IncomingMessage` The incoming HTTP request.
256
- * `res`: `ServerResponse` The HTTP response to write SSE messages to.
257
- * `opts` *(optional)*: `EvOptions` Optional configuration including heartbeat interval and authentication.
258
-
259
- ---
260
-
261
- ### Methods
262
-
263
- #### `authenticate(): Promise<boolean | undefined>`
264
-
265
- Performs optional token-based authentication if `opts.authentication` is provided.
266
-
267
- * If authentication fails, sends an error message and closes the connection.
268
- * Returns `true` if authenticated, `false` if rejected, or `undefined` if no authentication is configured.
269
-
270
- ---
271
-
272
- #### `message(msg: EvMessage): void`
273
-
274
- Sends an SSE message to the connected client.
275
-
276
- ##### Parameters:
277
-
278
- * `msg`: `EvMessage` Object containing `event`, `data`, and optionally `id`.
279
-
280
- ---
281
-
282
- #### `close(): void`
283
-
284
- Sends a final `end` event and closes the SSE connection.
285
-
286
- ---
287
-
288
- ### Example
289
-
290
- ```js
291
- const ev = new Evstream(req, res, {
292
- heartbeat: 30000,
293
- authentication: {
294
- param: 'token',
295
- verify: async (token) => token === 'valid_token'
296
- }
297
- })
298
-
299
- await ev.authenticate()
300
- ev.message({ event: 'message', data: { text: 'Hello world' }, id: '1' })
301
- ev.close()
302
- ```
303
-
304
- ---
305
-
306
- ## `EvStreamManager`
307
-
308
- Manages multiple Server-Sent Events (SSE) client streams. Supports connection tracking, message broadcasting, and channel-based listeners.
309
-
310
- ### Constructor
311
-
312
- ```js
313
- new EvStreamManager(opts?: EvManagerOptions)
314
- ```
315
-
316
- #### Parameters:
317
-
318
- * `opts` *(optional)*: `EvManagerOptions`
319
-
320
- * `maxConnection`: Maximum allowed active connections (default: `5000`)
321
- * `maxListeners`: Maximum listeners per channel (default: `5000`)
322
- * `id`: Optional prefix for client IDs
323
-
324
- ---
325
-
326
- ### Methods
327
-
328
- #### `createStream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions): { authenticate, message, close, listen }`
329
-
330
- Creates and tracks a new SSE stream.
331
-
332
- #### Parameters:
333
-
334
- * `req`: `IncomingMessage` – Incoming HTTP request
335
- * `res`: `ServerResponse` – HTTP response for the SSE connection
336
- * `opts` *(optional)*: `EvOptions` Optional stream config (heartbeat, authentication, etc.)
337
-
338
- #### Returns:
339
-
340
- An object with methods:
341
-
342
- * `authenticate(): Promise<boolean | undefined>` – Authenticates the stream (delegates to `Evstream`)
343
- * `message(msg: EvMessage): void` – Sends a message to the stream
344
- * `close(onClose?: EvOnClose): void` – Closes the stream and cleans up listeners
345
- * `listen(name: string): void` – Subscribes the stream to a named channel
346
-
347
- ---
348
-
349
- #### `send(name: string, msg: EvMessage): void`
350
-
351
- Broadcasts a message to all clients listening on the specified `name` (channel).
352
-
353
- ##### Parameters:
354
-
355
- * `name`: `string` – Channel name
356
- * `msg`: `EvMessage` – The message to broadcast
357
-
358
- ---
359
-
360
- ### Private Methods
361
-
362
- #### `#listen(name: string, id: string): void`
363
-
364
- Adds a client (by ID) to a named channel. Throws `EvMaxListenerError` if channel exceeds max listeners.
365
-
366
- ---
367
-
368
- #### `#unlisten(name: string, id: string): void`
369
-
370
- Removes a client from a channel. Deletes the channel if no listeners remain.
371
-
372
- ---
373
-
374
- ### Example
375
-
376
- ```js
377
- const manager = new EvStreamManager()
378
-
379
- const stream = manager.createStream(req, res)
380
-
381
- await stream.authenticate()
382
- stream.listen('news')
383
- stream.message({ event: 'hello', data: 'welcome' })
384
-
385
- manager.send('news', { event: 'news', data: 'breaking update' })
386
- ```
387
-
388
- ## `EvState<T>`
389
-
390
- Reactive state holder that broadcasts updates to a specified channel via an `EvStreamManager`. Designed for real-time state syncing over Server-Sent Events (SSE).
391
-
392
- ### Constructor
393
-
394
- ```ts
395
- new EvState<T>({
396
- channel,
397
- initialValue,
398
- manager,
399
- key,
400
- adapter
401
- }: EvStateOptions<T>)
402
- ```
403
-
404
- #### Parameters:
405
-
406
- * `channel`: `string` – The name of the channel to broadcast updates to.
407
- * `initialValue`: `T` – The initial state value.
408
- * `manager`: `EvStreamManager` The SSE manager instance used for broadcasting.
409
- * `key` *(optional)*: `string` – The key used in the broadcasted data object (default: `'value'`).
410
- * `adapter` *(optional)*: `EvStateAdapter` Adapter for distributed state synchronization (e.g. `EvRedisAdapter`).
411
-
412
- ---
413
-
414
- ### Methods
415
-
416
- #### `get(): T`
417
-
418
- Returns the current value of the state.
419
-
420
- ---
421
-
422
- #### `set(callback: (val: T) => T): void`
423
-
424
- Updates the internal state based on a callback function. If the new value is different (deep comparison), it broadcasts the updated value to the channel.
425
-
426
- ##### Parameters:
427
-
428
- * `callback`: `(val: T) => T` – A function that receives the current state and returns the new state.
429
-
430
- ---
431
-
432
- ### Example
433
-
434
- ```ts
435
- const state = new EvState({
436
- channel: 'counter',
437
- initialValue: 0,
438
- manager: evManager,
439
- key: 'count'
440
- })
441
-
442
- state.set(prev => prev + 1)
443
- // Will broadcast: { event: 'counter', data: { count: 1 } }
444
-
445
- const current = state.get()
446
- // current === 1
447
- ```
448
-
449
- ---
450
-
451
- ## `EvMaxConnectionsError`
452
-
453
- Represents an error thrown when the number of active SSE connections exceeds the allowed `maxConnection` limit (default: `5000`).
454
-
455
- ### Constructor
456
-
457
- ```ts
458
- new EvMaxConnectionsError(connections: number)
459
- ```
460
-
461
- #### Parameters:
462
-
463
- - `connections`: `number` – The current number of active connections when the limit is exceeded.
464
-
465
- #### Example
466
-
467
- ```ts
468
- const manager = new EvStreamManager({ maxConnection: 100 });
469
- if (tooManyConnections) {
470
- ```
471
-
472
- throw new EvMaxConnectionsError(100)
473
- }
474
- ```
475
-
476
- ---
477
-
478
- ## `EvRedisAdapter`
479
-
480
- Adapter for synchronizing `EvState` across multiple instances using Redis Pub/Sub.
481
-
482
- ### Constructor
483
-
484
- ```ts
485
- new EvRedisAdapter(options?: RedisOptions | string)
486
- ```
487
-
488
- #### Parameters:
489
-
490
- * `options`: `RedisOptions | string` – Configuration options for the Redis client (from `ioredis`), or a Redis connection URL.
491
-
492
- ---
493
-
494
- ## `EvMaxListenerError`
495
-
496
- Represents an error thrown when the number of listeners on a given channel exceeds the allowed `maxListeners` limit (default: `5000`).
497
-
498
- ### Constructor
499
-
500
- ```ts
501
- new EvMaxListenerError(listeners: number, channel: string)
502
- ```
503
-
504
- #### Parameters:
505
-
506
- - `listeners`: `number` – The current number of listeners on the channel.
507
- - `channel`: `string` – The name of the channel that exceeded the listener limit.
508
-
509
- #### Example
510
-
511
- ```ts
512
- if (tooManyListenersOnChannel) {
513
- throw new EvMaxListenerError(5000, 'news')
514
- }
515
- ```
516
-
517
- ---
518
-
519
- ## Type Definitions
520
-
521
- ---
522
-
523
- ### `EvEventsType`
524
-
525
- ```ts
526
- type EvEventsType = 'data' | 'error' | 'end'
527
- ```
528
-
529
- Represents built-in event types commonly used in Server-Sent Events.
530
-
531
- ---
532
-
533
- ### `EvMessage`
534
-
535
- ```ts
536
- interface EvMessage {
537
- event?: string | EvEventsType
538
- data: string | object
539
- id?: string
540
- }
541
- ```
542
-
543
- Represents a message sent to the client via SSE.
544
-
545
- - `event` *(optional)*: Name of the event.
546
- - `data`: The payload to send. Can be a string or an object.
547
- - `id` *(optional)*: Event ID for reconnection tracking.
548
-
549
- ---
550
-
551
- ### `EvAuthenticationOptions`
552
-
553
- ```ts
554
- interface EvAuthenticationOptions {
555
- method: 'query'
556
- param: string
557
- verify: (token: string) => Promise<EvMessage> | undefined | null | boolean
558
- }
559
- ```
560
-
561
- Options for enabling query-based token authentication.
562
-
563
- - `method`: Always `'query'`
564
- - `param`: Name of the query parameter containing the token.
565
- - `verify`: Async verification function. Can return:
566
- - `true` (authenticated)
567
- - `false` (rejected)
568
- - `EvMessage` (custom response)
569
- - `undefined` / `null` (unauthenticated)
570
-
571
- ---
572
-
573
- ### `EvOptions`
574
-
575
- ```ts
576
- interface EvOptions {
577
- authentication?: EvAuthenticationOptions
578
- heartbeat?: number
579
- }
580
- ```
581
-
582
- Optional config for an individual SSE stream.
583
-
584
- - `authentication`: Auth configuration (see `EvAuthenticationOptions`)
585
- - `heartbeat`: Interval in milliseconds for sending heartbeat events
586
-
587
- ---
588
-
589
- ### `EvManagerOptions`
590
-
591
- ```ts
592
- interface EvManagerOptions {
593
- id?: string
594
- maxConnection?: number
595
- maxListeners?: number
596
- }
597
- ```
598
-
599
- Configuration for `EvStreamManager`.
600
-
601
- - `id`: Optional prefix for client IDs
602
- - `maxConnection`: Max allowed connections (default: `5000`)
603
- - `maxListeners`: Max listeners per channel (default: `5000`)
604
-
605
- ---
606
-
607
- ### `EvStateOptions<T>`
608
-
609
- ```ts
610
- interface EvStateOptions<T> {
611
- initialValue: T
612
- channel: string
613
- manager: EvStreamManager
614
- key?: string
615
- }
616
- ```
617
-
618
- Options for initializing a reactive state with `EvState`.
619
-
620
- - `initialValue`: Initial state value
621
- - `channel`: Channel name for broadcasting
622
- - `manager`: Instance of `EvStreamManager`
623
- - `key` *(optional)*: Key for wrapping state in the broadcast (default: `'value'`)
624
- - `adapter` *(optional)*: Instance of `EvStateAdapter` (e.g., `EvRedisAdapter`) for distributed synchronization.
625
-
626
- ---
627
-
628
- ### `EvOnClose`
629
-
630
- ```ts
631
- type EvOnClose = (channels: string[]) => Promise<void>
632
- ```
633
-
634
- Callback triggered when a client connection is closed. Receives a list of channels the client was subscribed to.
635
-
636
- ---
637
-
638
- ## 🤝 Contribution
639
-
640
- Contributions are welcome! Whether it's a bug fix, feature request, or improvement to documentation, your help is appreciated.
641
-
642
- ### How to Contribute:
643
-
644
- 1. **Fork** the repository.
645
- 2. **Create a branch** for your feature or fix:
646
-
647
- ```bash
648
- git checkout -b feature/your-feature-name
649
- ```
650
- 3. **Commit your changes** with a clear message.
651
- 4. **Push to your fork**:
652
-
653
- ```bash
654
- git push origin feature/your-feature-name
655
- ```
656
- 5. **Open a Pull Request** and describe your changes.
657
-
658
- ### Guidelines:
659
-
660
- * Keep your code clean and consistent with the project's existing style.
661
- * Include relevant tests and documentation updates.
662
- * Make sure the project builds and passes all existing checks.
663
-
664
- ---
665
-
666
- ## 📄 License
667
-
668
- This project is licensed under the **MIT License**.
669
-
670
- You are free to use, modify, distribute, and sublicense this software for both personal and commercial use — provided that the original license and copyright notice are included in all copies.
671
-
672
- > See the [LICENSE](./LICENSE) file for full details.
673
-
674
- ---
1
+ # `evstream`
2
+
3
+ A simple, easy, and lightweight Server-Sent Events (SSE) library for Node.js that simplifies managing SSE connections, broadcasting events, and maintaining reactive state. It works out of the box with any backend library that supports native `IncomingMessage` and `ServerResponse` objects for IO.
4
+
5
+ ## Features
6
+
7
+ - Manage multiple SSE connections with centralized control.
8
+ - Broadcast events to channels (event names).
9
+ - Built-in support for connection and listener limits.
10
+ - Optional token-based authentication per connection.
11
+ - Heartbeat support to keep connections alive.
12
+ - Reactive state management with automatic broadcasting.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install evstream
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ We used `express.js` to show you the usage. However you can use the library with any backend library or framework supporting `IncomingMessage` and `ServerResponse` objects for IO.
23
+
24
+ ### 1. Creating a base SSE Connection
25
+
26
+ ```javascript
27
+ import { Evstream } from 'evstream'
28
+
29
+ app.get('/', (req, res) => {
30
+ const stream = new Evstream(req, res, { heartbeat: 5000 })
31
+
32
+ stream.message({ event: 'connected', data: { userId: 'a-user-id' } })
33
+
34
+ setTimeout(() => {
35
+ stream.close()
36
+ }, 5000)
37
+ })
38
+ ```
39
+
40
+ Client Recieves :
41
+
42
+ ```
43
+ event:connected
44
+ data:{"userId":"a-user-id"}
45
+
46
+ event:heartbeat
47
+ data:
48
+
49
+ event:end
50
+ data:
51
+ ```
52
+
53
+ ### 2. Creating a SSE Connection with query based authentication
54
+
55
+ ```javascript
56
+ app.get('/', async (req, res) => {
57
+ const stream = new Evstream(req, res, {
58
+ heartbeat: 5000,
59
+ authentication: {
60
+ method: 'query',
61
+ param: 'token',
62
+ verify: async (token) => false,
63
+ },
64
+ })
65
+
66
+ const isAuthenticated = await stream.authenticate()
67
+
68
+ if (!isAuthenticated) {
69
+ return
70
+ }
71
+
72
+ stream.message({ event: 'connected', data: { userId: 'a-user-id' } })
73
+
74
+ setTimeout(() => {
75
+ stream.close()
76
+ }, 5000)
77
+ })
78
+ ```
79
+
80
+ To test this out URL should be `/?token=<auth-token>`.
81
+
82
+ You'll get the query parameter value one the callback function's parameter passed to `verify` field.
83
+ You can either return boolean values or `EvMessage`.
84
+
85
+ #### Authentication
86
+
87
+ To authenticate the incoming request there is a built-in support in `evstream`. You can verify the query based token verification which is generally not recommended.
88
+
89
+ - Options :
90
+ - `method` : Authentication method to use (`"query"`).
91
+ - `param` : Field or parameter in `query` which holds the authentication token.
92
+ - `verify` : A callback function to check the token. If `false` returned req will get close.
93
+
94
+ `evstream` by default doesn't authenticate the request. You have to call the `authenticate` function from `Evstream` class to verify. If false returned you have to stop processing the request and return immediately.
95
+
96
+ ```javascript
97
+ const isAuthenticated = await stream.authenticate()
98
+ ```
99
+
100
+ ### 3. Creating a stream manager
101
+
102
+ Using `EvStreamManager` you can broadcast messages, create channels and manage connections in a much better way.
103
+
104
+ ```javascript
105
+ const manager = new EvStreamManager()
106
+
107
+ app.get('/', (req, res) => {
108
+ const stream = manager.createStream(req, res)
109
+
110
+ const i = setInterval(() => {
111
+ stream.message({ data: { hello: 'hii' } })
112
+ }, 2000)
113
+
114
+ stream.message({ data: { why: 'hii' }, event: 'hello' })
115
+
116
+ setTimeout(() => {
117
+ clearTimeout(i)
118
+ stream.close()
119
+ }, 10000)
120
+ })
121
+ ```
122
+
123
+ ### 4. Using Reactive State
124
+
125
+ Reactive states are data which you can shared across multiple clients within the same server. Whenever the data gets updated each client listening to that data get notified with an SSE message.
126
+
127
+ - #### Creating a reactive States
128
+
129
+ ```javascript
130
+ import { EvState, EvStreamManager } from 'evstream'
131
+
132
+ const manager = new EvStreamManager()
133
+ const userCount = new EvState({
134
+ channel: 'user-count',
135
+ initialValue: 0,
136
+ manager: manager,
137
+ })
138
+ ```
139
+
140
+ To create a reactive value you can use `EvState` class which takes a `channel` which is then listened by the connected client for any update.
141
+ - `channel` : A unique name to which client will listen to for state changes.
142
+ - `initialValue` : Default value for the state.
143
+ - `manager` : Connection manager for the connected clients.
144
+
145
+ **Getting the state data**
146
+
147
+ ```javascript
148
+ userCount.get()
149
+ ```
150
+
151
+ **Updating State data**
152
+
153
+ ```javascript
154
+ userCount.set((prev) => (prev += 1))
155
+ ```
156
+
157
+ This will update the values and send the data to all clients which are listening for the state changes.
158
+
159
+ - #### Listening for a reactive state
160
+
161
+ ```javascript
162
+ import { EvState, EvStreamManager } from 'evstream'
163
+
164
+ const manager = new EvStreamManager()
165
+ const userCount = new EvState({
166
+ channel: 'user-count',
167
+ initialValue: 0,
168
+ manager: manager,
169
+ })
170
+
171
+ app.get('/', (req, res) => {
172
+ const stream = manager.createStream(req, res)
173
+ stream.listen('user-count')
174
+ userCount.set((user) => user + 1)
175
+
176
+ const i = setInterval(() => {
177
+ stream.message({ data: { hello: 'hii' } })
178
+ }, 2000)
179
+
180
+ stream.message({ data: { why: 'hii' }, event: 'hello' })
181
+
182
+ setTimeout(() => {
183
+ clearTimeout(i)
184
+ stream.close((channels) => {
185
+ userCount.set((user) => user - 1)
186
+
187
+ console.log(channels)
188
+ })
189
+ }, 10000)
190
+ })
191
+ ```
192
+
193
+ This will now listen for a state change in `userCount` variables and push the update to all the connected client listening for that state.
194
+
195
+ **See** `channel` **and the value pass to the** `listen()` **must be the same**
196
+
197
+ ### 5. Distributed Reactive State (Redis)
198
+
199
+ When running multiple server instances, you can synchronize `EvState` across them using the built-in Redis adapter.
200
+
201
+ 1. **Install the peer dependency:**
202
+
203
+ ```bash
204
+ npm install ioredis
205
+ ```
206
+
207
+ 2. **Use the adapter:**
208
+
209
+ ```javascript
210
+ import { EvState, EvStreamManager } from 'evstream'
211
+ import { EvRedisAdapter } from 'evstream/adapter/redis'
212
+
213
+ const manager = new EvStreamManager()
214
+ const redisAdapter = new EvRedisAdapter('redis://localhost:6379')
215
+
216
+ const userCount = new EvState({
217
+ channel: 'user-count',
218
+ initialValue: 0,
219
+ manager: manager,
220
+ adapter: redisAdapter,
221
+ })
222
+ ```
223
+
224
+ Updates to `userCount` will now be synchronized across all instances connected to the same Redis.
225
+
226
+ ### 6. Sending data to a channel
227
+
228
+ To send data to a channel you can use `send()` method from `EvStreamManager` class.
229
+
230
+ Example :
231
+
232
+ ```javascript
233
+ import { EvStreamManager } from 'evstream'
234
+
235
+ const manager = new EvStreamManager()
236
+
237
+ manager.send('<channel-name>', { event: 'custom-event', data: { foo: 'bar' } })
238
+ ```
239
+
240
+ ### 7. Listening for channels
241
+
242
+ To listen for data from any channel you can use `listen()` function from `Evstream` class.
243
+
244
+ ```javascript
245
+ client.listen('<channel-name>')
246
+ ```
247
+
248
+ ### 8. Shared State (EvStateManager)
249
+
250
+ When running multiple server instances, you may want state creation and removal to stay in sync across all instances.
251
+
252
+ EvStateManager helps manage shared reactive states and keeps their lifecycle consistent using Pub/Sub.
253
+
254
+ ```typescript
255
+ import { EvStreamManager } from 'evstream'
256
+ import { EvRedisAdapter } from 'evstream/adapter/redis'
257
+ import { EvRedisPubSub } from 'evstream/adapter/pub-sub'
258
+ import { EvStateManager } from 'evstream/state-manager'
259
+
260
+ const streamManager = new EvStreamManager()
261
+ const adapter = new EvRedisAdapter('redis://localhost:6379')
262
+ const pubsub = new EvRedisPubSub({
263
+ subject: 'ev:states',
264
+ options: { host: 'localhost', port: 6379 },
265
+ })
266
+
267
+ const stateManager = new EvStateManager({
268
+ manager: streamManager,
269
+ adapter,
270
+ pubsub,
271
+ })
272
+
273
+ const userCount = stateManager.createState('user-count', 0)
274
+ ```
275
+
276
+ **Notes**
277
+
278
+ - States are identified by string-based keys
279
+ - State creation and removal are synchronized across instances
280
+ - State updates are still handled by EvState
281
+
282
+ ## API Reference
283
+
284
+ ## `Evstream`
285
+
286
+ Manages a Server-Sent Events (SSE) connection. Handles headers, heartbeat intervals, authentication, sending messages, and closing the stream.
287
+
288
+ ### Constructor
289
+
290
+ ```js
291
+ new Evstream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions)
292
+ ```
293
+
294
+ #### Parameters:
295
+
296
+ - `req`: `IncomingMessage` – The incoming HTTP request.
297
+ - `res`: `ServerResponse` – The HTTP response to write SSE messages to.
298
+ - `opts` _(optional)_: `EvOptions` – Optional configuration including heartbeat interval and authentication.
299
+
300
+ ---
301
+
302
+ ### Methods
303
+
304
+ #### `authenticate(): Promise<boolean | undefined>`
305
+
306
+ Performs optional token-based authentication if `opts.authentication` is provided.
307
+
308
+ - If authentication fails, sends an error message and closes the connection.
309
+ - Returns `true` if authenticated, `false` if rejected, or `undefined` if no authentication is configured.
310
+
311
+ ---
312
+
313
+ #### `message(msg: EvMessage): void`
314
+
315
+ Sends an SSE message to the connected client.
316
+
317
+ ##### Parameters:
318
+
319
+ - `msg`: `EvMessage` – Object containing `event`, `data`, and optionally `id`.
320
+
321
+ ---
322
+
323
+ #### `close(): void`
324
+
325
+ Sends a final `end` event and closes the SSE connection.
326
+
327
+ ---
328
+
329
+ ### Example
330
+
331
+ ```js
332
+ const ev = new Evstream(req, res, {
333
+ heartbeat: 30000,
334
+ authentication: {
335
+ param: 'token',
336
+ verify: async (token) => token === 'valid_token',
337
+ },
338
+ })
339
+
340
+ await ev.authenticate()
341
+ ev.message({ event: 'message', data: { text: 'Hello world' }, id: '1' })
342
+ ev.close()
343
+ ```
344
+
345
+ ---
346
+
347
+ ## `EvStreamManager`
348
+
349
+ Manages multiple Server-Sent Events (SSE) client streams. Supports connection tracking, message broadcasting, and channel-based listeners.
350
+
351
+ ### Constructor
352
+
353
+ ```js
354
+ new EvStreamManager(opts?: EvManagerOptions)
355
+ ```
356
+
357
+ #### Parameters:
358
+
359
+ - `opts` _(optional)_: `EvManagerOptions`
360
+ - `maxConnection`: Maximum allowed active connections (default: `5000`)
361
+ - `maxListeners`: Maximum listeners per channel (default: `5000`)
362
+ - `id`: Optional prefix for client IDs
363
+
364
+ ---
365
+
366
+ ### Methods
367
+
368
+ #### `createStream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions): { authenticate, message, close, listen }`
369
+
370
+ Creates and tracks a new SSE stream.
371
+
372
+ #### Parameters:
373
+
374
+ - `req`: `IncomingMessage` – Incoming HTTP request
375
+ - `res`: `ServerResponse` – HTTP response for the SSE connection
376
+ - `opts` _(optional)_: `EvOptions` – Optional stream config (heartbeat, authentication, etc.)
377
+
378
+ #### Returns:
379
+
380
+ An object with methods:
381
+
382
+ - `authenticate(): Promise<boolean | undefined>` – Authenticates the stream (delegates to `Evstream`)
383
+ - `message(msg: EvMessage): void` Sends a message to the stream
384
+ - `close(onClose?: EvOnClose): void` – Closes the stream and cleans up listeners
385
+ - `listen(name: string): void` Subscribes the stream to a named channel
386
+
387
+ ---
388
+
389
+ #### `send(name: string, msg: EvMessage): void`
390
+
391
+ Broadcasts a message to all clients listening on the specified `name` (channel).
392
+
393
+ ##### Parameters:
394
+
395
+ - `name`: `string` – Channel name
396
+ - `msg`: `EvMessage` – The message to broadcast
397
+
398
+ ---
399
+
400
+ ### Private Methods
401
+
402
+ #### `#listen(name: string, id: string): void`
403
+
404
+ Adds a client (by ID) to a named channel. Throws `EvMaxListenerError` if channel exceeds max listeners.
405
+
406
+ ---
407
+
408
+ #### `#unlisten(name: string, id: string): void`
409
+
410
+ Removes a client from a channel. Deletes the channel if no listeners remain.
411
+
412
+ ---
413
+
414
+ ### Example
415
+
416
+ ```js
417
+ const manager = new EvStreamManager()
418
+
419
+ const stream = manager.createStream(req, res)
420
+
421
+ await stream.authenticate()
422
+ stream.listen('news')
423
+ stream.message({ event: 'hello', data: 'welcome' })
424
+
425
+ manager.send('news', { event: 'news', data: 'breaking update' })
426
+ ```
427
+
428
+ ## `EvState<T>`
429
+
430
+ Reactive state holder that broadcasts updates to a specified channel via an `EvStreamManager`. Designed for real-time state syncing over Server-Sent Events (SSE).
431
+
432
+ ### Constructor
433
+
434
+ ```ts
435
+ new EvState<T>({
436
+ channel,
437
+ initialValue,
438
+ manager,
439
+ key,
440
+ adapter
441
+ }: EvStateOptions<T>)
442
+ ```
443
+
444
+ #### Parameters:
445
+
446
+ - `channel`: `string` – The name of the channel to broadcast updates to.
447
+ - `initialValue`: `T` – The initial state value.
448
+ - `manager`: `EvStreamManager` – The SSE manager instance used for broadcasting.
449
+ - `key` _(optional)_: `string` – The key used in the broadcasted data object (default: `'value'`).
450
+ - `adapter` _(optional)_: `EvStateAdapter` – Adapter for distributed state synchronization (e.g. `EvRedisAdapter`).
451
+
452
+ ---
453
+
454
+ ### Methods
455
+
456
+ #### `get(): T`
457
+
458
+ Returns the current value of the state.
459
+
460
+ ---
461
+
462
+ #### `set(callback: (val: T) => T): void`
463
+
464
+ Updates the internal state based on a callback function. If the new value is different (deep comparison), it broadcasts the updated value to the channel.
465
+
466
+ ##### Parameters:
467
+
468
+ - `callback`: `(val: T) => T` A function that receives the current state and returns the new state.
469
+
470
+ ---
471
+
472
+ ### Example
473
+
474
+ ```ts
475
+ const state = new EvState({
476
+ channel: 'counter',
477
+ initialValue: 0,
478
+ manager: evManager,
479
+ key: 'count',
480
+ })
481
+
482
+ state.set((prev) => prev + 1)
483
+ // Will broadcast: { event: 'counter', data: { count: 1 } }
484
+
485
+ const current = state.get()
486
+ // current === 1
487
+ ```
488
+
489
+ ---
490
+
491
+ ## `EvMaxConnectionsError`
492
+
493
+ Represents an error thrown when the number of active SSE connections exceeds the allowed `maxConnection` limit (default: `5000`).
494
+
495
+ ### Constructor
496
+
497
+ ```ts
498
+ new EvMaxConnectionsError(connections: number)
499
+ ```
500
+
501
+ #### Parameters:
502
+
503
+ - `connections`: `number` – The current number of active connections when the limit is exceeded.
504
+
505
+ #### Example
506
+
507
+ ```ts
508
+ const manager = new EvStreamManager({ maxConnection: 100 })
509
+ if (tooManyConnections) {
510
+ throw new EvMaxConnectionsError(100)
511
+ }
512
+ ```
513
+
514
+ ## `EvRedisAdapter`
515
+
516
+ Adapter for synchronizing `EvState` across multiple instances using Redis Pub/Sub.
517
+
518
+ ### Constructor
519
+
520
+ ```ts
521
+ new EvRedisAdapter(options?: RedisOptions | string)
522
+ ```
523
+
524
+ #### Parameters:
525
+
526
+ - `options`: `RedisOptions | string` Configuration options for the Redis client (from `ioredis`), or a Redis connection URL.
527
+
528
+ ---
529
+
530
+ ## `EvMaxListenerError`
531
+
532
+ Represents an error thrown when the number of listeners on a given channel exceeds the allowed `maxListeners` limit (default: `5000`).
533
+
534
+ ### Constructor
535
+
536
+ ```ts
537
+ new EvMaxListenerError(listeners: number, channel: string)
538
+ ```
539
+
540
+ #### Parameters:
541
+
542
+ - `listeners`: `number` – The current number of listeners on the channel.
543
+ - `channel`: `string` The name of the channel that exceeded the listener limit.
544
+
545
+ #### Example
546
+
547
+ ```ts
548
+ if (tooManyListenersOnChannel) {
549
+ throw new EvMaxListenerError(5000, 'news')
550
+ }
551
+ ```
552
+
553
+ ---
554
+
555
+ ## Type Definitions
556
+
557
+ ---
558
+
559
+ ### `EvEventsType`
560
+
561
+ ```ts
562
+ type EvEventsType = 'data' | 'error' | 'end'
563
+ ```
564
+
565
+ Represents built-in event types commonly used in Server-Sent Events.
566
+
567
+ ---
568
+
569
+ ### `EvMessage`
570
+
571
+ ```ts
572
+ interface EvMessage {
573
+ event?: string | EvEventsType
574
+ data: string | object
575
+ id?: string
576
+ }
577
+ ```
578
+
579
+ Represents a message sent to the client via SSE.
580
+
581
+ - `event` _(optional)_: Name of the event.
582
+ - `data`: The payload to send. Can be a string or an object.
583
+ - `id` _(optional)_: Event ID for reconnection tracking.
584
+
585
+ ---
586
+
587
+ ### `EvAuthenticationOptions`
588
+
589
+ ```ts
590
+ interface EvAuthenticationOptions {
591
+ method: 'query'
592
+ param: string
593
+ verify: (token: string) => Promise<EvMessage> | undefined | null | boolean
594
+ }
595
+ ```
596
+
597
+ Options for enabling query-based token authentication.
598
+
599
+ - `method`: Always `'query'`
600
+ - `param`: Name of the query parameter containing the token.
601
+ - `verify`: Async verification function. Can return:
602
+ - `true` (authenticated)
603
+ - `false` (rejected)
604
+ - `EvMessage` (custom response)
605
+ - `undefined` / `null` (unauthenticated)
606
+
607
+ ---
608
+
609
+ ### `EvOptions`
610
+
611
+ ```ts
612
+ interface EvOptions {
613
+ authentication?: EvAuthenticationOptions
614
+ heartbeat?: number
615
+ }
616
+ ```
617
+
618
+ Optional config for an individual SSE stream.
619
+
620
+ - `authentication`: Auth configuration (see `EvAuthenticationOptions`)
621
+ - `heartbeat`: Interval in milliseconds for sending heartbeat events
622
+
623
+ ---
624
+
625
+ ### `EvManagerOptions`
626
+
627
+ ```ts
628
+ interface EvManagerOptions {
629
+ id?: string
630
+ maxConnection?: number
631
+ maxListeners?: number
632
+ }
633
+ ```
634
+
635
+ Configuration for `EvStreamManager`.
636
+
637
+ - `id`: Optional prefix for client IDs
638
+ - `maxConnection`: Max allowed connections (default: `5000`)
639
+ - `maxListeners`: Max listeners per channel (default: `5000`)
640
+
641
+ ---
642
+
643
+ ### `EvStateOptions<T>`
644
+
645
+ ```ts
646
+ interface EvStateOptions<T> {
647
+ initialValue: T
648
+ channel: string
649
+ manager: EvStreamManager
650
+ key?: string
651
+ }
652
+ ```
653
+
654
+ Options for initializing a reactive state with `EvState`.
655
+
656
+ - `initialValue`: Initial state value
657
+ - `channel`: Channel name for broadcasting
658
+ - `manager`: Instance of `EvStreamManager`
659
+ - `key` _(optional)_: Key for wrapping state in the broadcast (default: `'value'`)
660
+ - `adapter` _(optional)_: Instance of `EvStateAdapter` (e.g., `EvRedisAdapter`) for distributed synchronization.
661
+
662
+ ---
663
+
664
+ ### `EvOnClose`
665
+
666
+ ```ts
667
+ type EvOnClose = (channels: string[]) => Promise<void>
668
+ ```
669
+
670
+ Callback triggered when a client connection is closed. Receives a list of channels the client was subscribed to.
671
+
672
+ ---
673
+
674
+ ## `EvStateManager<S>`
675
+
676
+ Manages a collection of shared reactive states and synchronizes their **creation and removal** across multiple instances using Pub/Sub.
677
+
678
+ ### Constructor
679
+
680
+ ```ts
681
+ new EvStateManager<S>({
682
+ manager,
683
+ adapter?,
684
+ pubsub?
685
+ })
686
+ ```
687
+
688
+ #### Parameters
689
+
690
+ - `manager`: `EvStreamManager`
691
+ Stream manager used by all states.
692
+
693
+ - `adapter` _(optional)_: `EvRedisAdapter`
694
+ Adapter used by `EvState` for distributed state updates.
695
+
696
+ - `pubsub` _(optional)_: `EvRedisPubSub`
697
+ Pub/Sub instance used to synchronize state lifecycle (`create` / `remove`).
698
+
699
+ ---
700
+
701
+ ### Methods
702
+
703
+ #### `createState<K extends keyof S>(key: K, initialValue: S[K]): EvState<S[K]>`
704
+
705
+ Creates a new state or returns an existing one.
706
+
707
+ - Creates the state locally
708
+ - Broadcasts creation to other instances (if Pub/Sub is enabled)
709
+
710
+ ---
711
+
712
+ #### `getState<K extends keyof S>(key: K): EvState<S[K]> | undefined`
713
+
714
+ Returns an existing state if it exists.
715
+
716
+ ---
717
+
718
+ #### `hasState<K extends keyof S>(key: K): boolean`
719
+
720
+ Checks whether a state exists.
721
+
722
+ ---
723
+
724
+ #### `removeState<K extends keyof S>(key: K): void`
725
+
726
+ Removes a state locally and broadcasts the removal to other instances.
727
+
728
+ ---
729
+
730
+ ### Example
731
+
732
+ ```ts
733
+ const state = stateManager.createState('user-count', 0)
734
+
735
+ state.set((v) => v + 1)
736
+
737
+ stateManager.removeState('user-count')
738
+ ```
739
+
740
+ ---
741
+
742
+ ## `EvRedisPubSub`
743
+
744
+ Lightweight Redis-based Pub/Sub utility used to synchronize events between server instances.
745
+
746
+ ### Constructor
747
+
748
+ ```ts
749
+ new EvRedisPubSub({
750
+ subject,
751
+ options,
752
+ onMessage?
753
+ })
754
+ ```
755
+
756
+ #### Parameters
757
+
758
+ - `subject`: `string`
759
+ Redis channel name used for Pub/Sub.
760
+
761
+ - `options`: `RedisOptions`
762
+ Redis connection options (`ioredis`).
763
+
764
+ - `onMessage` _(optional)_: `(message: any) => void`
765
+ Callback invoked when a message is received.
766
+
767
+ ---
768
+
769
+ ### Methods
770
+
771
+ #### `send(message: any): Promise<void>`
772
+
773
+ Publishes a message to the configured Redis channel.
774
+
775
+ - Automatically filters out self-published messages.
776
+
777
+ ---
778
+
779
+ #### `onMessage(callback: (message: any) => void): void`
780
+
781
+ Registers or replaces the message handler.
782
+
783
+ ---
784
+
785
+ #### `close(): Promise<void>`
786
+
787
+ Closes Redis publisher and subscriber connections.
788
+
789
+ ### Example
790
+
791
+ ```ts
792
+ const pubsub = new EvRedisPubSub({
793
+ subject: 'ev:states',
794
+ options: { host: 'localhost', port: 6379 },
795
+ })
796
+
797
+ pubsub.onMessage((msg) => {
798
+ console.log('received:', msg)
799
+ })
800
+
801
+ await pubsub.send({ type: 'create', channel: 'user-count' })
802
+ ```
803
+
804
+ ---
805
+
806
+ ## 🤝 Contribution
807
+
808
+ Contributions are welcome! Whether it's a bug fix, feature request, or improvement to documentation, your help is appreciated.
809
+
810
+ ### How to Contribute:
811
+
812
+ 1. **Fork** the repository.
813
+ 2. **Create a branch** for your feature or fix:
814
+
815
+ ```bash
816
+ git checkout -b feature/your-feature-name
817
+ ```
818
+
819
+ 3. **Commit your changes** with a clear message.
820
+ 4. **Push to your fork**:
821
+
822
+ ```bash
823
+ git push origin feature/your-feature-name
824
+ ```
825
+
826
+ 5. **Open a Pull Request** and describe your changes.
827
+
828
+ ### Guidelines:
829
+
830
+ - Keep your code clean and consistent with the project's existing style.
831
+ - Include relevant tests and documentation updates.
832
+ - Make sure the project builds and passes all existing checks.
833
+
834
+ ---
835
+
836
+ ## 📄 License
837
+
838
+ This project is licensed under the **MIT License**.
839
+
840
+ You are free to use, modify, distribute, and sublicense this software for both personal and commercial use — provided that the original license and copyright notice are included in all copies.
841
+
842
+ > See the [LICENSE](./LICENSE) file for full details.
843
+
844
+ ---