evstream 1.0.3 → 1.0.4
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/dist/adapters/pub-sub.d.ts +33 -7
- package/dist/adapters/pub-sub.js +37 -12
- package/dist/adapters/redis.d.ts +43 -0
- package/dist/adapters/redis.js +64 -13
- package/dist/extensions/state-manager.d.ts +74 -0
- package/dist/extensions/state-manager.js +69 -2
- package/dist/manager.d.ts +22 -12
- package/dist/manager.js +90 -49
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/manager.ts +209 -156
- package/src/types.ts +28 -25
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import { RedisOptions } from 'ioredis';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for EvRedisPubSub
|
|
4
|
+
*/
|
|
5
|
+
interface EvRedisPubSubOptions<T> {
|
|
6
|
+
/** Redis Pub/Sub channel name */
|
|
7
|
+
subject: string;
|
|
8
|
+
/** Redis connection options */
|
|
4
9
|
options: RedisOptions;
|
|
5
|
-
|
|
10
|
+
/** Optional initial message handler */
|
|
11
|
+
onMessage?: (message: T) => void;
|
|
6
12
|
}
|
|
7
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Redis-based Pub/Sub helper for cross-process communication.
|
|
15
|
+
*
|
|
16
|
+
* - Uses separate publisher and subscriber connections
|
|
17
|
+
* - Prevents self-message delivery using instance UID
|
|
18
|
+
* - Typed message payload via generics
|
|
19
|
+
*/
|
|
20
|
+
export declare class EvRedisPubSub<T = unknown> {
|
|
8
21
|
#private;
|
|
9
|
-
constructor({ options, subject, onMessage }: EvRedisPubSubOptions);
|
|
22
|
+
constructor({ options, subject, onMessage }: EvRedisPubSubOptions<T>);
|
|
23
|
+
/**
|
|
24
|
+
* Initializes Redis subscriptions and listeners.
|
|
25
|
+
*/
|
|
10
26
|
private init;
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Publishes a message to the Redis channel.
|
|
29
|
+
*/
|
|
30
|
+
send(msg: T): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Registers or replaces the message handler.
|
|
33
|
+
*/
|
|
34
|
+
onMessage(callback: (msg: T) => void): void;
|
|
35
|
+
/**
|
|
36
|
+
* Gracefully closes Redis connections.
|
|
37
|
+
*/
|
|
38
|
+
close(): Promise<void>;
|
|
13
39
|
}
|
|
14
40
|
export {};
|
package/dist/adapters/pub-sub.js
CHANGED
|
@@ -18,12 +18,18 @@ 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
|
|
21
|
+
var _EvRedisPubSub_subject, _EvRedisPubSub_pub, _EvRedisPubSub_sub, _EvRedisPubSub_instanceId, _EvRedisPubSub_onMessage;
|
|
22
22
|
import Redis from 'ioredis';
|
|
23
23
|
import { uid } from '../utils.js';
|
|
24
|
+
/**
|
|
25
|
+
* Redis-based Pub/Sub helper for cross-process communication.
|
|
26
|
+
*
|
|
27
|
+
* - Uses separate publisher and subscriber connections
|
|
28
|
+
* - Prevents self-message delivery using instance UID
|
|
29
|
+
* - Typed message payload via generics
|
|
30
|
+
*/
|
|
24
31
|
export class EvRedisPubSub {
|
|
25
32
|
constructor({ options, subject, onMessage }) {
|
|
26
|
-
_EvRedisPubSub_options.set(this, void 0);
|
|
27
33
|
_EvRedisPubSub_subject.set(this, void 0);
|
|
28
34
|
_EvRedisPubSub_pub.set(this, void 0);
|
|
29
35
|
_EvRedisPubSub_sub.set(this, void 0);
|
|
@@ -31,36 +37,55 @@ export class EvRedisPubSub {
|
|
|
31
37
|
_EvRedisPubSub_onMessage.set(this, void 0);
|
|
32
38
|
__classPrivateFieldSet(this, _EvRedisPubSub_pub, new Redis(options), "f");
|
|
33
39
|
__classPrivateFieldSet(this, _EvRedisPubSub_sub, new Redis(options), "f");
|
|
40
|
+
__classPrivateFieldSet(this, _EvRedisPubSub_subject, subject, "f");
|
|
34
41
|
__classPrivateFieldSet(this, _EvRedisPubSub_onMessage, onMessage, "f");
|
|
35
42
|
__classPrivateFieldSet(this, _EvRedisPubSub_instanceId, uid({ prefix: subject, counter: Math.random() }), "f");
|
|
36
|
-
__classPrivateFieldSet(this, _EvRedisPubSub_subject, subject, "f");
|
|
37
43
|
this.init();
|
|
38
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Initializes Redis subscriptions and listeners.
|
|
47
|
+
*/
|
|
39
48
|
init() {
|
|
40
49
|
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
-
__classPrivateFieldGet(this, _EvRedisPubSub_pub, "f").on('error', (
|
|
42
|
-
__classPrivateFieldGet(this, _EvRedisPubSub_sub, "f").on('error', (
|
|
50
|
+
__classPrivateFieldGet(this, _EvRedisPubSub_pub, "f").on('error', () => { });
|
|
51
|
+
__classPrivateFieldGet(this, _EvRedisPubSub_sub, "f").on('error', () => { });
|
|
43
52
|
yield __classPrivateFieldGet(this, _EvRedisPubSub_sub, "f").subscribe(__classPrivateFieldGet(this, _EvRedisPubSub_subject, "f"));
|
|
44
53
|
__classPrivateFieldGet(this, _EvRedisPubSub_sub, "f").on('message', (_, raw) => {
|
|
54
|
+
var _a;
|
|
45
55
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
__classPrivateFieldGet(this, _EvRedisPubSub_onMessage, "f").call(this, msg === null || msg === void 0 ? void 0 : msg.msg);
|
|
56
|
+
const data = JSON.parse(raw);
|
|
57
|
+
// Ignore messages from the same instance
|
|
58
|
+
if ((data === null || data === void 0 ? void 0 : data.uid) !== __classPrivateFieldGet(this, _EvRedisPubSub_instanceId, "f")) {
|
|
59
|
+
(_a = __classPrivateFieldGet(this, _EvRedisPubSub_onMessage, "f")) === null || _a === void 0 ? void 0 : _a.call(this, data.msg);
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
|
-
catch (
|
|
62
|
+
catch (_b) {
|
|
63
|
+
// Ignore malformed payloads
|
|
64
|
+
}
|
|
54
65
|
});
|
|
55
66
|
});
|
|
56
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Publishes a message to the Redis channel.
|
|
70
|
+
*/
|
|
57
71
|
send(msg) {
|
|
58
72
|
return __awaiter(this, void 0, void 0, function* () {
|
|
59
73
|
yield __classPrivateFieldGet(this, _EvRedisPubSub_pub, "f").publish(__classPrivateFieldGet(this, _EvRedisPubSub_subject, "f"), JSON.stringify({ uid: __classPrivateFieldGet(this, _EvRedisPubSub_instanceId, "f"), msg }));
|
|
60
74
|
});
|
|
61
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Registers or replaces the message handler.
|
|
78
|
+
*/
|
|
62
79
|
onMessage(callback) {
|
|
63
80
|
__classPrivateFieldSet(this, _EvRedisPubSub_onMessage, callback, "f");
|
|
64
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Gracefully closes Redis connections.
|
|
84
|
+
*/
|
|
85
|
+
close() {
|
|
86
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
yield Promise.all([__classPrivateFieldGet(this, _EvRedisPubSub_pub, "f").quit(), __classPrivateFieldGet(this, _EvRedisPubSub_sub, "f").quit()]);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
65
90
|
}
|
|
66
|
-
|
|
91
|
+
_EvRedisPubSub_subject = new WeakMap(), _EvRedisPubSub_pub = new WeakMap(), _EvRedisPubSub_sub = new WeakMap(), _EvRedisPubSub_instanceId = new WeakMap(), _EvRedisPubSub_onMessage = new WeakMap();
|
package/dist/adapters/redis.d.ts
CHANGED
|
@@ -1,12 +1,55 @@
|
|
|
1
1
|
import { RedisOptions } from 'ioredis';
|
|
2
2
|
import { EvRedisPubSub } from './pub-sub.js';
|
|
3
3
|
import { EvStateAdapter } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Redis-based implementation of {@link EvStateAdapter}.
|
|
6
|
+
*
|
|
7
|
+
* This adapter enables distributed state updates using Redis Pub/Sub.
|
|
8
|
+
* It supports:
|
|
9
|
+
* - Channel-based subscriptions
|
|
10
|
+
* - Multiple listeners per channel
|
|
11
|
+
* - Self-message filtering via instance ID
|
|
12
|
+
*
|
|
13
|
+
* Designed to be used by EvState / EvStateManager for
|
|
14
|
+
* cross-process state synchronization.
|
|
15
|
+
*/
|
|
4
16
|
declare class EvRedisAdapter implements EvStateAdapter {
|
|
5
17
|
#private;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new Redis state adapter.
|
|
20
|
+
*
|
|
21
|
+
* @param options - Optional Redis connection options
|
|
22
|
+
*/
|
|
6
23
|
constructor(options?: RedisOptions);
|
|
24
|
+
/**
|
|
25
|
+
* Publishes a message to a Redis channel.
|
|
26
|
+
*
|
|
27
|
+
* The payload is wrapped with the instance ID to
|
|
28
|
+
* prevent self-delivery.
|
|
29
|
+
*
|
|
30
|
+
* @param channel - Redis channel name
|
|
31
|
+
* @param message - Message payload
|
|
32
|
+
*/
|
|
7
33
|
publish(channel: string, message: any): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Subscribes to a Redis channel.
|
|
36
|
+
*
|
|
37
|
+
* Multiple listeners can be registered per channel.
|
|
38
|
+
* The Redis subscription is created only once per channel.
|
|
39
|
+
*
|
|
40
|
+
* @param channel - Redis channel name
|
|
41
|
+
* @param onMessage - Callback invoked on incoming messages
|
|
42
|
+
*/
|
|
8
43
|
subscribe(channel: string, onMessage: (message: any) => void): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Unsubscribes from a Redis channel and removes all listeners.
|
|
46
|
+
*
|
|
47
|
+
* @param channel - Redis channel name
|
|
48
|
+
*/
|
|
9
49
|
unsubscribe(channel: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Gracefully closes Redis connections.
|
|
52
|
+
*/
|
|
10
53
|
quit(): void;
|
|
11
54
|
}
|
|
12
55
|
export { EvRedisPubSub, EvRedisAdapter };
|
package/dist/adapters/redis.js
CHANGED
|
@@ -22,11 +22,35 @@ var _EvRedisAdapter_pub, _EvRedisAdapter_sub, _EvRedisAdapter_listeners, _EvRedi
|
|
|
22
22
|
import Redis from 'ioredis';
|
|
23
23
|
import { EvRedisPubSub } from './pub-sub.js';
|
|
24
24
|
import { uid } from '../utils.js';
|
|
25
|
+
/**
|
|
26
|
+
* Redis-based implementation of {@link EvStateAdapter}.
|
|
27
|
+
*
|
|
28
|
+
* This adapter enables distributed state updates using Redis Pub/Sub.
|
|
29
|
+
* It supports:
|
|
30
|
+
* - Channel-based subscriptions
|
|
31
|
+
* - Multiple listeners per channel
|
|
32
|
+
* - Self-message filtering via instance ID
|
|
33
|
+
*
|
|
34
|
+
* Designed to be used by EvState / EvStateManager for
|
|
35
|
+
* cross-process state synchronization.
|
|
36
|
+
*/
|
|
25
37
|
class EvRedisAdapter {
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new Redis state adapter.
|
|
40
|
+
*
|
|
41
|
+
* @param options - Optional Redis connection options
|
|
42
|
+
*/
|
|
26
43
|
constructor(options) {
|
|
44
|
+
/** Publisher Redis client */
|
|
27
45
|
_EvRedisAdapter_pub.set(this, void 0);
|
|
46
|
+
/** Subscriber Redis client */
|
|
28
47
|
_EvRedisAdapter_sub.set(this, void 0);
|
|
48
|
+
/**
|
|
49
|
+
* Channel → listeners mapping.
|
|
50
|
+
* Each channel may have multiple local handlers.
|
|
51
|
+
*/
|
|
29
52
|
_EvRedisAdapter_listeners.set(this, void 0);
|
|
53
|
+
/** Unique identifier for this adapter instance */
|
|
30
54
|
_EvRedisAdapter_instanceId.set(this, void 0);
|
|
31
55
|
__classPrivateFieldSet(this, _EvRedisAdapter_pub, new Redis(options), "f");
|
|
32
56
|
__classPrivateFieldSet(this, _EvRedisAdapter_sub, new Redis(options), "f");
|
|
@@ -34,26 +58,45 @@ class EvRedisAdapter {
|
|
|
34
58
|
__classPrivateFieldSet(this, _EvRedisAdapter_instanceId, uid({ counter: Math.ceil(Math.random() * 100) }), "f");
|
|
35
59
|
__classPrivateFieldGet(this, _EvRedisAdapter_sub, "f").on('message', (channel, message) => {
|
|
36
60
|
const handlers = __classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").get(channel);
|
|
37
|
-
if (handlers)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
canHandle = (parsed === null || parsed === void 0 ? void 0 : parsed.id) !== __classPrivateFieldGet(this, _EvRedisAdapter_instanceId, "f");
|
|
43
|
-
}
|
|
44
|
-
catch (_a) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (canHandle)
|
|
48
|
-
handlers.forEach((handler) => handler(parsed === null || parsed === void 0 ? void 0 : parsed.message));
|
|
61
|
+
if (!handlers)
|
|
62
|
+
return;
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(message);
|
|
49
66
|
}
|
|
67
|
+
catch (_a) {
|
|
68
|
+
// Ignore malformed payloads
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Ignore messages published by this instance
|
|
72
|
+
if ((parsed === null || parsed === void 0 ? void 0 : parsed.id) === __classPrivateFieldGet(this, _EvRedisAdapter_instanceId, "f"))
|
|
73
|
+
return;
|
|
74
|
+
handlers.forEach((handler) => handler(parsed === null || parsed === void 0 ? void 0 : parsed.message));
|
|
50
75
|
});
|
|
51
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Publishes a message to a Redis channel.
|
|
79
|
+
*
|
|
80
|
+
* The payload is wrapped with the instance ID to
|
|
81
|
+
* prevent self-delivery.
|
|
82
|
+
*
|
|
83
|
+
* @param channel - Redis channel name
|
|
84
|
+
* @param message - Message payload
|
|
85
|
+
*/
|
|
52
86
|
publish(channel, message) {
|
|
53
87
|
return __awaiter(this, void 0, void 0, function* () {
|
|
54
|
-
yield __classPrivateFieldGet(this, _EvRedisAdapter_pub, "f").publish(channel, JSON.stringify({ id: __classPrivateFieldGet(this, _EvRedisAdapter_instanceId, "f"), message
|
|
88
|
+
yield __classPrivateFieldGet(this, _EvRedisAdapter_pub, "f").publish(channel, JSON.stringify({ id: __classPrivateFieldGet(this, _EvRedisAdapter_instanceId, "f"), message }));
|
|
55
89
|
});
|
|
56
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Subscribes to a Redis channel.
|
|
93
|
+
*
|
|
94
|
+
* Multiple listeners can be registered per channel.
|
|
95
|
+
* The Redis subscription is created only once per channel.
|
|
96
|
+
*
|
|
97
|
+
* @param channel - Redis channel name
|
|
98
|
+
* @param onMessage - Callback invoked on incoming messages
|
|
99
|
+
*/
|
|
57
100
|
subscribe(channel, onMessage) {
|
|
58
101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
59
102
|
if (!__classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").has(channel)) {
|
|
@@ -63,12 +106,20 @@ class EvRedisAdapter {
|
|
|
63
106
|
__classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").get(channel).add(onMessage);
|
|
64
107
|
});
|
|
65
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Unsubscribes from a Redis channel and removes all listeners.
|
|
111
|
+
*
|
|
112
|
+
* @param channel - Redis channel name
|
|
113
|
+
*/
|
|
66
114
|
unsubscribe(channel) {
|
|
67
115
|
return __awaiter(this, void 0, void 0, function* () {
|
|
68
116
|
yield __classPrivateFieldGet(this, _EvRedisAdapter_sub, "f").unsubscribe(channel);
|
|
69
117
|
__classPrivateFieldGet(this, _EvRedisAdapter_listeners, "f").delete(channel);
|
|
70
118
|
});
|
|
71
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Gracefully closes Redis connections.
|
|
122
|
+
*/
|
|
72
123
|
quit() {
|
|
73
124
|
__classPrivateFieldGet(this, _EvRedisAdapter_pub, "f").quit();
|
|
74
125
|
__classPrivateFieldGet(this, _EvRedisAdapter_sub, "f").quit();
|
|
@@ -2,20 +2,94 @@ import type { EvStreamManager } from '../manager.js';
|
|
|
2
2
|
import type { EvRedisAdapter } from '../adapters/redis.js';
|
|
3
3
|
import type { EvRedisPubSub } from '../adapters/pub-sub.js';
|
|
4
4
|
import { EvState } from '../state.js';
|
|
5
|
+
/**
|
|
6
|
+
* Options for creating an {@link EvStateManager}.
|
|
7
|
+
*/
|
|
5
8
|
interface EvStateManagerOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Stream manager responsible for managing client connections
|
|
11
|
+
* and broadcasting state updates.
|
|
12
|
+
*/
|
|
6
13
|
manager: EvStreamManager;
|
|
14
|
+
/**
|
|
15
|
+
* Optional distributed state adapter (e.g. Redis).
|
|
16
|
+
* Enables cross-process state propagation.
|
|
17
|
+
*/
|
|
7
18
|
adapter?: EvRedisAdapter;
|
|
19
|
+
/**
|
|
20
|
+
* Optional Pub/Sub instance used to synchronize
|
|
21
|
+
* state creation and removal across instances.
|
|
22
|
+
*/
|
|
8
23
|
pubsub?: EvRedisPubSub;
|
|
9
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Manages a collection of named {@link EvState} instances.
|
|
27
|
+
*
|
|
28
|
+
* Responsibilities:
|
|
29
|
+
* - Create and cache state objects locally
|
|
30
|
+
* - Synchronize state lifecycle (create/remove) across processes
|
|
31
|
+
* - Bridge EvState with stream manager and adapters
|
|
32
|
+
*
|
|
33
|
+
* Internally, all state keys are converted to strings to remain
|
|
34
|
+
* Redis-safe and transport-friendly.
|
|
35
|
+
*
|
|
36
|
+
* @typeParam S - Mapping of state keys to their value types
|
|
37
|
+
*/
|
|
10
38
|
export declare class EvStateManager<S extends Record<string, any>> {
|
|
11
39
|
#private;
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new state manager.
|
|
42
|
+
*
|
|
43
|
+
* @param options - Initialization options
|
|
44
|
+
*/
|
|
12
45
|
constructor({ manager, adapter, pubsub }: EvStateManagerOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Creates a state locally without emitting Pub/Sub events.
|
|
48
|
+
*
|
|
49
|
+
* @param channel - State channel name
|
|
50
|
+
* @param initialValue - Initial state value
|
|
51
|
+
*/
|
|
13
52
|
private createLocalState;
|
|
53
|
+
/**
|
|
54
|
+
* Removes a state locally without emitting Pub/Sub events.
|
|
55
|
+
*
|
|
56
|
+
* @param channel - State channel name
|
|
57
|
+
*/
|
|
14
58
|
private removeLocalState;
|
|
59
|
+
/**
|
|
60
|
+
* Creates or returns an existing state.
|
|
61
|
+
*
|
|
62
|
+
* If Pub/Sub is enabled, the creation is broadcast
|
|
63
|
+
* to other instances.
|
|
64
|
+
*
|
|
65
|
+
* @param key - State key
|
|
66
|
+
* @param initialValue - Initial state value
|
|
67
|
+
*/
|
|
15
68
|
createState<K extends keyof S>(key: K, initialValue: S[K]): EvState<S[K]>;
|
|
69
|
+
/**
|
|
70
|
+
* Retrieves an existing state.
|
|
71
|
+
*
|
|
72
|
+
* @param key - State key
|
|
73
|
+
*/
|
|
16
74
|
getState<K extends keyof S>(key: K): EvState<S[K]> | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Checks whether a state exists.
|
|
77
|
+
*
|
|
78
|
+
* @param key - State key
|
|
79
|
+
*/
|
|
17
80
|
hasState<K extends keyof S>(key: K): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Removes a state locally and propagates the removal
|
|
83
|
+
* to other instances via Pub/Sub.
|
|
84
|
+
*
|
|
85
|
+
* @param key - State key
|
|
86
|
+
*/
|
|
18
87
|
removeState<K extends keyof S>(key: K): void;
|
|
88
|
+
/**
|
|
89
|
+
* Handles incoming Pub/Sub lifecycle events.
|
|
90
|
+
*
|
|
91
|
+
* @param msg - Pub/Sub message payload
|
|
92
|
+
*/
|
|
19
93
|
private pubSubCallback;
|
|
20
94
|
}
|
|
21
95
|
export {};
|
|
@@ -11,12 +11,38 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
11
11
|
};
|
|
12
12
|
var _EvStateManager_states, _EvStateManager_manager, _EvStateManager_adapter, _EvStateManager_pubsub;
|
|
13
13
|
import { EvState } from '../state.js';
|
|
14
|
+
/**
|
|
15
|
+
* Manages a collection of named {@link EvState} instances.
|
|
16
|
+
*
|
|
17
|
+
* Responsibilities:
|
|
18
|
+
* - Create and cache state objects locally
|
|
19
|
+
* - Synchronize state lifecycle (create/remove) across processes
|
|
20
|
+
* - Bridge EvState with stream manager and adapters
|
|
21
|
+
*
|
|
22
|
+
* Internally, all state keys are converted to strings to remain
|
|
23
|
+
* Redis-safe and transport-friendly.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam S - Mapping of state keys to their value types
|
|
26
|
+
*/
|
|
14
27
|
export class EvStateManager {
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new state manager.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Initialization options
|
|
32
|
+
*/
|
|
15
33
|
constructor({ manager, adapter, pubsub }) {
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Internal state registry.
|
|
36
|
+
* Keyed by string channel name.
|
|
37
|
+
*/
|
|
38
|
+
_EvStateManager_states.set(this, new Map()
|
|
39
|
+
/** Stream manager used by all states */
|
|
40
|
+
);
|
|
41
|
+
/** Stream manager used by all states */
|
|
18
42
|
_EvStateManager_manager.set(this, void 0);
|
|
43
|
+
/** Optional distributed adapter */
|
|
19
44
|
_EvStateManager_adapter.set(this, void 0);
|
|
45
|
+
/** Optional Pub/Sub synchronizer */
|
|
20
46
|
_EvStateManager_pubsub.set(this, void 0);
|
|
21
47
|
__classPrivateFieldSet(this, _EvStateManager_manager, manager, "f");
|
|
22
48
|
__classPrivateFieldSet(this, _EvStateManager_adapter, adapter, "f");
|
|
@@ -26,6 +52,12 @@ export class EvStateManager {
|
|
|
26
52
|
__classPrivateFieldGet(this, _EvStateManager_pubsub, "f").onMessage(this.pubSubCallback);
|
|
27
53
|
}
|
|
28
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates a state locally without emitting Pub/Sub events.
|
|
57
|
+
*
|
|
58
|
+
* @param channel - State channel name
|
|
59
|
+
* @param initialValue - Initial state value
|
|
60
|
+
*/
|
|
29
61
|
createLocalState(channel, initialValue) {
|
|
30
62
|
const state = new EvState({
|
|
31
63
|
channel,
|
|
@@ -36,9 +68,23 @@ export class EvStateManager {
|
|
|
36
68
|
__classPrivateFieldGet(this, _EvStateManager_states, "f").set(channel, state);
|
|
37
69
|
return state;
|
|
38
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Removes a state locally without emitting Pub/Sub events.
|
|
73
|
+
*
|
|
74
|
+
* @param channel - State channel name
|
|
75
|
+
*/
|
|
39
76
|
removeLocalState(channel) {
|
|
40
77
|
__classPrivateFieldGet(this, _EvStateManager_states, "f").delete(channel);
|
|
41
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Creates or returns an existing state.
|
|
81
|
+
*
|
|
82
|
+
* If Pub/Sub is enabled, the creation is broadcast
|
|
83
|
+
* to other instances.
|
|
84
|
+
*
|
|
85
|
+
* @param key - State key
|
|
86
|
+
* @param initialValue - Initial state value
|
|
87
|
+
*/
|
|
42
88
|
createState(key, initialValue) {
|
|
43
89
|
var _a;
|
|
44
90
|
const channel = String(key);
|
|
@@ -53,12 +99,28 @@ export class EvStateManager {
|
|
|
53
99
|
});
|
|
54
100
|
return state;
|
|
55
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Retrieves an existing state.
|
|
104
|
+
*
|
|
105
|
+
* @param key - State key
|
|
106
|
+
*/
|
|
56
107
|
getState(key) {
|
|
57
108
|
return __classPrivateFieldGet(this, _EvStateManager_states, "f").get(String(key));
|
|
58
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Checks whether a state exists.
|
|
112
|
+
*
|
|
113
|
+
* @param key - State key
|
|
114
|
+
*/
|
|
59
115
|
hasState(key) {
|
|
60
116
|
return __classPrivateFieldGet(this, _EvStateManager_states, "f").has(String(key));
|
|
61
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Removes a state locally and propagates the removal
|
|
120
|
+
* to other instances via Pub/Sub.
|
|
121
|
+
*
|
|
122
|
+
* @param key - State key
|
|
123
|
+
*/
|
|
62
124
|
removeState(key) {
|
|
63
125
|
var _a;
|
|
64
126
|
const channel = String(key);
|
|
@@ -68,6 +130,11 @@ export class EvStateManager {
|
|
|
68
130
|
channel,
|
|
69
131
|
});
|
|
70
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Handles incoming Pub/Sub lifecycle events.
|
|
135
|
+
*
|
|
136
|
+
* @param msg - Pub/Sub message payload
|
|
137
|
+
*/
|
|
71
138
|
pubSubCallback(msg) {
|
|
72
139
|
if (!msg || typeof msg.channel !== 'string')
|
|
73
140
|
return;
|
package/dist/manager.d.ts
CHANGED
|
@@ -3,31 +3,41 @@ import type { EvManagerOptions, EvMessage, EvOnClose, EvOptions } from './types.
|
|
|
3
3
|
/**
|
|
4
4
|
* `EvStreamManager` manages multiple SSE connections.
|
|
5
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
6
|
*/
|
|
16
7
|
export declare class EvStreamManager {
|
|
17
8
|
#private;
|
|
18
9
|
constructor(opts?: EvManagerOptions);
|
|
19
10
|
/**
|
|
20
|
-
* Creates a new SSE stream
|
|
21
|
-
* Enforces max connection limit.
|
|
11
|
+
* Creates a new SSE stream
|
|
22
12
|
*/
|
|
23
13
|
createStream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions): {
|
|
14
|
+
id: string;
|
|
24
15
|
authenticate: any;
|
|
25
16
|
message: any;
|
|
26
17
|
close: (onClose?: EvOnClose) => void;
|
|
27
18
|
listen: (name: string) => void;
|
|
28
19
|
};
|
|
29
20
|
/**
|
|
30
|
-
* Sends a message to
|
|
21
|
+
* Sends a message directly to a local client by ID.
|
|
22
|
+
*
|
|
23
|
+
* This method only targets clients connected to the current process.
|
|
24
|
+
* It does not forward the message to Redis.
|
|
25
|
+
*/
|
|
26
|
+
private toLocal;
|
|
27
|
+
/**
|
|
28
|
+
* Sends a message to a specific client by ID.
|
|
29
|
+
*
|
|
30
|
+
* If the client exists locally, the message is delivered immediately.
|
|
31
|
+
* If not, the message is forwarded through Redis so another instance
|
|
32
|
+
* can deliver it to the target client.
|
|
33
|
+
*/
|
|
34
|
+
to(id: string, msg: EvMessage): void;
|
|
35
|
+
/**
|
|
36
|
+
* Send message locally to listeners
|
|
37
|
+
*/
|
|
38
|
+
private sendLocal;
|
|
39
|
+
/**
|
|
40
|
+
* Sends message to channel (local + Redis)
|
|
31
41
|
*/
|
|
32
42
|
send(name: string, msg: EvMessage): void;
|
|
33
43
|
}
|
package/dist/manager.js
CHANGED
|
@@ -9,22 +9,13 @@ 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 _EvStreamManager_instances, _EvStreamManager_clients, _EvStreamManager_listeners, _EvStreamManager_count, _EvStreamManager_maxConnections, _EvStreamManager_maxListeners, _EvStreamManager_id, _EvStreamManager_listen, _EvStreamManager_unlisten;
|
|
12
|
+
var _EvStreamManager_instances, _EvStreamManager_clients, _EvStreamManager_listeners, _EvStreamManager_count, _EvStreamManager_maxConnections, _EvStreamManager_maxListeners, _EvStreamManager_pubSub, _EvStreamManager_id, _EvStreamManager_listen, _EvStreamManager_unlisten, _EvStreamManager_onMessage;
|
|
13
13
|
import { Evstream } from './stream.js';
|
|
14
14
|
import { uid } from './utils.js';
|
|
15
15
|
import { EvMaxConnectionsError, EvMaxListenerError } from './errors.js';
|
|
16
16
|
/**
|
|
17
17
|
* `EvStreamManager` manages multiple SSE connections.
|
|
18
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
19
|
*/
|
|
29
20
|
export class EvStreamManager {
|
|
30
21
|
constructor(opts) {
|
|
@@ -34,6 +25,7 @@ export class EvStreamManager {
|
|
|
34
25
|
_EvStreamManager_count.set(this, void 0);
|
|
35
26
|
_EvStreamManager_maxConnections.set(this, void 0);
|
|
36
27
|
_EvStreamManager_maxListeners.set(this, void 0);
|
|
28
|
+
_EvStreamManager_pubSub.set(this, void 0);
|
|
37
29
|
_EvStreamManager_id.set(this, void 0);
|
|
38
30
|
__classPrivateFieldSet(this, _EvStreamManager_clients, new Map(), "f");
|
|
39
31
|
__classPrivateFieldSet(this, _EvStreamManager_listeners, new Map(), "f");
|
|
@@ -41,10 +33,13 @@ export class EvStreamManager {
|
|
|
41
33
|
__classPrivateFieldSet(this, _EvStreamManager_maxConnections, (opts === null || opts === void 0 ? void 0 : opts.maxConnection) || 5000, "f");
|
|
42
34
|
__classPrivateFieldSet(this, _EvStreamManager_maxListeners, (opts === null || opts === void 0 ? void 0 : opts.maxListeners) || 5000, "f");
|
|
43
35
|
__classPrivateFieldSet(this, _EvStreamManager_id, opts === null || opts === void 0 ? void 0 : opts.id, "f");
|
|
36
|
+
__classPrivateFieldSet(this, _EvStreamManager_pubSub, opts === null || opts === void 0 ? void 0 : opts.pubSub, "f");
|
|
37
|
+
if (__classPrivateFieldGet(this, _EvStreamManager_pubSub, "f")) {
|
|
38
|
+
__classPrivateFieldGet(this, _EvStreamManager_pubSub, "f").onMessage((msg) => __classPrivateFieldGet(this, _EvStreamManager_instances, "m", _EvStreamManager_onMessage).call(this, msg));
|
|
39
|
+
}
|
|
44
40
|
}
|
|
45
41
|
/**
|
|
46
|
-
* Creates a new SSE stream
|
|
47
|
-
* Enforces max connection limit.
|
|
42
|
+
* Creates a new SSE stream
|
|
48
43
|
*/
|
|
49
44
|
createStream(req, res, opts) {
|
|
50
45
|
if (__classPrivateFieldGet(this, _EvStreamManager_count, "f") >= __classPrivateFieldGet(this, _EvStreamManager_maxConnections, "f")) {
|
|
@@ -52,70 +47,99 @@ export class EvStreamManager {
|
|
|
52
47
|
}
|
|
53
48
|
const client = new Evstream(req, res, opts);
|
|
54
49
|
const id = uid({ counter: __classPrivateFieldGet(this, _EvStreamManager_count, "f"), prefix: __classPrivateFieldGet(this, _EvStreamManager_id, "f") });
|
|
55
|
-
const
|
|
50
|
+
const channels = [];
|
|
56
51
|
let isClosed = false;
|
|
57
52
|
__classPrivateFieldSet(this, _EvStreamManager_count, __classPrivateFieldGet(this, _EvStreamManager_count, "f") + 1, "f");
|
|
58
53
|
__classPrivateFieldGet(this, _EvStreamManager_clients, "f").set(id, client);
|
|
59
54
|
const close = (onClose) => {
|
|
60
55
|
if (isClosed)
|
|
61
56
|
return;
|
|
57
|
+
isClosed = true;
|
|
62
58
|
if (typeof onClose === 'function') {
|
|
63
|
-
onClose(
|
|
59
|
+
onClose(channels);
|
|
64
60
|
}
|
|
65
|
-
isClosed = true;
|
|
66
|
-
// Remove close event listener to prevent memory leaks
|
|
67
61
|
res.removeAllListeners('close');
|
|
68
|
-
// Clean up client
|
|
69
62
|
client.close();
|
|
70
|
-
// Decrement count
|
|
71
63
|
__classPrivateFieldSet(this, _EvStreamManager_count, __classPrivateFieldGet(this, _EvStreamManager_count, "f") - 1, "f");
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Clear channel array to release references
|
|
75
|
-
channel.length = 0;
|
|
76
|
-
// Remove client from map
|
|
64
|
+
channels.forEach((ch) => __classPrivateFieldGet(this, _EvStreamManager_instances, "m", _EvStreamManager_unlisten).call(this, ch, id));
|
|
65
|
+
channels.length = 0;
|
|
77
66
|
__classPrivateFieldGet(this, _EvStreamManager_clients, "f").delete(id);
|
|
78
|
-
|
|
79
|
-
if (!res.writableEnded) {
|
|
67
|
+
if (!res.writableEnded)
|
|
80
68
|
res.end();
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
const onCloseHandler = () => {
|
|
84
|
-
if (!isClosed) {
|
|
85
|
-
close();
|
|
86
|
-
}
|
|
87
69
|
};
|
|
88
|
-
res.on('close',
|
|
70
|
+
res.on('close', close);
|
|
89
71
|
return {
|
|
72
|
+
id: id,
|
|
90
73
|
authenticate: client.authenticate.bind(client),
|
|
91
74
|
message: client.message.bind(client),
|
|
92
|
-
close
|
|
75
|
+
close,
|
|
93
76
|
listen: (name) => {
|
|
94
77
|
if (isClosed)
|
|
95
78
|
return;
|
|
96
|
-
|
|
79
|
+
channels.push(name);
|
|
97
80
|
__classPrivateFieldGet(this, _EvStreamManager_instances, "m", _EvStreamManager_listen).call(this, name, id);
|
|
98
81
|
},
|
|
99
82
|
};
|
|
100
83
|
}
|
|
101
84
|
/**
|
|
102
|
-
* Sends a message to
|
|
85
|
+
* Sends a message directly to a local client by ID.
|
|
86
|
+
*
|
|
87
|
+
* This method only targets clients connected to the current process.
|
|
88
|
+
* It does not forward the message to Redis.
|
|
103
89
|
*/
|
|
104
|
-
|
|
90
|
+
toLocal(id, msg) {
|
|
91
|
+
const client = __classPrivateFieldGet(this, _EvStreamManager_clients, "f").get(id);
|
|
92
|
+
if (client) {
|
|
93
|
+
client.message({
|
|
94
|
+
data: msg,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return client;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Sends a message to a specific client by ID.
|
|
101
|
+
*
|
|
102
|
+
* If the client exists locally, the message is delivered immediately.
|
|
103
|
+
* If not, the message is forwarded through Redis so another instance
|
|
104
|
+
* can deliver it to the target client.
|
|
105
|
+
*/
|
|
106
|
+
to(id, msg) {
|
|
107
|
+
const client = this.toLocal(id, msg);
|
|
108
|
+
if (!client) {
|
|
109
|
+
if (__classPrivateFieldGet(this, _EvStreamManager_pubSub, "f")) {
|
|
110
|
+
__classPrivateFieldGet(this, _EvStreamManager_pubSub, "f").send({ type: 'to', data: { id: id, message: msg } });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Send message locally to listeners
|
|
116
|
+
*/
|
|
117
|
+
sendLocal(name, msg) {
|
|
105
118
|
const listeners = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name);
|
|
106
119
|
if (!listeners)
|
|
107
|
-
return;
|
|
108
|
-
for (const
|
|
120
|
+
return msg;
|
|
121
|
+
for (const id of listeners) {
|
|
109
122
|
const client = __classPrivateFieldGet(this, _EvStreamManager_clients, "f").get(id);
|
|
110
|
-
if (client) {
|
|
111
|
-
|
|
112
|
-
? { ch: name, data: msg }
|
|
113
|
-
: Object.assign({ ch: name }, msg.data) }));
|
|
123
|
+
if (!client) {
|
|
124
|
+
continue;
|
|
114
125
|
}
|
|
126
|
+
client.message(Object.assign(Object.assign({}, msg), { data: typeof msg.data === 'string'
|
|
127
|
+
? { ch: name, data: msg }
|
|
128
|
+
: Object.assign({ ch: name }, msg.data) }));
|
|
129
|
+
}
|
|
130
|
+
return msg;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Sends message to channel (local + Redis)
|
|
134
|
+
*/
|
|
135
|
+
send(name, msg) {
|
|
136
|
+
this.sendLocal(name, msg);
|
|
137
|
+
if (__classPrivateFieldGet(this, _EvStreamManager_pubSub, "f")) {
|
|
138
|
+
__classPrivateFieldGet(this, _EvStreamManager_pubSub, "f").send({ type: 'send', data: { name, message: msg } });
|
|
115
139
|
}
|
|
116
140
|
}
|
|
117
141
|
}
|
|
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) {
|
|
142
|
+
_EvStreamManager_clients = new WeakMap(), _EvStreamManager_listeners = new WeakMap(), _EvStreamManager_count = new WeakMap(), _EvStreamManager_maxConnections = new WeakMap(), _EvStreamManager_maxListeners = new WeakMap(), _EvStreamManager_pubSub = new WeakMap(), _EvStreamManager_id = new WeakMap(), _EvStreamManager_instances = new WeakSet(), _EvStreamManager_listen = function _EvStreamManager_listen(name, id) {
|
|
119
143
|
let listeners = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name);
|
|
120
144
|
if (!listeners) {
|
|
121
145
|
listeners = new Set();
|
|
@@ -126,11 +150,28 @@ _EvStreamManager_clients = new WeakMap(), _EvStreamManager_listeners = new WeakM
|
|
|
126
150
|
}
|
|
127
151
|
listeners.add(id);
|
|
128
152
|
}, _EvStreamManager_unlisten = function _EvStreamManager_unlisten(name, id) {
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
const listeners = __classPrivateFieldGet(this, _EvStreamManager_listeners, "f").get(name);
|
|
154
|
+
if (!listeners)
|
|
155
|
+
return;
|
|
156
|
+
listeners.delete(id);
|
|
157
|
+
if (listeners.size === 0) {
|
|
158
|
+
__classPrivateFieldGet(this, _EvStreamManager_listeners, "f").delete(name);
|
|
159
|
+
}
|
|
160
|
+
}, _EvStreamManager_onMessage = function _EvStreamManager_onMessage(msg) {
|
|
161
|
+
var _a, _b, _c, _d;
|
|
162
|
+
const type = msg === null || msg === void 0 ? void 0 : msg.type;
|
|
163
|
+
switch (type) {
|
|
164
|
+
case 'send':
|
|
165
|
+
const name = (_a = msg === null || msg === void 0 ? void 0 : msg.data) === null || _a === void 0 ? void 0 : _a.name;
|
|
166
|
+
const message = (_b = msg === null || msg === void 0 ? void 0 : msg.data) === null || _b === void 0 ? void 0 : _b.message;
|
|
167
|
+
if (!name || !message)
|
|
168
|
+
return;
|
|
169
|
+
this.sendLocal(name, message);
|
|
170
|
+
break;
|
|
171
|
+
case 'to':
|
|
172
|
+
const data = (_c = msg === null || msg === void 0 ? void 0 : msg.data) === null || _c === void 0 ? void 0 : _c.message;
|
|
173
|
+
const id = (_d = msg === null || msg === void 0 ? void 0 : msg.data) === null || _d === void 0 ? void 0 : _d.id;
|
|
174
|
+
this.toLocal(id, data);
|
|
175
|
+
break;
|
|
135
176
|
}
|
|
136
177
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EvRedisPubSub } from './adapters/pub-sub.js';
|
|
1
2
|
import type { EvStreamManager } from './manager.js';
|
|
2
3
|
export type EvEventsType = 'data' | 'error' | 'end';
|
|
3
4
|
export interface EvMessage {
|
|
@@ -18,6 +19,7 @@ export interface EvManagerOptions {
|
|
|
18
19
|
id?: string;
|
|
19
20
|
maxConnection?: number;
|
|
20
21
|
maxListeners?: number;
|
|
22
|
+
pubSub?: EvRedisPubSub;
|
|
21
23
|
}
|
|
22
24
|
export interface EvStateAdapter {
|
|
23
25
|
publish(channel: string, message: any): Promise<void>;
|
package/package.json
CHANGED
package/src/manager.ts
CHANGED
|
@@ -5,167 +5,220 @@ import { uid } from './utils.js'
|
|
|
5
5
|
import { EvMaxConnectionsError, EvMaxListenerError } from './errors.js'
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
EvManagerOptions,
|
|
9
|
+
EvMessage,
|
|
10
|
+
EvOnClose,
|
|
11
|
+
EvOptions,
|
|
12
12
|
} from './types.js'
|
|
13
|
+
import type { EvRedisPubSub } from './adapters/pub-sub.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* `EvStreamManager` manages multiple SSE connections.
|
|
16
17
|
* 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
18
|
*/
|
|
27
19
|
export class EvStreamManager {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
20
|
+
#clients: Map<string, Evstream>
|
|
21
|
+
#listeners: Map<string, Set<string>>
|
|
22
|
+
#count: number
|
|
23
|
+
#maxConnections: number
|
|
24
|
+
#maxListeners: number
|
|
25
|
+
#pubSub?: EvRedisPubSub
|
|
26
|
+
#id?: string
|
|
27
|
+
|
|
28
|
+
constructor(opts?: EvManagerOptions) {
|
|
29
|
+
this.#clients = new Map()
|
|
30
|
+
this.#listeners = new Map()
|
|
31
|
+
this.#count = 0
|
|
32
|
+
|
|
33
|
+
this.#maxConnections = opts?.maxConnection || 5000
|
|
34
|
+
this.#maxListeners = opts?.maxListeners || 5000
|
|
35
|
+
this.#id = opts?.id
|
|
36
|
+
|
|
37
|
+
this.#pubSub = opts?.pubSub
|
|
38
|
+
|
|
39
|
+
if (this.#pubSub) {
|
|
40
|
+
this.#pubSub.onMessage((msg) => this.#onMessage(msg))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a new SSE stream
|
|
46
|
+
*/
|
|
47
|
+
createStream(req: IncomingMessage, res: ServerResponse, opts?: EvOptions) {
|
|
48
|
+
if (this.#count >= this.#maxConnections) {
|
|
49
|
+
throw new EvMaxConnectionsError(this.#maxConnections)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const client = new Evstream(req, res, opts)
|
|
53
|
+
const id = uid({ counter: this.#count, prefix: this.#id })
|
|
54
|
+
const channels: string[] = []
|
|
55
|
+
let isClosed = false
|
|
56
|
+
|
|
57
|
+
this.#count += 1
|
|
58
|
+
this.#clients.set(id, client)
|
|
59
|
+
|
|
60
|
+
const close = (onClose?: EvOnClose) => {
|
|
61
|
+
if (isClosed) return
|
|
62
|
+
isClosed = true
|
|
63
|
+
|
|
64
|
+
if (typeof onClose === 'function') {
|
|
65
|
+
onClose(channels)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
res.removeAllListeners('close')
|
|
69
|
+
client.close()
|
|
70
|
+
|
|
71
|
+
this.#count -= 1
|
|
72
|
+
channels.forEach((ch) => this.#unlisten(ch, id))
|
|
73
|
+
channels.length = 0
|
|
74
|
+
this.#clients.delete(id)
|
|
75
|
+
|
|
76
|
+
if (!res.writableEnded) res.end()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
res.on('close', close)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id: id,
|
|
83
|
+
authenticate: client.authenticate.bind(client),
|
|
84
|
+
message: client.message.bind(client),
|
|
85
|
+
close,
|
|
86
|
+
listen: (name: string) => {
|
|
87
|
+
if (isClosed) return
|
|
88
|
+
channels.push(name)
|
|
89
|
+
this.#listen(name, id)
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Sends a message directly to a local client by ID.
|
|
96
|
+
*
|
|
97
|
+
* This method only targets clients connected to the current process.
|
|
98
|
+
* It does not forward the message to Redis.
|
|
99
|
+
*/
|
|
100
|
+
private toLocal(id: string, msg: EvMessage) {
|
|
101
|
+
const client = this.#clients.get(id)
|
|
102
|
+
|
|
103
|
+
if (client) {
|
|
104
|
+
client.message({
|
|
105
|
+
data: msg,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return client
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sends a message to a specific client by ID.
|
|
114
|
+
*
|
|
115
|
+
* If the client exists locally, the message is delivered immediately.
|
|
116
|
+
* If not, the message is forwarded through Redis so another instance
|
|
117
|
+
* can deliver it to the target client.
|
|
118
|
+
*/
|
|
119
|
+
to(id: string, msg: EvMessage) {
|
|
120
|
+
const client = this.toLocal(id, msg)
|
|
121
|
+
|
|
122
|
+
if (!client) {
|
|
123
|
+
if (this.#pubSub) {
|
|
124
|
+
this.#pubSub.send({ type: 'to', data: { id: id, message: msg } })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Send message locally to listeners
|
|
131
|
+
*/
|
|
132
|
+
private sendLocal(name: string, msg: EvMessage) {
|
|
133
|
+
const listeners = this.#listeners.get(name)
|
|
134
|
+
|
|
135
|
+
if (!listeners) return msg
|
|
136
|
+
|
|
137
|
+
for (const id of listeners) {
|
|
138
|
+
const client = this.#clients.get(id)
|
|
139
|
+
|
|
140
|
+
if (!client) {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
client.message({
|
|
145
|
+
...msg,
|
|
146
|
+
data:
|
|
147
|
+
typeof msg.data === 'string'
|
|
148
|
+
? { ch: name, data: msg }
|
|
149
|
+
: { ch: name, ...msg.data },
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return msg
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sends message to channel (local + Redis)
|
|
158
|
+
*/
|
|
159
|
+
send(name: string, msg: EvMessage) {
|
|
160
|
+
this.sendLocal(name, msg)
|
|
161
|
+
|
|
162
|
+
if (this.#pubSub) {
|
|
163
|
+
this.#pubSub.send({ type: 'send', data: { name, message: msg } })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Subscribe client to channel
|
|
169
|
+
*/
|
|
170
|
+
#listen(name: string, id: string) {
|
|
171
|
+
let listeners = this.#listeners.get(name)
|
|
172
|
+
|
|
173
|
+
if (!listeners) {
|
|
174
|
+
listeners = new Set()
|
|
175
|
+
this.#listeners.set(name, listeners)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (listeners.size >= this.#maxListeners) {
|
|
179
|
+
throw new EvMaxListenerError(listeners.size, name)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
listeners.add(id)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Unsubscribe client from channel
|
|
187
|
+
*/
|
|
188
|
+
#unlisten(name: string, id: string) {
|
|
189
|
+
const listeners = this.#listeners.get(name)
|
|
190
|
+
|
|
191
|
+
if (!listeners) return
|
|
192
|
+
|
|
193
|
+
listeners.delete(id)
|
|
194
|
+
|
|
195
|
+
if (listeners.size === 0) {
|
|
196
|
+
this.#listeners.delete(name)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Redis → process entry
|
|
202
|
+
*/
|
|
203
|
+
#onMessage(msg: Record<string, any>) {
|
|
204
|
+
const type = msg?.type
|
|
205
|
+
|
|
206
|
+
switch (type) {
|
|
207
|
+
case 'send':
|
|
208
|
+
const name = msg?.data?.name
|
|
209
|
+
const message = msg?.data?.message
|
|
210
|
+
|
|
211
|
+
if (!name || !message) return
|
|
212
|
+
|
|
213
|
+
this.sendLocal(name, message)
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
case 'to':
|
|
217
|
+
const data = msg?.data?.message
|
|
218
|
+
const id = msg?.data?.id
|
|
219
|
+
|
|
220
|
+
this.toLocal(id, data)
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
171
224
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EvRedisPubSub } from './adapters/pub-sub.js'
|
|
1
2
|
import type { EvStreamManager } from './manager.js'
|
|
2
3
|
|
|
3
4
|
// Built-in event types.
|
|
@@ -5,52 +6,54 @@ export type EvEventsType = 'data' | 'error' | 'end'
|
|
|
5
6
|
|
|
6
7
|
// Represents a message sent to the client over SSE.
|
|
7
8
|
export interface EvMessage {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
// Optional event name.
|
|
10
|
+
event?: string | EvEventsType
|
|
11
|
+
// Data to send; can be a string or object.
|
|
12
|
+
data: string | object
|
|
13
|
+
// Optional ID of the event.
|
|
14
|
+
id?: string
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
// Options for token-based authentication from query parameters.
|
|
17
18
|
export interface EvAuthenticationOptions {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
method: 'query'
|
|
20
|
+
param: string
|
|
21
|
+
verify: (token: string) => Promise<EvMessage> | undefined | null | boolean
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
// Options for configuring a single SSE stream.
|
|
24
25
|
export interface EvOptions {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
authentication?: EvAuthenticationOptions
|
|
27
|
+
heartbeat?: number
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// Configuration options for EvStreamManager.
|
|
30
31
|
export interface EvManagerOptions {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
// Unique ID for the manager
|
|
33
|
+
id?: string
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Max Connection which a manager can handle. If this limit exceeds it throws `EvMaxConnectionsError`
|
|
36
|
+
maxConnection?: number
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// Max Listeners which a listener can broadcast a message to. If this limit exceeds it throw `EvMaxListenerError`
|
|
39
|
+
maxListeners?: number
|
|
40
|
+
|
|
41
|
+
pubSub?: EvRedisPubSub
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
// Options for initializing EvState.
|
|
42
45
|
export interface EvStateAdapter {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
publish(channel: string, message: any): Promise<void>
|
|
47
|
+
subscribe(channel: string, onMessage: (message: any) => void): Promise<void>
|
|
48
|
+
unsubscribe(channel: string): Promise<void>
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export interface EvStateOptions<T> {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
initialValue: T
|
|
53
|
+
channel: string
|
|
54
|
+
manager: EvStreamManager
|
|
55
|
+
key?: string
|
|
56
|
+
adapter?: EvStateAdapter
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export type EvOnClose = (channels: string[]) => Promise<void>
|