@@ -8,9 +8,42 @@ const debug = debugModule("socket.io-redis");
8
8
const RETURN_BUFFERS = true ;
9
9
10
10
export interface ShardedRedisAdapterOptions {
11
+ /**
12
+ * The prefix for the Redis Pub/Sub channels.
13
+ *
14
+ * @default "socket.io"
15
+ */
11
16
channelPrefix ?: string ;
17
+ /**
18
+ * The subscription mode impacts the number of Redis Pub/Sub channels:
19
+ *
20
+ * - "static": 2 channels per namespace
21
+ *
22
+ * Useful when used with dynamic namespaces.
23
+ *
24
+ * - "dynamic": (2 + 1 per public room) channels per namespace
25
+ *
26
+ * The default value, useful when some rooms have a low number of clients (so only a few Socket.IO servers are notified).
27
+ *
28
+ * Only public rooms (i.e. not related to a particular Socket ID) are taken in account, because:
29
+ *
30
+ * - a lot of connected clients would mean a lot of subscription/unsubscription
31
+ * - the Socket ID attribute is ephemeral
32
+ *
33
+ * @default "dynamic"
34
+ */
35
+ subscriptionMode ?: "static" | "dynamic" ;
12
36
}
13
37
38
+ /**
39
+ * Create a new Adapter based on Redis sharded Pub/Sub introduced in Redis 7.0.
40
+ *
41
+ * @see https://redis.io/docs/manual/pubsub/#sharded-pubsub
42
+ *
43
+ * @param pubClient - the Redis client used to publish (from the `redis` package)
44
+ * @param subClient - the Redis client used to subscribe (from the `redis` package)
45
+ * @param opts - some additional options
46
+ */
14
47
export function createShardedAdapter (
15
48
pubClient : any ,
16
49
subClient : any ,
@@ -36,6 +69,7 @@ class ShardedRedisAdapter extends ClusterAdapter {
36
69
this . opts = Object . assign (
37
70
{
38
71
channelPrefix : "socket.io" ,
72
+ subscriptionMode : "dynamic" ,
39
73
} ,
40
74
opts
41
75
) ;
@@ -48,25 +82,69 @@ class ShardedRedisAdapter extends ClusterAdapter {
48
82
this . subClient . sSubscribe ( this . channel , handler , RETURN_BUFFERS ) ;
49
83
this . subClient . sSubscribe ( this . responseChannel , handler , RETURN_BUFFERS ) ;
50
84
51
- this . cleanup = ( ) => {
52
- return Promise . all ( [
53
- this . subClient . sUnsubscribe ( this . channel , handler ) ,
54
- this . subClient . sUnsubscribe ( this . responseChannel , handler ) ,
55
- ] ) ;
56
- } ;
85
+ if ( this . opts . subscriptionMode === "dynamic" ) {
86
+ this . on ( "create-room" , ( room ) => {
87
+ const isPublicRoom = ! this . sids . has ( room ) ;
88
+ if ( isPublicRoom ) {
89
+ this . subClient . sSubscribe (
90
+ this . dynamicChannel ( room ) ,
91
+ handler ,
92
+ RETURN_BUFFERS
93
+ ) ;
94
+ }
95
+ } ) ;
96
+
97
+ this . on ( "delete-room" , ( room ) => {
98
+ const isPublicRoom = ! this . sids . has ( room ) ;
99
+ if ( isPublicRoom ) {
100
+ this . subClient . sUnsubscribe ( this . dynamicChannel ( room ) ) ;
101
+ }
102
+ } ) ;
103
+ }
57
104
}
58
105
59
106
override close ( ) : Promise < void > | void {
60
- this . cleanup ( ) ;
107
+ const channels = [ this . channel , this . responseChannel ] ;
108
+
109
+ if ( this . opts . subscriptionMode === "dynamic" ) {
110
+ this . rooms . forEach ( ( _sids , room ) => {
111
+ const isPublicRoom = ! this . sids . has ( room ) ;
112
+ if ( isPublicRoom ) {
113
+ channels . push ( this . dynamicChannel ( room ) ) ;
114
+ }
115
+ } ) ;
116
+ }
117
+
118
+ return this . subClient . sUnsubscribe ( channels ) ;
61
119
}
62
120
63
121
override publishMessage ( message ) {
64
- debug ( "publishing message of type %s to %s" , message . type , this . channel ) ;
65
- this . pubClient . sPublish ( this . channel , this . encode ( message ) ) ;
122
+ const channel = this . computeChannel ( message ) ;
123
+ debug ( "publishing message of type %s to %s" , message . type , channel ) ;
124
+ this . pubClient . sPublish ( channel , this . encode ( message ) ) ;
66
125
67
126
return Promise . resolve ( "" ) ;
68
127
}
69
128
129
+ private computeChannel ( message ) {
130
+ // broadcast with ack can not use a dynamic channel, because the serverCount() method return the number of all
131
+ // servers, not only the ones where the given room exists
132
+ const useDynamicChannel =
133
+ this . opts . subscriptionMode === "dynamic" &&
134
+ message . type === MessageType . BROADCAST &&
135
+ message . data . requestId === undefined &&
136
+ message . data . opts . rooms . length === 1 ;
137
+ if ( useDynamicChannel ) {
138
+ return this . dynamicChannel ( message . data . opts . rooms [ 0 ] ) ;
139
+ } else {
140
+ return this . channel ;
141
+ }
142
+ }
143
+
144
+ private dynamicChannel ( room ) {
145
+ return this . channel + room + "#" ;
146
+ }
147
+
70
148
override publishResponse ( requesterUid , response ) {
71
149
debug ( "publishing response of type %s to %s" , response . type , requesterUid ) ;
72
150
@@ -104,10 +182,10 @@ class ShardedRedisAdapter extends ClusterAdapter {
104
182
return debug ( "invalid format: %s" , e . message ) ;
105
183
}
106
184
107
- if ( channel . toString ( ) === this . channel ) {
108
- this . onMessage ( message , "" ) ;
109
- } else {
185
+ if ( channel . toString ( ) === this . responseChannel ) {
110
186
this . onResponse ( message ) ;
187
+ } else {
188
+ this . onMessage ( message ) ;
111
189
}
112
190
}
113
191
0 commit comments