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/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
+ }