evstream 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/.github/workflows/publish-npm.yml +31 -0
- package/LICENSE +7 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/manager.d.ts +33 -0
- package/dist/manager.js +122 -0
- package/dist/message.d.ts +8 -0
- package/dist/message.js +15 -0
- package/dist/state.d.ts +19 -0
- package/dist/state.js +52 -0
- package/dist/stream.d.ts +31 -0
- package/dist/stream.js +101 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +26 -0
- package/package.json +47 -0
- package/readme.md +625 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish NPM Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout Code
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: '20'
|
|
20
|
+
registry-url: "https://registry.npmjs/org/"
|
|
21
|
+
|
|
22
|
+
- name: Install Dependencies
|
|
23
|
+
run: npm install
|
|
24
|
+
|
|
25
|
+
- name: Build Package
|
|
26
|
+
run: npm run build
|
|
27
|
+
|
|
28
|
+
- name: Publish to NPM
|
|
29
|
+
env:
|
|
30
|
+
NODE_AUTH_TOKEN: $${{ secrets.NPM_TOKEN }}
|
|
31
|
+
run: npm publish
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 kisshan13
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EvMaxConnectionsError` represents a error which occurs when `maxConnection` reached. Default `maxConnection` is 5000.
|
|
3
|
+
*
|
|
4
|
+
* To change connection limit you can set `maxConnection` while initializing `new EvStreamManager();`
|
|
5
|
+
*/
|
|
6
|
+
export declare class EvMaxConnectionsError extends Error {
|
|
7
|
+
constructor(connections: number);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `EvMaxListenerError` represents a error which occurs when `maxListeners` reached. Default `maxListeners` is 5000.
|
|
11
|
+
*
|
|
12
|
+
* To change listeners limit you can set `maxListeners` while initializing `new EvStreamManager();`
|
|
13
|
+
*/
|
|
14
|
+
export declare class EvMaxListenerError extends Error {
|
|
15
|
+
constructor(listeners: number, channel: string);
|
|
16
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EvMaxConnectionsError` represents a error which occurs when `maxConnection` reached. Default `maxConnection` is 5000.
|
|
3
|
+
*
|
|
4
|
+
* To change connection limit you can set `maxConnection` while initializing `new EvStreamManager();`
|
|
5
|
+
*/
|
|
6
|
+
export class EvMaxConnectionsError extends Error {
|
|
7
|
+
constructor(connections) {
|
|
8
|
+
super();
|
|
9
|
+
this.message = `Max number of connected client reached. Total Connection : ${connections}`;
|
|
10
|
+
this.name = `EvMaxConnectionsError`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `EvMaxListenerError` represents a error which occurs when `maxListeners` reached. Default `maxListeners` is 5000.
|
|
15
|
+
*
|
|
16
|
+
* To change listeners limit you can set `maxListeners` while initializing `new EvStreamManager();`
|
|
17
|
+
*/
|
|
18
|
+
export class EvMaxListenerError extends Error {
|
|
19
|
+
constructor(listeners, channel) {
|
|
20
|
+
super();
|
|
21
|
+
this.message = `Max number of listeners for the channle ${channel} reached (Listener: ${listeners}).`;
|
|
22
|
+
this.name = `EvMaxListenerError`;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Evstream } from './stream.js';
|
|
2
|
+
import { EvStreamManager } from './manager.js';
|
|
3
|
+
import { EvState } from './state.js';
|
|
4
|
+
import { EvMaxListenerError, EvMaxConnectionsError } from './errors.js';
|
|
5
|
+
import { EvOptions, EvAuthenticationOptions, EvEventsType, EvManagerOptions, EvMessage, EvStateOptions } from './types.js';
|
|
6
|
+
export { EvMaxConnectionsError, EvMaxListenerError, Evstream, EvStreamManager, EvState, EvOptions, EvAuthenticationOptions, EvEventsType, EvManagerOptions, EvMessage, EvStateOptions, };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Evstream } from './stream.js';
|
|
2
|
+
import { EvStreamManager } from './manager.js';
|
|
3
|
+
import { EvState } from './state.js';
|
|
4
|
+
import { EvMaxListenerError, EvMaxConnectionsError } from './errors.js';
|
|
5
|
+
export { EvMaxConnectionsError, EvMaxListenerError, Evstream, EvStreamManager, EvState, };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import type { EvManagerOptions, EvMessage, EvOnClose, EvOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `EvStreamManager` manages multiple SSE connections.
|
|
5
|
+
* Handles client creation, broadcasting messages, and channel-based listeners.
|
|
6
|
+
*
|
|
7
|
+
* Example :
|
|
8
|
+
*
|
|
9
|
+
* ```javascript
|
|
10
|
+
* const evManager = new EvStreamManager();
|
|
11
|
+
*
|
|
12
|
+
* const stream = evManager.createStream(req, res);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
export declare class EvStreamManager {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(opts?: EvManagerOptions);
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new SSE stream, tracks it, and returns control methods.
|
|
21
|
+
* Enforces max connection limit.
|
|
22
|
+
*/
|
|
23
|
+
createStream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions): {
|
|
24
|
+
authenticate: any;
|
|
25
|
+
message: any;
|
|
26
|
+
close: (onClose?: EvOnClose) => void;
|
|
27
|
+
listen: (name: string) => void;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Sends a message to all clients listening to a specific channel.
|
|
31
|
+
*/
|
|
32
|
+
send(name: string, msg: EvMessage): void;
|
|
33
|
+
}
|
package/dist/manager.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _EvStreamManager_instances, _EvStreamManager_clients, _EvStreamManager_listeners, _EvStreamManager_count, _EvStreamManager_maxConnections, _EvStreamManager_maxListeners, _EvStreamManager_id, _EvStreamManager_listen, _EvStreamManager_unlisten;
|
|
13
|
+
import { Evstream } from './stream.js';
|
|
14
|
+
import { uid } from './utils.js';
|
|
15
|
+
import { EvMaxConnectionsError, EvMaxListenerError } from './errors.js';
|
|
16
|
+
/**
|
|
17
|
+
* `EvStreamManager` manages multiple SSE connections.
|
|
18
|
+
* Handles client creation, broadcasting messages, and channel-based listeners.
|
|
19
|
+
*
|
|
20
|
+
* Example :
|
|
21
|
+
*
|
|
22
|
+
* ```javascript
|
|
23
|
+
* const evManager = new EvStreamManager();
|
|
24
|
+
*
|
|
25
|
+
* const stream = evManager.createStream(req, res);
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
*/
|
|
29
|
+
export class EvStreamManager {
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
_EvStreamManager_instances.add(this);
|
|
32
|
+
_EvStreamManager_clients.set(this, void 0);
|
|
33
|
+
_EvStreamManager_listeners.set(this, void 0);
|
|
34
|
+
_EvStreamManager_count.set(this, void 0);
|
|
35
|
+
_EvStreamManager_maxConnections.set(this, void 0);
|
|
36
|
+
_EvStreamManager_maxListeners.set(this, void 0);
|
|
37
|
+
_EvStreamManager_id.set(this, void 0);
|
|
38
|
+
__classPrivateFieldSet(this, _EvStreamManager_clients, new Map(), "f");
|
|
39
|
+
__classPrivateFieldSet(this, _EvStreamManager_listeners, new Map(), "f");
|
|
40
|
+
__classPrivateFieldSet(this, _EvStreamManager_count, 0, "f");
|
|
41
|
+
__classPrivateFieldSet(this, _EvStreamManager_maxConnections, (opts === null || opts === void 0 ? void 0 : opts.maxConnection) || 5000, "f");
|
|
42
|
+
__classPrivateFieldSet(this, _EvStreamManager_maxListeners, (opts === null || opts === void 0 ? void 0 : opts.maxListeners) || 5000, "f");
|
|
43
|
+
__classPrivateFieldSet(this, _EvStreamManager_id, opts === null || opts === void 0 ? void 0 : opts.id, "f");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new SSE stream, tracks it, and returns control methods.
|
|
47
|
+
* Enforces max connection limit.
|
|
48
|
+
*/
|
|
49
|
+
createStream(req, res, opts) {
|
|
50
|
+
if (__classPrivateFieldGet(this, _EvStreamManager_count, "f") >= __classPrivateFieldGet(this, _EvStreamManager_maxConnections, "f")) {
|
|
51
|
+
throw new EvMaxConnectionsError(__classPrivateFieldGet(this, _EvStreamManager_maxConnections, "f"));
|
|
52
|
+
}
|
|
53
|
+
const client = new Evstream(req, res, opts);
|
|
54
|
+
const id = uid({ counter: __classPrivateFieldGet(this, _EvStreamManager_count, "f"), prefix: __classPrivateFieldGet(this, _EvStreamManager_id, "f") });
|
|
55
|
+
const channel = [];
|
|
56
|
+
let isClosed = false;
|
|
57
|
+
__classPrivateFieldSet(this, _EvStreamManager_count, __classPrivateFieldGet(this, _EvStreamManager_count, "f") + 1, "f");
|
|
58
|
+
__classPrivateFieldGet(this, _EvStreamManager_clients, "f").set(id, client);
|
|
59
|
+
const close = (onClose) => {
|
|
60
|
+
if (isClosed)
|
|
61
|
+
return;
|
|
62
|
+
if (typeof onClose === 'function') {
|
|
63
|
+
onClose(channel);
|
|
64
|
+
}
|
|
65
|
+
isClosed = true;
|
|
66
|
+
client.close();
|
|
67
|
+
__classPrivateFieldSet(this, _EvStreamManager_count, __classPrivateFieldGet(this, _EvStreamManager_count, "f") - 1, "f");
|
|
68
|
+
channel.forEach(chan => __classPrivateFieldGet(this, _EvStreamManager_instances, "m", _EvStreamManager_unlisten).call(this, chan, id));
|
|
69
|
+
__classPrivateFieldGet(this, _EvStreamManager_clients, "f").delete(id);
|
|
70
|
+
res.end();
|
|
71
|
+
};
|
|
72
|
+
res.on('close', () => {
|
|
73
|
+
if (!isClosed) {
|
|
74
|
+
close();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
authenticate: client.authenticate.bind(client),
|
|
79
|
+
message: client.message.bind(client),
|
|
80
|
+
close: close,
|
|
81
|
+
listen: (name) => {
|
|
82
|
+
if (isClosed)
|
|
83
|
+
return;
|
|
84
|
+
channel.push(name);
|
|
85
|
+
__classPrivateFieldGet(this, _EvStreamManager_instances, "m", _EvStreamManager_listen).call(this, name, id);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Sends a message to all clients listening to a specific channel.
|
|
91
|
+
*/
|
|
92
|
+
send(name, msg) {
|
|
93
|
+
const listeners = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name);
|
|
94
|
+
for (const [_, id] of listeners.entries()) {
|
|
95
|
+
const client = __classPrivateFieldGet(this, _EvStreamManager_clients, "f").get(id);
|
|
96
|
+
if (client) {
|
|
97
|
+
client.message(Object.assign(Object.assign({}, msg), { data: typeof msg.data === 'string'
|
|
98
|
+
? { ch: name, data: msg }
|
|
99
|
+
: Object.assign({ ch: name }, msg.data) }));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
_EvStreamManager_clients = new WeakMap(), _EvStreamManager_listeners = new WeakMap(), _EvStreamManager_count = new WeakMap(), _EvStreamManager_maxConnections = new WeakMap(), _EvStreamManager_maxListeners = new WeakMap(), _EvStreamManager_id = new WeakMap(), _EvStreamManager_instances = new WeakSet(), _EvStreamManager_listen = function _EvStreamManager_listen(name, id) {
|
|
105
|
+
var _a;
|
|
106
|
+
if (!__classPrivateFieldGet(this, _EvStreamManager_listeners, "f").has(name)) {
|
|
107
|
+
const size = (_a = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name)) === null || _a === void 0 ? void 0 : _a.size;
|
|
108
|
+
if (size >= __classPrivateFieldGet(this, _EvStreamManager_maxListeners, "f")) {
|
|
109
|
+
throw new EvMaxListenerError(size, name);
|
|
110
|
+
}
|
|
111
|
+
__classPrivateFieldGet(this, _EvStreamManager_listeners, "f").set(name, new Set());
|
|
112
|
+
}
|
|
113
|
+
__classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name).add(id);
|
|
114
|
+
}, _EvStreamManager_unlisten = function _EvStreamManager_unlisten(name, id) {
|
|
115
|
+
const isListenerExists = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name);
|
|
116
|
+
if (isListenerExists) {
|
|
117
|
+
isListenerExists.delete(id);
|
|
118
|
+
if (isListenerExists.size === 0) {
|
|
119
|
+
__classPrivateFieldGet(this, _EvStreamManager_listeners, "f").delete(name);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
package/dist/message.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { safeJsonParse } from './utils.js';
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* This function convert the data to event stream compatible format.
|
|
5
|
+
*
|
|
6
|
+
* @param msg Message which you want to send to the client.
|
|
7
|
+
*/
|
|
8
|
+
export function message(msg) {
|
|
9
|
+
const event = `event:${msg.event || 'message'}\n`;
|
|
10
|
+
const data = `data:${safeJsonParse(msg.data)}\n`;
|
|
11
|
+
if (data === '') {
|
|
12
|
+
return `${msg.id ? `id:${msg.id}\n` : ''}${event}\n`;
|
|
13
|
+
}
|
|
14
|
+
return `${msg.id ? `id:${msg.id}\n` : ''}${event}${data}\n`;
|
|
15
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EvStateOptions } from './types.js';
|
|
2
|
+
type EvSetState<T> = (val: T) => T;
|
|
3
|
+
/**
|
|
4
|
+
* EvState holds a reactive state and broadcasts updates to a channel using EvStreamManager.
|
|
5
|
+
*/
|
|
6
|
+
export declare class EvState<T> {
|
|
7
|
+
#private;
|
|
8
|
+
constructor({ channel, initialValue, manager, key }: EvStateOptions<T>);
|
|
9
|
+
/**
|
|
10
|
+
* Returns the current state value.
|
|
11
|
+
*/
|
|
12
|
+
get(): T;
|
|
13
|
+
/**
|
|
14
|
+
* Updates the state using a callback.
|
|
15
|
+
* Broadcasts the new value if it has changed.
|
|
16
|
+
*/
|
|
17
|
+
set(callback: EvSetState<T>): void;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _EvState_value, _EvState_channel, _EvState_manager, _EvState_key;
|
|
13
|
+
import loadash from 'lodash';
|
|
14
|
+
const { isEqual } = loadash;
|
|
15
|
+
/**
|
|
16
|
+
* EvState holds a reactive state and broadcasts updates to a channel using EvStreamManager.
|
|
17
|
+
*/
|
|
18
|
+
export class EvState {
|
|
19
|
+
constructor({ channel, initialValue, manager, key }) {
|
|
20
|
+
_EvState_value.set(this, void 0);
|
|
21
|
+
_EvState_channel.set(this, void 0);
|
|
22
|
+
_EvState_manager.set(this, void 0);
|
|
23
|
+
_EvState_key.set(this, void 0);
|
|
24
|
+
__classPrivateFieldSet(this, _EvState_value, initialValue, "f");
|
|
25
|
+
__classPrivateFieldSet(this, _EvState_channel, channel, "f");
|
|
26
|
+
__classPrivateFieldSet(this, _EvState_manager, manager, "f");
|
|
27
|
+
__classPrivateFieldSet(this, _EvState_key, key || 'value', "f");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the current state value.
|
|
31
|
+
*/
|
|
32
|
+
get() {
|
|
33
|
+
return __classPrivateFieldGet(this, _EvState_value, "f");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Updates the state using a callback.
|
|
37
|
+
* Broadcasts the new value if it has changed.
|
|
38
|
+
*/
|
|
39
|
+
set(callback) {
|
|
40
|
+
const newValue = callback(__classPrivateFieldGet(this, _EvState_value, "f"));
|
|
41
|
+
if (!isEqual(newValue, __classPrivateFieldGet(this, _EvState_value, "f"))) {
|
|
42
|
+
__classPrivateFieldSet(this, _EvState_value, newValue, "f");
|
|
43
|
+
__classPrivateFieldGet(this, _EvState_manager, "f").send(__classPrivateFieldGet(this, _EvState_channel, "f"), {
|
|
44
|
+
event: __classPrivateFieldGet(this, _EvState_channel, "f"),
|
|
45
|
+
data: {
|
|
46
|
+
[__classPrivateFieldGet(this, _EvState_key, "f")]: newValue,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
_EvState_value = new WeakMap(), _EvState_channel = new WeakMap(), _EvState_manager = new WeakMap(), _EvState_key = new WeakMap();
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import { EvMessage, EvOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Evstream manages a Server-Sent Events (SSE) connection.
|
|
5
|
+
* Sets necessary headers, handles heartbeat, authentication, sending messages, and closing the stream.
|
|
6
|
+
* Example :
|
|
7
|
+
*
|
|
8
|
+
* ```javascript
|
|
9
|
+
* const ev = new Evstream(req, res);
|
|
10
|
+
*
|
|
11
|
+
* ev.message({event: "message", data: {message: "a message"}, id: "event_id_1"})
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare class Evstream {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(req: IncomingMessage, res: ServerResponse, opts?: EvOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Handles optional authentication using provided token verification.
|
|
19
|
+
* Sends error message and closes connection if authentication fails.
|
|
20
|
+
*/
|
|
21
|
+
authenticate(): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Sends an SSE message to the client.
|
|
24
|
+
* Accepts an `EvMessage` object.
|
|
25
|
+
*/
|
|
26
|
+
message(msg: EvMessage): void;
|
|
27
|
+
/**
|
|
28
|
+
* Sends an "end" event and closes the SSE connection.
|
|
29
|
+
*/
|
|
30
|
+
close(): void;
|
|
31
|
+
}
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
11
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
12
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
13
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
14
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
15
|
+
};
|
|
16
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
17
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
18
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
19
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
20
|
+
};
|
|
21
|
+
var _Evstream_res, _Evstream_opts, _Evstream_url;
|
|
22
|
+
import { message } from './message.js';
|
|
23
|
+
/**
|
|
24
|
+
* Evstream manages a Server-Sent Events (SSE) connection.
|
|
25
|
+
* Sets necessary headers, handles heartbeat, authentication, sending messages, and closing the stream.
|
|
26
|
+
* Example :
|
|
27
|
+
*
|
|
28
|
+
* ```javascript
|
|
29
|
+
* const ev = new Evstream(req, res);
|
|
30
|
+
*
|
|
31
|
+
* ev.message({event: "message", data: {message: "a message"}, id: "event_id_1"})
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class Evstream {
|
|
35
|
+
constructor(req, res, opts) {
|
|
36
|
+
_Evstream_res.set(this, void 0);
|
|
37
|
+
_Evstream_opts.set(this, void 0);
|
|
38
|
+
_Evstream_url.set(this, void 0);
|
|
39
|
+
__classPrivateFieldSet(this, _Evstream_res, res, "f");
|
|
40
|
+
__classPrivateFieldSet(this, _Evstream_opts, opts, "f");
|
|
41
|
+
__classPrivateFieldSet(this, _Evstream_url, new URL(req.url, `http://${req.headers.host}`), "f");
|
|
42
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").setHeader('Content-Type', 'text/event-stream');
|
|
43
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").setHeader('Cache-Control', 'no-cache');
|
|
44
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").setHeader('Connection', 'keep-alive');
|
|
45
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").flushHeaders();
|
|
46
|
+
if (opts === null || opts === void 0 ? void 0 : opts.heartbeat) {
|
|
47
|
+
const timeout = setInterval(() => {
|
|
48
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").write(message({ event: 'heartbeat', data: '' }));
|
|
49
|
+
}, __classPrivateFieldGet(this, _Evstream_opts, "f").heartbeat);
|
|
50
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").on('close', () => {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Handles optional authentication using provided token verification.
|
|
57
|
+
* Sends error message and closes connection if authentication fails.
|
|
58
|
+
*/
|
|
59
|
+
authenticate() {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
if (__classPrivateFieldGet(this, _Evstream_opts, "f").authentication) {
|
|
62
|
+
const token = __classPrivateFieldGet(this, _Evstream_url, "f").searchParams.get(__classPrivateFieldGet(this, _Evstream_opts, "f").authentication.param);
|
|
63
|
+
const isAuthenticated = yield __classPrivateFieldGet(this, _Evstream_opts, "f").authentication.verify(token);
|
|
64
|
+
if (typeof isAuthenticated === 'boolean') {
|
|
65
|
+
if (!isAuthenticated) {
|
|
66
|
+
this.message({
|
|
67
|
+
data: { message: 'authentication failed' },
|
|
68
|
+
event: 'error',
|
|
69
|
+
});
|
|
70
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").end();
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (typeof isAuthenticated === 'object') {
|
|
76
|
+
this.message(isAuthenticated);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sends an SSE message to the client.
|
|
85
|
+
* Accepts an `EvMessage` object.
|
|
86
|
+
*/
|
|
87
|
+
message(msg) {
|
|
88
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").write(message(msg));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sends an "end" event and closes the SSE connection.
|
|
92
|
+
*/
|
|
93
|
+
close() {
|
|
94
|
+
this.message({
|
|
95
|
+
event: 'end',
|
|
96
|
+
data: '',
|
|
97
|
+
});
|
|
98
|
+
__classPrivateFieldGet(this, _Evstream_res, "f").end();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
_Evstream_res = new WeakMap(), _Evstream_opts = new WeakMap(), _Evstream_url = new WeakMap();
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { EvStreamManager } from './manager.js';
|
|
2
|
+
export type EvEventsType = 'data' | 'error' | 'end';
|
|
3
|
+
export interface EvMessage {
|
|
4
|
+
event?: string | EvEventsType;
|
|
5
|
+
data: string | object;
|
|
6
|
+
id?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EvAuthenticationOptions {
|
|
9
|
+
method: 'query';
|
|
10
|
+
param: string;
|
|
11
|
+
verify: (token: string) => Promise<EvMessage> | undefined | null | boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface EvOptions {
|
|
14
|
+
authentication?: EvAuthenticationOptions;
|
|
15
|
+
heartbeat?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface EvManagerOptions {
|
|
18
|
+
id?: string;
|
|
19
|
+
maxConnection?: number;
|
|
20
|
+
maxListeners?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface EvStateOptions<T> {
|
|
23
|
+
initialValue: T;
|
|
24
|
+
channel: string;
|
|
25
|
+
manager: EvStreamManager;
|
|
26
|
+
key?: string;
|
|
27
|
+
}
|
|
28
|
+
export type EvOnClose = (channels: string[]) => Promise<void>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* This function takes a value which can be string or an object and returns a string for that value. If value cannot be convertable to string then it will return a empty string.
|
|
4
|
+
*
|
|
5
|
+
* @param val Data which needs to be serialize to JSON string format
|
|
6
|
+
* @returns
|
|
7
|
+
*/
|
|
8
|
+
export declare function safeJsonParse(val: any): string;
|
|
9
|
+
export declare function uid(opts?: {
|
|
10
|
+
prefix?: string;
|
|
11
|
+
counter?: number;
|
|
12
|
+
}): string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* This function takes a value which can be string or an object and returns a string for that value. If value cannot be convertable to string then it will return a empty string.
|
|
4
|
+
*
|
|
5
|
+
* @param val Data which needs to be serialize to JSON string format
|
|
6
|
+
* @returns
|
|
7
|
+
*/
|
|
8
|
+
export function safeJsonParse(val) {
|
|
9
|
+
if (typeof val === "string") {
|
|
10
|
+
return val;
|
|
11
|
+
}
|
|
12
|
+
if (typeof val === "object") {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.stringify(val);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
export function uid(opts) {
|
|
23
|
+
const now = Date.now().toString(36);
|
|
24
|
+
const rand = Math.random().toString(26).substring(2, 10);
|
|
25
|
+
return `${(opts === null || opts === void 0 ? void 0 : opts.prefix) ? `${opts === null || opts === void 0 ? void 0 : opts.prefix}-` : ""}${now}-${rand}-${opts === null || opts === void 0 ? void 0 : opts.counter}`;
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "evstream",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A simple and easy to implement server sent event library for express.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sse",
|
|
7
|
+
"server-sent-events",
|
|
8
|
+
"event-source"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/kisshan13/evstream#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/kisshan13/evstream/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/kisshan13/evstream.git"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Kishan Sharma",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsc --watch",
|
|
24
|
+
"clean": "rimraf ./dist",
|
|
25
|
+
"build": "rimraf ./dist && tsc --incremental false"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=17.8.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/lodash": "^4.17.19",
|
|
32
|
+
"@types/node": "^24.0.7",
|
|
33
|
+
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
|
34
|
+
"@typescript-eslint/parser": "^8.35.0",
|
|
35
|
+
"eslint": "^9.30.0",
|
|
36
|
+
"eslint-config-prettier": "^10.1.5",
|
|
37
|
+
"eslint-plugin-import": "^2.32.0",
|
|
38
|
+
"eslint-plugin-prettier": "^5.5.1",
|
|
39
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
40
|
+
"prettier": "^3.6.2",
|
|
41
|
+
"rimraf": "^6.0.1",
|
|
42
|
+
"typescript": "^5.8.3"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"lodash": "^4.17.21"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
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. Sending data to a channel
|
|
192
|
+
|
|
193
|
+
To send data to a channel you can use `send()` method from `EvStreamManager` class.
|
|
194
|
+
|
|
195
|
+
Example :
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
import { EvStreamManager } from "evstream"
|
|
199
|
+
|
|
200
|
+
const manager = new EvStreamManager();
|
|
201
|
+
|
|
202
|
+
manager.send("<channel-name>", {event: "custom-event", data: {"foo": "bar"}})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 6. Listening for channels
|
|
206
|
+
|
|
207
|
+
To listen for data from any channel you can use `listen()` function from `Evstream` class.
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
client.listen("<channel-name>")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## API Reference
|
|
214
|
+
|
|
215
|
+
## `Evstream`
|
|
216
|
+
|
|
217
|
+
Manages a Server-Sent Events (SSE) connection. Handles headers, heartbeat intervals, authentication, sending messages, and closing the stream.
|
|
218
|
+
|
|
219
|
+
### Constructor
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
new Evstream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### Parameters:
|
|
226
|
+
|
|
227
|
+
* `req`: `IncomingMessage` – The incoming HTTP request.
|
|
228
|
+
* `res`: `ServerResponse` – The HTTP response to write SSE messages to.
|
|
229
|
+
* `opts` *(optional)*: `EvOptions` – Optional configuration including heartbeat interval and authentication.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Methods
|
|
234
|
+
|
|
235
|
+
#### `authenticate(): Promise<boolean | undefined>`
|
|
236
|
+
|
|
237
|
+
Performs optional token-based authentication if `opts.authentication` is provided.
|
|
238
|
+
|
|
239
|
+
* If authentication fails, sends an error message and closes the connection.
|
|
240
|
+
* Returns `true` if authenticated, `false` if rejected, or `undefined` if no authentication is configured.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
#### `message(msg: EvMessage): void`
|
|
245
|
+
|
|
246
|
+
Sends an SSE message to the connected client.
|
|
247
|
+
|
|
248
|
+
##### Parameters:
|
|
249
|
+
|
|
250
|
+
* `msg`: `EvMessage` – Object containing `event`, `data`, and optionally `id`.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
#### `close(): void`
|
|
255
|
+
|
|
256
|
+
Sends a final `end` event and closes the SSE connection.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
### Example
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
const ev = new Evstream(req, res, {
|
|
264
|
+
heartbeat: 30000,
|
|
265
|
+
authentication: {
|
|
266
|
+
param: 'token',
|
|
267
|
+
verify: async (token) => token === 'valid_token'
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
await ev.authenticate()
|
|
272
|
+
ev.message({ event: 'message', data: { text: 'Hello world' }, id: '1' })
|
|
273
|
+
ev.close()
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## `EvStreamManager`
|
|
279
|
+
|
|
280
|
+
Manages multiple Server-Sent Events (SSE) client streams. Supports connection tracking, message broadcasting, and channel-based listeners.
|
|
281
|
+
|
|
282
|
+
### Constructor
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
new EvStreamManager(opts?: EvManagerOptions)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
#### Parameters:
|
|
289
|
+
|
|
290
|
+
* `opts` *(optional)*: `EvManagerOptions`
|
|
291
|
+
|
|
292
|
+
* `maxConnection`: Maximum allowed active connections (default: `5000`)
|
|
293
|
+
* `maxListeners`: Maximum listeners per channel (default: `5000`)
|
|
294
|
+
* `id`: Optional prefix for client IDs
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### Methods
|
|
299
|
+
|
|
300
|
+
#### `createStream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions): { authenticate, message, close, listen }`
|
|
301
|
+
|
|
302
|
+
Creates and tracks a new SSE stream.
|
|
303
|
+
|
|
304
|
+
#### Parameters:
|
|
305
|
+
|
|
306
|
+
* `req`: `IncomingMessage` – Incoming HTTP request
|
|
307
|
+
* `res`: `ServerResponse` – HTTP response for the SSE connection
|
|
308
|
+
* `opts` *(optional)*: `EvOptions` – Optional stream config (heartbeat, authentication, etc.)
|
|
309
|
+
|
|
310
|
+
#### Returns:
|
|
311
|
+
|
|
312
|
+
An object with methods:
|
|
313
|
+
|
|
314
|
+
* `authenticate(): Promise<boolean | undefined>` – Authenticates the stream (delegates to `Evstream`)
|
|
315
|
+
* `message(msg: EvMessage): void` – Sends a message to the stream
|
|
316
|
+
* `close(onClose?: EvOnClose): void` – Closes the stream and cleans up listeners
|
|
317
|
+
* `listen(name: string): void` – Subscribes the stream to a named channel
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
#### `send(name: string, msg: EvMessage): void`
|
|
322
|
+
|
|
323
|
+
Broadcasts a message to all clients listening on the specified `name` (channel).
|
|
324
|
+
|
|
325
|
+
##### Parameters:
|
|
326
|
+
|
|
327
|
+
* `name`: `string` – Channel name
|
|
328
|
+
* `msg`: `EvMessage` – The message to broadcast
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### Private Methods
|
|
333
|
+
|
|
334
|
+
#### `#listen(name: string, id: string): void`
|
|
335
|
+
|
|
336
|
+
Adds a client (by ID) to a named channel. Throws `EvMaxListenerError` if channel exceeds max listeners.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
#### `#unlisten(name: string, id: string): void`
|
|
341
|
+
|
|
342
|
+
Removes a client from a channel. Deletes the channel if no listeners remain.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
### Example
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
const manager = new EvStreamManager()
|
|
350
|
+
|
|
351
|
+
const stream = manager.createStream(req, res)
|
|
352
|
+
|
|
353
|
+
await stream.authenticate()
|
|
354
|
+
stream.listen('news')
|
|
355
|
+
stream.message({ event: 'hello', data: 'welcome' })
|
|
356
|
+
|
|
357
|
+
manager.send('news', { event: 'news', data: 'breaking update' })
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## `EvState<T>`
|
|
361
|
+
|
|
362
|
+
Reactive state holder that broadcasts updates to a specified channel via an `EvStreamManager`. Designed for real-time state syncing over Server-Sent Events (SSE).
|
|
363
|
+
|
|
364
|
+
### Constructor
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
new EvState<T>({
|
|
368
|
+
channel,
|
|
369
|
+
initialValue,
|
|
370
|
+
manager,
|
|
371
|
+
key
|
|
372
|
+
}: EvStateOptions<T>)
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
#### Parameters:
|
|
376
|
+
|
|
377
|
+
* `channel`: `string` – The name of the channel to broadcast updates to.
|
|
378
|
+
* `initialValue`: `T` – The initial state value.
|
|
379
|
+
* `manager`: `EvStreamManager` – The SSE manager instance used for broadcasting.
|
|
380
|
+
* `key` *(optional)*: `string` – The key used in the broadcasted data object (default: `'value'`).
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
### Methods
|
|
385
|
+
|
|
386
|
+
#### `get(): T`
|
|
387
|
+
|
|
388
|
+
Returns the current value of the state.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
#### `set(callback: (val: T) => T): void`
|
|
393
|
+
|
|
394
|
+
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.
|
|
395
|
+
|
|
396
|
+
##### Parameters:
|
|
397
|
+
|
|
398
|
+
* `callback`: `(val: T) => T` – A function that receives the current state and returns the new state.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
### Example
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
const state = new EvState({
|
|
406
|
+
channel: 'counter',
|
|
407
|
+
initialValue: 0,
|
|
408
|
+
manager: evManager,
|
|
409
|
+
key: 'count'
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
state.set(prev => prev + 1)
|
|
413
|
+
// Will broadcast: { event: 'counter', data: { count: 1 } }
|
|
414
|
+
|
|
415
|
+
const current = state.get()
|
|
416
|
+
// current === 1
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## `EvMaxConnectionsError`
|
|
422
|
+
|
|
423
|
+
Represents an error thrown when the number of active SSE connections exceeds the allowed `maxConnection` limit (default: `5000`).
|
|
424
|
+
|
|
425
|
+
### Constructor
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
new EvMaxConnectionsError(connections: number)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
#### Parameters:
|
|
432
|
+
|
|
433
|
+
- `connections`: `number` – The current number of active connections when the limit is exceeded.
|
|
434
|
+
|
|
435
|
+
#### Example
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
const manager = new EvStreamManager({ maxConnection: 100 });
|
|
439
|
+
if (tooManyConnections) {
|
|
440
|
+
throw new EvMaxConnectionsError(100)
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## `EvMaxListenerError`
|
|
447
|
+
|
|
448
|
+
Represents an error thrown when the number of listeners on a given channel exceeds the allowed `maxListeners` limit (default: `5000`).
|
|
449
|
+
|
|
450
|
+
### Constructor
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
new EvMaxListenerError(listeners: number, channel: string)
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
#### Parameters:
|
|
457
|
+
|
|
458
|
+
- `listeners`: `number` – The current number of listeners on the channel.
|
|
459
|
+
- `channel`: `string` – The name of the channel that exceeded the listener limit.
|
|
460
|
+
|
|
461
|
+
#### Example
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
if (tooManyListenersOnChannel) {
|
|
465
|
+
throw new EvMaxListenerError(5000, 'news')
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Type Definitions
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
### `EvEventsType`
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
type EvEventsType = 'data' | 'error' | 'end'
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Represents built-in event types commonly used in Server-Sent Events.
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
### `EvMessage`
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
interface EvMessage {
|
|
489
|
+
event?: string | EvEventsType
|
|
490
|
+
data: string | object
|
|
491
|
+
id?: string
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Represents a message sent to the client via SSE.
|
|
496
|
+
|
|
497
|
+
- `event` *(optional)*: Name of the event.
|
|
498
|
+
- `data`: The payload to send. Can be a string or an object.
|
|
499
|
+
- `id` *(optional)*: Event ID for reconnection tracking.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
### `EvAuthenticationOptions`
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
interface EvAuthenticationOptions {
|
|
507
|
+
method: 'query'
|
|
508
|
+
param: string
|
|
509
|
+
verify: (token: string) => Promise<EvMessage> | undefined | null | boolean
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Options for enabling query-based token authentication.
|
|
514
|
+
|
|
515
|
+
- `method`: Always `'query'`
|
|
516
|
+
- `param`: Name of the query parameter containing the token.
|
|
517
|
+
- `verify`: Async verification function. Can return:
|
|
518
|
+
- `true` (authenticated)
|
|
519
|
+
- `false` (rejected)
|
|
520
|
+
- `EvMessage` (custom response)
|
|
521
|
+
- `undefined` / `null` (unauthenticated)
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
### `EvOptions`
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
interface EvOptions {
|
|
529
|
+
authentication?: EvAuthenticationOptions
|
|
530
|
+
heartbeat?: number
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Optional config for an individual SSE stream.
|
|
535
|
+
|
|
536
|
+
- `authentication`: Auth configuration (see `EvAuthenticationOptions`)
|
|
537
|
+
- `heartbeat`: Interval in milliseconds for sending heartbeat events
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
### `EvManagerOptions`
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
interface EvManagerOptions {
|
|
545
|
+
id?: string
|
|
546
|
+
maxConnection?: number
|
|
547
|
+
maxListeners?: number
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Configuration for `EvStreamManager`.
|
|
552
|
+
|
|
553
|
+
- `id`: Optional prefix for client IDs
|
|
554
|
+
- `maxConnection`: Max allowed connections (default: `5000`)
|
|
555
|
+
- `maxListeners`: Max listeners per channel (default: `5000`)
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
### `EvStateOptions<T>`
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
interface EvStateOptions<T> {
|
|
563
|
+
initialValue: T
|
|
564
|
+
channel: string
|
|
565
|
+
manager: EvStreamManager
|
|
566
|
+
key?: string
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Options for initializing a reactive state with `EvState`.
|
|
571
|
+
|
|
572
|
+
- `initialValue`: Initial state value
|
|
573
|
+
- `channel`: Channel name for broadcasting
|
|
574
|
+
- `manager`: Instance of `EvStreamManager`
|
|
575
|
+
- `key` *(optional)*: Key for wrapping state in the broadcast (default: `'value'`)
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
### `EvOnClose`
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
type EvOnClose = (channels: string[]) => Promise<void>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Callback triggered when a client connection is closed. Receives a list of channels the client was subscribed to.
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## 🤝 Contribution
|
|
590
|
+
|
|
591
|
+
Contributions are welcome! Whether it's a bug fix, feature request, or improvement to documentation, your help is appreciated.
|
|
592
|
+
|
|
593
|
+
### How to Contribute:
|
|
594
|
+
|
|
595
|
+
1. **Fork** the repository.
|
|
596
|
+
2. **Create a branch** for your feature or fix:
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
git checkout -b feature/your-feature-name
|
|
600
|
+
```
|
|
601
|
+
3. **Commit your changes** with a clear message.
|
|
602
|
+
4. **Push to your fork**:
|
|
603
|
+
|
|
604
|
+
```bash
|
|
605
|
+
git push origin feature/your-feature-name
|
|
606
|
+
```
|
|
607
|
+
5. **Open a Pull Request** and describe your changes.
|
|
608
|
+
|
|
609
|
+
### Guidelines:
|
|
610
|
+
|
|
611
|
+
* Keep your code clean and consistent with the project's existing style.
|
|
612
|
+
* Include relevant tests and documentation updates.
|
|
613
|
+
* Make sure the project builds and passes all existing checks.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## 📄 License
|
|
618
|
+
|
|
619
|
+
This project is licensed under the **MIT License**.
|
|
620
|
+
|
|
621
|
+
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.
|
|
622
|
+
|
|
623
|
+
> See the [LICENSE](./LICENSE) file for full details.
|
|
624
|
+
|
|
625
|
+
---
|