evstream 1.0.0 → 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
+ }
@@ -0,0 +1,10 @@
1
+ import { RedisOptions } from 'ioredis';
2
+ import { EvStateAdapter } from '../types.js';
3
+ export declare class EvRedisAdapter implements EvStateAdapter {
4
+ #private;
5
+ constructor(options?: RedisOptions);
6
+ publish(channel: string, message: any): Promise<void>;
7
+ subscribe(channel: string, onMessage: (message: any) => void): Promise<void>;
8
+ unsubscribe(channel: string): Promise<void>;
9
+ quit(): void;
10
+ }
@@ -0,0 +1,70 @@
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 _EvRedisAdapter_pub, _EvRedisAdapter_sub, _EvRedisAdapter_listeners;
22
+ import Redis from 'ioredis';
23
+ export class EvRedisAdapter {
24
+ constructor(options) {
25
+ _EvRedisAdapter_pub.set(this, void 0);
26
+ _EvRedisAdapter_sub.set(this, void 0);
27
+ _EvRedisAdapter_listeners.set(this, void 0);
28
+ __classPrivateFieldSet(this, _EvRedisAdapter_pub, new Redis(options), "f");
29
+ __classPrivateFieldSet(this, _EvRedisAdapter_sub, new Redis(options), "f");
30
+ __classPrivateFieldSet(this, _EvRedisAdapter_listeners, new Map(), "f");
31
+ __classPrivateFieldGet(this, _EvRedisAdapter_sub, "f").on('message', (channel, message) => {
32
+ const handlers = __classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").get(channel);
33
+ if (handlers) {
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(message);
37
+ }
38
+ catch (_a) {
39
+ return;
40
+ }
41
+ handlers.forEach((handler) => handler(parsed));
42
+ }
43
+ });
44
+ }
45
+ publish(channel, message) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ yield __classPrivateFieldGet(this, _EvRedisAdapter_pub, "f").publish(channel, JSON.stringify(message));
48
+ });
49
+ }
50
+ subscribe(channel, onMessage) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ if (!__classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").has(channel)) {
53
+ __classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").set(channel, new Set());
54
+ yield __classPrivateFieldGet(this, _EvRedisAdapter_sub, "f").subscribe(channel);
55
+ }
56
+ __classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").get(channel).add(onMessage);
57
+ });
58
+ }
59
+ unsubscribe(channel) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ yield __classPrivateFieldGet(this, _EvRedisAdapter_sub, "f").unsubscribe(channel);
62
+ __classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").delete(channel);
63
+ });
64
+ }
65
+ quit() {
66
+ __classPrivateFieldGet(this, _EvRedisAdapter_pub, "f").quit();
67
+ __classPrivateFieldGet(this, _EvRedisAdapter_sub, "f").quit();
68
+ }
69
+ }
70
+ _EvRedisAdapter_pub = new WeakMap(), _EvRedisAdapter_sub = new WeakMap(), _EvRedisAdapter_listeners = new WeakMap();
package/dist/manager.js CHANGED
@@ -63,17 +63,29 @@ export class EvStreamManager {
63
63
  onClose(channel);
64
64
  }
65
65
  isClosed = true;
66
+ // Remove close event listener to prevent memory leaks
67
+ res.removeAllListeners('close');
68
+ // Clean up client
66
69
  client.close();
70
+ // Decrement count
67
71
  __classPrivateFieldSet(this, _EvStreamManager_count, __classPrivateFieldGet(this, _EvStreamManager_count, "f") - 1, "f");
72
+ // Remove from all channels
68
73
  channel.forEach(chan => __classPrivateFieldGet(this, _EvStreamManager_instances, "m", _EvStreamManager_unlisten).call(this, chan, id));
74
+ // Clear channel array to release references
75
+ channel.length = 0;
76
+ // Remove client from map
69
77
  __classPrivateFieldGet(this, _EvStreamManager_clients, "f").delete(id);
70
- res.end();
78
+ // End response if not already ended
79
+ if (!res.writableEnded) {
80
+ res.end();
81
+ }
71
82
  };
72
- res.on('close', () => {
83
+ const onCloseHandler = () => {
73
84
  if (!isClosed) {
74
85
  close();
75
86
  }
76
- });
87
+ };
88
+ res.on('close', onCloseHandler);
77
89
  return {
78
90
  authenticate: client.authenticate.bind(client),
79
91
  message: client.message.bind(client),
@@ -91,6 +103,8 @@ export class EvStreamManager {
91
103
  */
92
104
  send(name, msg) {
93
105
  const listeners = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name);
106
+ if (!listeners)
107
+ return;
94
108
  for (const [_, id] of listeners.entries()) {
95
109
  const client = __classPrivateFieldGet(this, _EvStreamManager_clients, "f").get(id);
96
110
  if (client) {
@@ -102,15 +116,15 @@ export class EvStreamManager {
102
116
  }
103
117
  }
104
118
  _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());
119
+ let listeners = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name);
120
+ if (!listeners) {
121
+ listeners = new Set();
122
+ __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").set(name, listeners);
123
+ }
124
+ if (listeners.size >= __classPrivateFieldGet(this, _EvStreamManager_maxListeners, "f")) {
125
+ throw new EvMaxListenerError(listeners.size, name);
112
126
  }
113
- __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name).add(id);
127
+ listeners.add(id);
114
128
  }, _EvStreamManager_unlisten = function _EvStreamManager_unlisten(name, id) {
115
129
  const isListenerExists = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name);
116
130
  if (isListenerExists) {
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 }: 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
@@ -9,22 +9,30 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
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
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _EvState_value, _EvState_channel, _EvState_manager, _EvState_key;
12
+ var _EvState_instances, _EvState_value, _EvState_channel, _EvState_manager, _EvState_key, _EvState_adapter, _EvState_handleRemoteUpdate;
13
13
  import loadash from 'lodash';
14
14
  const { isEqual } = loadash;
15
15
  /**
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 }) {
19
+ constructor({ channel, initialValue, manager, key, adapter, }) {
20
+ _EvState_instances.add(this);
20
21
  _EvState_value.set(this, void 0);
21
22
  _EvState_channel.set(this, void 0);
22
23
  _EvState_manager.set(this, void 0);
23
24
  _EvState_key.set(this, void 0);
25
+ _EvState_adapter.set(this, void 0);
24
26
  __classPrivateFieldSet(this, _EvState_value, initialValue, "f");
25
27
  __classPrivateFieldSet(this, _EvState_channel, channel, "f");
26
28
  __classPrivateFieldSet(this, _EvState_manager, manager, "f");
27
29
  __classPrivateFieldSet(this, _EvState_key, key || 'value', "f");
30
+ __classPrivateFieldSet(this, _EvState_adapter, adapter, "f");
31
+ if (__classPrivateFieldGet(this, _EvState_adapter, "f")) {
32
+ __classPrivateFieldGet(this, _EvState_adapter, "f").subscribe(__classPrivateFieldGet(this, _EvState_channel, "f"), (data) => {
33
+ __classPrivateFieldGet(this, _EvState_instances, "m", _EvState_handleRemoteUpdate).call(this, data);
34
+ });
35
+ }
28
36
  }
29
37
  /**
30
38
  * Returns the current state value.
@@ -46,7 +54,23 @@ export class EvState {
46
54
  [__classPrivateFieldGet(this, _EvState_key, "f")]: newValue,
47
55
  },
48
56
  });
57
+ if (__classPrivateFieldGet(this, _EvState_adapter, "f")) {
58
+ __classPrivateFieldGet(this, _EvState_adapter, "f").publish(__classPrivateFieldGet(this, _EvState_channel, "f"), { [__classPrivateFieldGet(this, _EvState_key, "f")]: newValue });
59
+ }
49
60
  }
50
61
  }
51
62
  }
52
- _EvState_value = new WeakMap(), _EvState_channel = new WeakMap(), _EvState_manager = new WeakMap(), _EvState_key = new WeakMap();
63
+ _EvState_value = new WeakMap(), _EvState_channel = new WeakMap(), _EvState_manager = new WeakMap(), _EvState_key = new WeakMap(), _EvState_adapter = new WeakMap(), _EvState_instances = new WeakSet(), _EvState_handleRemoteUpdate = function _EvState_handleRemoteUpdate(data) {
64
+ if (data && typeof data === 'object' && __classPrivateFieldGet(this, _EvState_key, "f") in data) {
65
+ const newValue = data[__classPrivateFieldGet(this, _EvState_key, "f")];
66
+ if (!isEqual(newValue, __classPrivateFieldGet(this, _EvState_value, "f"))) {
67
+ __classPrivateFieldSet(this, _EvState_value, newValue, "f");
68
+ __classPrivateFieldGet(this, _EvState_manager, "f").send(__classPrivateFieldGet(this, _EvState_channel, "f"), {
69
+ event: __classPrivateFieldGet(this, _EvState_channel, "f"),
70
+ data: {
71
+ [__classPrivateFieldGet(this, _EvState_key, "f")]: newValue,
72
+ },
73
+ });
74
+ }
75
+ }
76
+ };
package/dist/stream.d.ts CHANGED
@@ -26,6 +26,7 @@ export declare class Evstream {
26
26
  message(msg: EvMessage): void;
27
27
  /**
28
28
  * Sends an "end" event and closes the SSE connection.
29
+ * Cleans up heartbeat interval and event listeners to prevent memory leaks.
29
30
  */
30
31
  close(): void;
31
32
  }
package/dist/stream.js CHANGED
@@ -18,7 +18,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
18
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
19
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
20
20
  };
21
- var _Evstream_res, _Evstream_opts, _Evstream_url;
21
+ var _Evstream_instances, _Evstream_res, _Evstream_opts, _Evstream_url, _Evstream_heartbeatInterval, _Evstream_onCloseHandler, _Evstream_clearHeartbeat, _Evstream_removeCloseListener;
22
22
  import { message } from './message.js';
23
23
  /**
24
24
  * Evstream manages a Server-Sent Events (SSE) connection.
@@ -33,9 +33,12 @@ import { message } from './message.js';
33
33
  */
34
34
  export class Evstream {
35
35
  constructor(req, res, opts) {
36
+ _Evstream_instances.add(this);
36
37
  _Evstream_res.set(this, void 0);
37
38
  _Evstream_opts.set(this, void 0);
38
39
  _Evstream_url.set(this, void 0);
40
+ _Evstream_heartbeatInterval.set(this, void 0);
41
+ _Evstream_onCloseHandler.set(this, void 0);
39
42
  __classPrivateFieldSet(this, _Evstream_res, res, "f");
40
43
  __classPrivateFieldSet(this, _Evstream_opts, opts, "f");
41
44
  __classPrivateFieldSet(this, _Evstream_url, new URL(req.url, `http://${req.headers.host}`), "f");
@@ -44,12 +47,13 @@ export class Evstream {
44
47
  __classPrivateFieldGet(this, _Evstream_res, "f").setHeader('Connection', 'keep-alive');
45
48
  __classPrivateFieldGet(this, _Evstream_res, "f").flushHeaders();
46
49
  if (opts === null || opts === void 0 ? void 0 : opts.heartbeat) {
47
- const timeout = setInterval(() => {
50
+ __classPrivateFieldSet(this, _Evstream_heartbeatInterval, setInterval(() => {
48
51
  __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
- });
52
+ }, __classPrivateFieldGet(this, _Evstream_opts, "f").heartbeat), "f");
53
+ __classPrivateFieldSet(this, _Evstream_onCloseHandler, () => {
54
+ __classPrivateFieldGet(this, _Evstream_instances, "m", _Evstream_clearHeartbeat).call(this);
55
+ }, "f");
56
+ __classPrivateFieldGet(this, _Evstream_res, "f").on('close', __classPrivateFieldGet(this, _Evstream_onCloseHandler, "f"));
53
57
  }
54
58
  }
55
59
  /**
@@ -63,6 +67,7 @@ export class Evstream {
63
67
  const isAuthenticated = yield __classPrivateFieldGet(this, _Evstream_opts, "f").authentication.verify(token);
64
68
  if (typeof isAuthenticated === 'boolean') {
65
69
  if (!isAuthenticated) {
70
+ __classPrivateFieldGet(this, _Evstream_instances, "m", _Evstream_clearHeartbeat).call(this);
66
71
  this.message({
67
72
  data: { message: 'authentication failed' },
68
73
  event: 'error',
@@ -89,8 +94,11 @@ export class Evstream {
89
94
  }
90
95
  /**
91
96
  * Sends an "end" event and closes the SSE connection.
97
+ * Cleans up heartbeat interval and event listeners to prevent memory leaks.
92
98
  */
93
99
  close() {
100
+ __classPrivateFieldGet(this, _Evstream_instances, "m", _Evstream_clearHeartbeat).call(this);
101
+ __classPrivateFieldGet(this, _Evstream_instances, "m", _Evstream_removeCloseListener).call(this);
94
102
  this.message({
95
103
  event: 'end',
96
104
  data: '',
@@ -98,4 +106,14 @@ export class Evstream {
98
106
  __classPrivateFieldGet(this, _Evstream_res, "f").end();
99
107
  }
100
108
  }
101
- _Evstream_res = new WeakMap(), _Evstream_opts = new WeakMap(), _Evstream_url = new WeakMap();
109
+ _Evstream_res = new WeakMap(), _Evstream_opts = new WeakMap(), _Evstream_url = new WeakMap(), _Evstream_heartbeatInterval = new WeakMap(), _Evstream_onCloseHandler = new WeakMap(), _Evstream_instances = new WeakSet(), _Evstream_clearHeartbeat = function _Evstream_clearHeartbeat() {
110
+ if (__classPrivateFieldGet(this, _Evstream_heartbeatInterval, "f")) {
111
+ clearInterval(__classPrivateFieldGet(this, _Evstream_heartbeatInterval, "f"));
112
+ __classPrivateFieldSet(this, _Evstream_heartbeatInterval, undefined, "f");
113
+ }
114
+ }, _Evstream_removeCloseListener = function _Evstream_removeCloseListener() {
115
+ if (__classPrivateFieldGet(this, _Evstream_onCloseHandler, "f")) {
116
+ __classPrivateFieldGet(this, _Evstream_res, "f").removeListener('close', __classPrivateFieldGet(this, _Evstream_onCloseHandler, "f"));
117
+ __classPrivateFieldSet(this, _Evstream_onCloseHandler, undefined, "f");
118
+ }
119
+ };
package/dist/types.d.ts CHANGED
@@ -19,10 +19,16 @@ export interface EvManagerOptions {
19
19
  maxConnection?: number;
20
20
  maxListeners?: number;
21
21
  }
22
+ export interface EvStateAdapter {
23
+ publish(channel: string, message: any): Promise<void>;
24
+ subscribe(channel: string, onMessage: (message: any) => void): Promise<void>;
25
+ unsubscribe(channel: string): Promise<void>;
26
+ }
22
27
  export interface EvStateOptions<T> {
23
28
  initialValue: T;
24
29
  channel: string;
25
30
  manager: EvStreamManager;
26
31
  key?: string;
32
+ adapter?: EvStateAdapter;
27
33
  }
28
34
  export type EvOnClose = (channels: string[]) => Promise<void>;
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,47 +1,73 @@
1
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
- }
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
+ }
47
73
  }
package/readme.md CHANGED
@@ -188,7 +188,35 @@ Reactive states are data which you can shared across multiple clients within the
188
188
 
189
189
  **See** `channel` **and the value pass to the** `listen()` **must be the same**
190
190
 
191
- ### 5. Sending data to a channel
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
192
220
 
193
221
  To send data to a channel you can use `send()` method from `EvStreamManager` class.
194
222
 
@@ -202,7 +230,7 @@ const manager = new EvStreamManager();
202
230
  manager.send("<channel-name>", {event: "custom-event", data: {"foo": "bar"}})
203
231
  ```
204
232
 
205
- ### 6. Listening for channels
233
+ ### 7. Listening for channels
206
234
 
207
235
  To listen for data from any channel you can use `listen()` function from `Evstream` class.
208
236
 
@@ -368,7 +396,8 @@ new EvState<T>({
368
396
  channel,
369
397
  initialValue,
370
398
  manager,
371
- key
399
+ key,
400
+ adapter
372
401
  }: EvStateOptions<T>)
373
402
  ```
374
403
 
@@ -378,6 +407,7 @@ new EvState<T>({
378
407
  * `initialValue`: `T` – The initial state value.
379
408
  * `manager`: `EvStreamManager` – The SSE manager instance used for broadcasting.
380
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`).
381
411
 
382
412
  ---
383
413
 
@@ -437,13 +467,31 @@ new EvMaxConnectionsError(connections: number)
437
467
  ```ts
438
468
  const manager = new EvStreamManager({ maxConnection: 100 });
439
469
  if (tooManyConnections) {
470
+ ```
471
+
440
472
  throw new EvMaxConnectionsError(100)
441
473
  }
442
474
  ```
443
475
 
444
476
  ---
445
-
446
- ## `EvMaxListenerError`
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`
447
495
 
448
496
  Represents an error thrown when the number of listeners on a given channel exceeds the allowed `maxListeners` limit (default: `5000`).
449
497
 
@@ -573,6 +621,7 @@ Options for initializing a reactive state with `EvState`.
573
621
  - `channel`: Channel name for broadcasting
574
622
  - `manager`: Instance of `EvStreamManager`
575
623
  - `key` *(optional)*: Key for wrapping state in the broadcast (default: `'value'`)
624
+ - `adapter` *(optional)*: Instance of `EvStateAdapter` (e.g., `EvRedisAdapter`) for distributed synchronization.
576
625
 
577
626
  ---
578
627
 
@@ -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
+ }