evstream 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ npm run lint-staged
@@ -0,0 +1,15 @@
1
+ # Ignore build outputs
2
+ dist/
3
+ build/
4
+ coverage/
5
+
6
+ # Ignore dependencies
7
+ node_modules/
8
+
9
+ # Ignore generated files
10
+ *.min.js
11
+ *.bundle.js
12
+
13
+ # Ignore specific files
14
+ package-lock.json
15
+ yarn.lock
package/.prettierrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5",
6
+ "jsxSingleQuote": true,
7
+ "useTabs": true
8
+ }
@@ -38,7 +38,7 @@ export class EvRedisAdapter {
38
38
  catch (_a) {
39
39
  return;
40
40
  }
41
- handlers.forEach(handler => handler(parsed));
41
+ handlers.forEach((handler) => handler(parsed));
42
42
  }
43
43
  });
44
44
  }
package/dist/state.d.ts CHANGED
@@ -5,7 +5,7 @@ type EvSetState<T> = (val: T) => T;
5
5
  */
6
6
  export declare class EvState<T> {
7
7
  #private;
8
- constructor({ channel, initialValue, manager, key, adapter }: EvStateOptions<T>);
8
+ constructor({ channel, initialValue, manager, key, adapter, }: EvStateOptions<T>);
9
9
  /**
10
10
  * Returns the current state value.
11
11
  */
package/dist/state.js CHANGED
@@ -16,7 +16,7 @@ const { isEqual } = loadash;
16
16
  * EvState holds a reactive state and broadcasts updates to a channel using EvStreamManager.
17
17
  */
18
18
  export class EvState {
19
- constructor({ channel, initialValue, manager, key, adapter }) {
19
+ constructor({ channel, initialValue, manager, key, adapter, }) {
20
20
  _EvState_instances.add(this);
21
21
  _EvState_value.set(this, void 0);
22
22
  _EvState_channel.set(this, void 0);
package/dist/utils.js CHANGED
@@ -6,21 +6,21 @@
6
6
  * @returns
7
7
  */
8
8
  export function safeJsonParse(val) {
9
- if (typeof val === "string") {
9
+ if (typeof val === 'string') {
10
10
  return val;
11
11
  }
12
- if (typeof val === "object") {
12
+ if (typeof val === 'object') {
13
13
  try {
14
14
  return JSON.stringify(val);
15
15
  }
16
16
  catch (error) {
17
- return "";
17
+ return '';
18
18
  }
19
19
  }
20
- return "";
20
+ return '';
21
21
  }
22
22
  export function uid(opts) {
23
23
  const now = Date.now().toString(36);
24
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}`;
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
26
  }
package/package.json CHANGED
@@ -1,60 +1,73 @@
1
- {
2
- "name": "evstream",
3
- "version": "1.0.1",
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
- "types": "./dist/index.d.ts",
23
- "exports": {
24
- ".": {
25
- "import": "./dist/index.js",
26
- "types": "./dist/index.d.ts"
27
- },
28
- "./adapter/redis": {
29
- "import": "./dist/adapters/redis.js",
30
- "types": "./dist/adapters/redis.d.ts"
31
- }
32
- },
33
- "scripts": {
34
- "dev": "tsc --watch",
35
- "clean": "rimraf ./dist",
36
- "build": "rimraf ./dist && tsc --incremental false"
37
- },
38
- "engines": {
39
- "node": ">=17.8.0"
40
- },
41
- "devDependencies": {
42
- "@types/ioredis": "^5.0.0",
43
- "@types/lodash": "^4.17.19",
44
- "@types/node": "^24.0.7",
45
- "@typescript-eslint/eslint-plugin": "^8.35.0",
46
- "@typescript-eslint/parser": "^8.35.0",
47
- "eslint": "^9.30.0",
48
- "eslint-config-prettier": "^10.1.5",
49
- "eslint-plugin-import": "^2.32.0",
50
- "eslint-plugin-prettier": "^5.5.1",
51
- "eslint-plugin-simple-import-sort": "^12.1.1",
52
- "ioredis": "^5.5.0",
53
- "prettier": "^3.6.2",
54
- "rimraf": "^6.0.1",
55
- "typescript": "^5.8.3"
56
- },
57
- "dependencies": {
58
- "lodash": "^4.17.21"
59
- }
60
- }
1
+ {
2
+ "name": "evstream",
3
+ "version": "1.0.2",
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
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./dist/index.js",
26
+ "types": "./dist/index.d.ts"
27
+ },
28
+ "./adapter/redis": {
29
+ "import": "./dist/adapters/redis.js",
30
+ "types": "./dist/adapters/redis.d.ts"
31
+ }
32
+ },
33
+ "lint-staged": {
34
+ "*.{js,jsx,ts,tsx,cjs,mjs}": [
35
+ "prettier --write"
36
+ ],
37
+ "*.{json,md,yml,yaml}": [
38
+ "prettier --write"
39
+ ]
40
+ },
41
+ "scripts": {
42
+ "dev": "tsc --watch",
43
+ "clean": "rimraf ./dist",
44
+ "build": "rimraf ./dist && tsc --incremental false",
45
+ "prepare": "husky",
46
+ "lint-staged": "lint-staged",
47
+ "format": "prettier --write \"**/*.{js,jsx,ts,tsx}\""
48
+ },
49
+ "engines": {
50
+ "node": ">=17.8.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/ioredis": "^5.0.0",
54
+ "@types/lodash": "^4.17.19",
55
+ "@types/node": "^24.0.7",
56
+ "@typescript-eslint/eslint-plugin": "^8.35.0",
57
+ "@typescript-eslint/parser": "^8.35.0",
58
+ "eslint": "^9.30.0",
59
+ "eslint-config-prettier": "^10.1.5",
60
+ "eslint-plugin-import": "^2.32.0",
61
+ "eslint-plugin-prettier": "^5.5.1",
62
+ "eslint-plugin-simple-import-sort": "^12.1.1",
63
+ "ioredis": "^5.5.0",
64
+ "prettier": "^3.7.4",
65
+ "rimraf": "^6.0.1",
66
+ "typescript": "^5.8.3",
67
+ "husky": "^9.1.7",
68
+ "lint-staged": "^16.2.7"
69
+ },
70
+ "dependencies": {
71
+ "lodash": "^4.17.21"
72
+ }
73
+ }
@@ -0,0 +1,53 @@
1
+ import Redis, { RedisOptions } from 'ioredis'
2
+
3
+ import { EvStateAdapter } from '../types.js'
4
+
5
+ export class EvRedisAdapter implements EvStateAdapter {
6
+ #pub: Redis
7
+ #sub: Redis
8
+ #listeners: Map<string, Set<(msg: any) => void>>
9
+
10
+ constructor(options?: RedisOptions) {
11
+ this.#pub = new Redis(options)
12
+ this.#sub = new Redis(options)
13
+ this.#listeners = new Map()
14
+
15
+ this.#sub.on('message', (channel, message) => {
16
+ const handlers = this.#listeners.get(channel)
17
+ if (handlers) {
18
+ let parsed: any
19
+ try {
20
+ parsed = JSON.parse(message)
21
+ } catch {
22
+ return
23
+ }
24
+ handlers.forEach((handler) => handler(parsed))
25
+ }
26
+ })
27
+ }
28
+
29
+ async publish(channel: string, message: any): Promise<void> {
30
+ await this.#pub.publish(channel, JSON.stringify(message))
31
+ }
32
+
33
+ async subscribe(
34
+ channel: string,
35
+ onMessage: (message: any) => void
36
+ ): Promise<void> {
37
+ if (!this.#listeners.has(channel)) {
38
+ this.#listeners.set(channel, new Set())
39
+ await this.#sub.subscribe(channel)
40
+ }
41
+ this.#listeners.get(channel)!.add(onMessage)
42
+ }
43
+
44
+ async unsubscribe(channel: string): Promise<void> {
45
+ await this.#sub.unsubscribe(channel)
46
+ this.#listeners.delete(channel)
47
+ }
48
+
49
+ quit() {
50
+ this.#pub.quit()
51
+ this.#sub.quit()
52
+ }
53
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,25 @@
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: number) {
8
+ super()
9
+ this.message = `Max number of connected client reached. Total Connection : ${connections}`
10
+ this.name = `EvMaxConnectionsError`
11
+ }
12
+ }
13
+
14
+ /**
15
+ * `EvMaxListenerError` represents a error which occurs when `maxListeners` reached. Default `maxListeners` is 5000.
16
+ *
17
+ * To change listeners limit you can set `maxListeners` while initializing `new EvStreamManager();`
18
+ */
19
+ export class EvMaxListenerError extends Error {
20
+ constructor(listeners: number, channel: string) {
21
+ super()
22
+ this.message = `Max number of listeners for the channle ${channel} reached (Listener: ${listeners}).`
23
+ this.name = `EvMaxListenerError`
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { Evstream } from './stream.js'
2
+ import { EvStreamManager } from './manager.js'
3
+ import { EvState } from './state.js'
4
+
5
+ import { EvMaxListenerError, EvMaxConnectionsError } from './errors.js'
6
+
7
+ import {
8
+ EvOptions,
9
+ EvAuthenticationOptions,
10
+ EvEventsType,
11
+ EvManagerOptions,
12
+ EvMessage,
13
+ EvStateOptions,
14
+ } from './types.js'
15
+
16
+ export {
17
+ EvMaxConnectionsError,
18
+ EvMaxListenerError,
19
+ Evstream,
20
+ EvStreamManager,
21
+ EvState,
22
+ EvOptions,
23
+ EvAuthenticationOptions,
24
+ EvEventsType,
25
+ EvManagerOptions,
26
+ EvMessage,
27
+ EvStateOptions,
28
+ }
package/src/manager.ts ADDED
@@ -0,0 +1,171 @@
1
+ import { IncomingMessage, ServerResponse } from 'http'
2
+
3
+ import { Evstream } from './stream.js'
4
+ import { uid } from './utils.js'
5
+ import { EvMaxConnectionsError, EvMaxListenerError } from './errors.js'
6
+
7
+ import type {
8
+ EvManagerOptions,
9
+ EvMessage,
10
+ EvOnClose,
11
+ EvOptions,
12
+ } from './types.js'
13
+
14
+ /**
15
+ * `EvStreamManager` manages multiple SSE connections.
16
+ * Handles client creation, broadcasting messages, and channel-based listeners.
17
+ *
18
+ * Example :
19
+ *
20
+ * ```javascript
21
+ * const evManager = new EvStreamManager();
22
+ *
23
+ * const stream = evManager.createStream(req, res);
24
+ * ```
25
+ *
26
+ */
27
+ export class EvStreamManager {
28
+ #clients: Map<string, Evstream>
29
+ #listeners: Map<string, Set<string>>
30
+ #count: number
31
+ #maxConnections: number
32
+ #maxListeners: number
33
+ #id?: string
34
+ constructor(opts?: EvManagerOptions) {
35
+ this.#clients = new Map()
36
+ this.#listeners = new Map()
37
+ this.#count = 0
38
+
39
+ this.#maxConnections = opts?.maxConnection || 5000
40
+ this.#maxListeners = opts?.maxListeners || 5000
41
+ this.#id = opts?.id
42
+ }
43
+
44
+ /**
45
+ * Creates a new SSE stream, tracks it, and returns control methods.
46
+ * Enforces max connection limit.
47
+ */
48
+ createStream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions) {
49
+ if (this.#count >= this.#maxConnections) {
50
+ throw new EvMaxConnectionsError(this.#maxConnections)
51
+ }
52
+
53
+ const client = new Evstream(req, res, opts)
54
+ const id = uid({ counter: this.#count, prefix: this.#id })
55
+ const channel: string[] = []
56
+ let isClosed = false
57
+
58
+ this.#count += 1
59
+ this.#clients.set(id, client)
60
+
61
+ const close = (onClose?: EvOnClose) => {
62
+ if (isClosed) return
63
+
64
+ if (typeof onClose === 'function') {
65
+ onClose(channel)
66
+ }
67
+
68
+ isClosed = true
69
+
70
+ // Remove close event listener to prevent memory leaks
71
+ res.removeAllListeners('close')
72
+
73
+ // Clean up client
74
+ client.close()
75
+
76
+ // Decrement count
77
+ this.#count -= 1
78
+
79
+ // Remove from all channels
80
+ channel.forEach(chan => this.#unlisten(chan, id))
81
+
82
+ // Clear channel array to release references
83
+ channel.length = 0
84
+
85
+ // Remove client from map
86
+ this.#clients.delete(id)
87
+
88
+ // End response if not already ended
89
+ if (!res.writableEnded) {
90
+ res.end()
91
+ }
92
+ }
93
+
94
+ const onCloseHandler = () => {
95
+ if (!isClosed) {
96
+ close()
97
+ }
98
+ }
99
+
100
+ res.on('close', onCloseHandler)
101
+
102
+ return {
103
+ authenticate: client.authenticate.bind(client),
104
+ message: client.message.bind(client),
105
+ close: close,
106
+ listen: (name: string) => {
107
+ if (isClosed) return
108
+ channel.push(name)
109
+ this.#listen(name, id)
110
+ },
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Sends a message to all clients listening to a specific channel.
116
+ */
117
+ send(name: string, msg: EvMessage) {
118
+ const listeners = this.#listeners.get(name)
119
+
120
+ if (!listeners) return
121
+
122
+ for (const [_, id] of listeners.entries()) {
123
+ const client = this.#clients.get(id)
124
+
125
+ if (client) {
126
+ client.message({
127
+ ...msg,
128
+ data:
129
+ typeof msg.data === 'string'
130
+ ? { ch: name, data: msg }
131
+ : { ch: name, ...msg.data },
132
+ })
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Adds a client to a specific channel.
139
+ * Enforces max listeners per channel.
140
+ */
141
+ #listen(name: string, id: string) {
142
+ let listeners = this.#listeners.get(name)
143
+
144
+ if (!listeners) {
145
+ listeners = new Set<string>()
146
+ this.#listeners.set(name, listeners)
147
+ }
148
+
149
+ if (listeners.size >= this.#maxListeners) {
150
+ throw new EvMaxListenerError(listeners.size, name)
151
+ }
152
+
153
+ listeners.add(id)
154
+ }
155
+
156
+ /**
157
+ * Removes a client from a specific channel.
158
+ * Deletes the channel if no listeners remain.
159
+ */
160
+ #unlisten(name: string, id: string) {
161
+ const isListenerExists = this.#listeners.get(name)
162
+
163
+ if (isListenerExists) {
164
+ isListenerExists.delete(id)
165
+
166
+ if (isListenerExists.size === 0) {
167
+ this.#listeners.delete(name)
168
+ }
169
+ }
170
+ }
171
+ }
package/src/message.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { EvMessage } from './types.js'
2
+ import { safeJsonParse } from './utils.js'
3
+
4
+ /**
5
+ *
6
+ * This function convert the data to event stream compatible format.
7
+ *
8
+ * @param msg Message which you want to send to the client.
9
+ */
10
+ export function message(msg: EvMessage) {
11
+ const event = `event:${msg.event || 'message'}\n`
12
+ const data = `data:${safeJsonParse(msg.data)}\n`
13
+
14
+ if (data === '') {
15
+ return `${msg.id ? `id:${msg.id}\n` : ''}${event}\n`
16
+ }
17
+
18
+ return `${msg.id ? `id:${msg.id}\n` : ''}${event}${data}\n`
19
+ }
package/src/state.ts ADDED
@@ -0,0 +1,84 @@
1
+ import loadash from 'lodash'
2
+
3
+ import { EvStreamManager } from './manager.js'
4
+ import { EvStateAdapter, EvStateOptions } from './types.js'
5
+
6
+ const { isEqual } = loadash
7
+
8
+ type EvSetState<T> = (val: T) => T
9
+
10
+ /**
11
+ * EvState holds a reactive state and broadcasts updates to a channel using EvStreamManager.
12
+ */
13
+ export class EvState<T> {
14
+ #value: T
15
+ #channel: string
16
+ #manager: EvStreamManager
17
+ #key: string
18
+ #adapter?: EvStateAdapter
19
+
20
+ constructor({
21
+ channel,
22
+ initialValue,
23
+ manager,
24
+ key,
25
+ adapter,
26
+ }: EvStateOptions<T>) {
27
+ this.#value = initialValue
28
+ this.#channel = channel
29
+ this.#manager = manager
30
+ this.#key = key || 'value'
31
+ this.#adapter = adapter
32
+
33
+ if (this.#adapter) {
34
+ this.#adapter.subscribe(this.#channel, (data) => {
35
+ this.#handleRemoteUpdate(data)
36
+ })
37
+ }
38
+ }
39
+
40
+ #handleRemoteUpdate(data: any) {
41
+ if (data && typeof data === 'object' && this.#key in data) {
42
+ const newValue = data[this.#key]
43
+
44
+ if (!isEqual(newValue, this.#value)) {
45
+ this.#value = newValue
46
+ this.#manager.send(this.#channel, {
47
+ event: this.#channel,
48
+ data: {
49
+ [this.#key]: newValue,
50
+ },
51
+ })
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Returns the current state value.
58
+ */
59
+ get() {
60
+ return this.#value
61
+ }
62
+
63
+ /**
64
+ * Updates the state using a callback.
65
+ * Broadcasts the new value if it has changed.
66
+ */
67
+ set(callback: EvSetState<T>) {
68
+ const newValue = callback(this.#value)
69
+
70
+ if (!isEqual(newValue, this.#value)) {
71
+ this.#value = newValue
72
+ this.#manager.send(this.#channel, {
73
+ event: this.#channel,
74
+ data: {
75
+ [this.#key]: newValue,
76
+ },
77
+ })
78
+
79
+ if (this.#adapter) {
80
+ this.#adapter.publish(this.#channel, { [this.#key]: newValue })
81
+ }
82
+ }
83
+ }
84
+ }
package/src/stream.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { IncomingMessage, ServerResponse } from 'http'
2
+ import { EvMessage, EvOptions } from './types.js'
3
+ import { message } from './message.js'
4
+
5
+ /**
6
+ * Evstream manages a Server-Sent Events (SSE) connection.
7
+ * Sets necessary headers, handles heartbeat, authentication, sending messages, and closing the stream.
8
+ * Example :
9
+ *
10
+ * ```javascript
11
+ * const ev = new Evstream(req, res);
12
+ *
13
+ * ev.message({event: "message", data: {message: "a message"}, id: "event_id_1"})
14
+ * ```
15
+ */
16
+ export class Evstream {
17
+ #res: ServerResponse
18
+ #opts?: EvOptions
19
+ #url: URL
20
+ #heartbeatInterval?: NodeJS.Timeout
21
+ #onCloseHandler?: () => void
22
+ constructor(req: IncomingMessage, res: ServerResponse, opts?: EvOptions) {
23
+ this.#res = res
24
+ this.#opts = opts
25
+ this.#url = new URL(req.url!, `http://${req.headers.host}`)
26
+
27
+ this.#res.setHeader('Content-Type', 'text/event-stream')
28
+ this.#res.setHeader('Cache-Control', 'no-cache')
29
+ this.#res.setHeader('Connection', 'keep-alive')
30
+ this.#res.flushHeaders()
31
+
32
+ if (opts?.heartbeat) {
33
+ this.#heartbeatInterval = setInterval(() => {
34
+ this.#res.write(message({ event: 'heartbeat', data: '' }))
35
+ }, this.#opts.heartbeat)
36
+
37
+ this.#onCloseHandler = () => {
38
+ this.#clearHeartbeat()
39
+ }
40
+
41
+ this.#res.on('close', this.#onCloseHandler)
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Clears the heartbeat interval if it exists.
47
+ * Prevents memory leaks by ensuring the interval is properly cleaned up.
48
+ */
49
+ #clearHeartbeat() {
50
+ if (this.#heartbeatInterval) {
51
+ clearInterval(this.#heartbeatInterval)
52
+ this.#heartbeatInterval = undefined
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Removes the close event listener to prevent memory leaks.
58
+ */
59
+ #removeCloseListener() {
60
+ if (this.#onCloseHandler) {
61
+ this.#res.removeListener('close', this.#onCloseHandler)
62
+ this.#onCloseHandler = undefined
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Handles optional authentication using provided token verification.
68
+ * Sends error message and closes connection if authentication fails.
69
+ */
70
+ async authenticate() {
71
+ if (this.#opts.authentication) {
72
+ const token = this.#url.searchParams.get(this.#opts.authentication.param)
73
+
74
+ const isAuthenticated = await this.#opts.authentication.verify(token)
75
+
76
+ if (typeof isAuthenticated === 'boolean') {
77
+ if (!isAuthenticated) {
78
+ this.#clearHeartbeat()
79
+ this.message({
80
+ data: { message: 'authentication failed' },
81
+ event: 'error',
82
+ })
83
+ this.#res.end()
84
+ return false
85
+ }
86
+
87
+ return true
88
+ }
89
+
90
+ if (typeof isAuthenticated === 'object') {
91
+ this.message(isAuthenticated)
92
+ return true
93
+ }
94
+
95
+ return false
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Sends an SSE message to the client.
101
+ * Accepts an `EvMessage` object.
102
+ */
103
+ message(msg: EvMessage) {
104
+ this.#res.write(message(msg))
105
+ }
106
+
107
+ /**
108
+ * Sends an "end" event and closes the SSE connection.
109
+ * Cleans up heartbeat interval and event listeners to prevent memory leaks.
110
+ */
111
+ close() {
112
+ this.#clearHeartbeat()
113
+ this.#removeCloseListener()
114
+
115
+ this.message({
116
+ event: 'end',
117
+ data: '',
118
+ })
119
+
120
+ this.#res.end()
121
+ }
122
+ }
package/src/types.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { EvStreamManager } from './manager.js'
2
+
3
+ // Built-in event types.
4
+ export type EvEventsType = 'data' | 'error' | 'end'
5
+
6
+ // Represents a message sent to the client over SSE.
7
+ export interface EvMessage {
8
+ // Optional event name.
9
+ event?: string | EvEventsType
10
+ // Data to send; can be a string or object.
11
+ data: string | object
12
+ // Optional ID of the event.
13
+ id?: string
14
+ }
15
+
16
+ // Options for token-based authentication from query parameters.
17
+ export interface EvAuthenticationOptions {
18
+ method: 'query'
19
+ param: string
20
+ verify: (token: string) => Promise<EvMessage> | undefined | null | boolean
21
+ }
22
+
23
+ // Options for configuring a single SSE stream.
24
+ export interface EvOptions {
25
+ authentication?: EvAuthenticationOptions
26
+ heartbeat?: number
27
+ }
28
+
29
+ // Configuration options for EvStreamManager.
30
+ export interface EvManagerOptions {
31
+ // Unique ID for the manager
32
+ id?: string
33
+
34
+ // Max Connection which a manager can handle. If this limit exceeds it throws `EvMaxConnectionsError`
35
+ maxConnection?: number
36
+
37
+ // Max Listeners which a listener can broadcast a message to. If this limit exceeds it throw `EvMaxListenerError`
38
+ maxListeners?: number
39
+ }
40
+
41
+ // Options for initializing EvState.
42
+ export interface EvStateAdapter {
43
+ publish(channel: string, message: any): Promise<void>
44
+ subscribe(channel: string, onMessage: (message: any) => void): Promise<void>
45
+ unsubscribe(channel: string): Promise<void>
46
+ }
47
+
48
+ export interface EvStateOptions<T> {
49
+ initialValue: T
50
+ channel: string
51
+ manager: EvStreamManager
52
+ key?: string
53
+ adapter?: EvStateAdapter
54
+ }
55
+
56
+ export type EvOnClose = (channels: string[]) => Promise<void>
package/src/utils.ts ADDED
@@ -0,0 +1,29 @@
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: any) {
9
+ if (typeof val === 'string') {
10
+ return val
11
+ }
12
+
13
+ if (typeof val === 'object') {
14
+ try {
15
+ return JSON.stringify(val)
16
+ } catch (error) {
17
+ return ''
18
+ }
19
+ }
20
+
21
+ return ''
22
+ }
23
+
24
+ export function uid(opts?: { prefix?: string; counter?: number }) {
25
+ const now = Date.now().toString(36)
26
+ const rand = Math.random().toString(26).substring(2, 10)
27
+
28
+ return `${opts?.prefix ? `${opts?.prefix}-` : ''}${now}-${rand}-${opts?.counter}`
29
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compileOnSave": true,
3
+ "compilerOptions": {
4
+ "newLine": "LF",
5
+ "rootDir": "src/",
6
+ "outDir": "dist/",
7
+ "module": "ESNext",
8
+ "target": "ES2015",
9
+ "declaration": true,
10
+ "incremental": true,
11
+ "alwaysStrict": true,
12
+ "esModuleInterop": true,
13
+ "moduleResolution": "node",
14
+ "preserveConstEnums": true
15
+ }
16
+ }