1- import { ClusterAdapterWithHeartbeat , MessageType } from "socket.io-adapter" ;
1+ import { ClusterAdapter , MessageType } from "socket.io-adapter" ;
22import type {
33 ClusterAdapterOptions ,
44 ClusterMessage ,
@@ -13,11 +13,16 @@ import {
1313 hasBinary ,
1414 GETDEL ,
1515 SET ,
16+ SUBSCRIBE ,
1617 XADD ,
1718 XRANGE ,
1819 XREAD ,
1920 hashCode ,
2021 duplicateClient ,
22+ SPUBLISH ,
23+ PUBLISH ,
24+ PUBSUB ,
25+ SSUBSCRIBE ,
2126} from "./util" ;
2227
2328const debug = debugModule ( "socket.io-redis-streams-adapter" ) ;
@@ -42,6 +47,17 @@ export interface RedisStreamsAdapterOptions {
4247 * @default 1
4348 */
4449 streamCount ?: number ;
50+ /**
51+ * The prefix of the Redis PUB/SUB channels used to communicate between the nodes.
52+ * @default "socket.io"
53+ */
54+ channelPrefix ?: string ;
55+ /**
56+ * Whether to use sharded PUB/SUB (added in Redis 7.0) to communicate between the nodes.
57+ * @default false
58+ * @see https://redis.io/docs/latest/develop/pubsub/#sharded-pubsub
59+ */
60+ useShardedPubSub ?: boolean ;
4561 /**
4662 * The maximum size of the stream. Almost exact trimming (~) is used.
4763 * @default 10_000
@@ -168,6 +184,8 @@ export function createAdapter(
168184 {
169185 streamName : "socket.io" ,
170186 streamCount : 1 ,
187+ channelPrefix : "socket.io" ,
188+ useShardedPubSub : false ,
171189 maxLen : 10_000 ,
172190 readCount : 100 ,
173191 blockTimeInMs : 5_000 ,
@@ -197,8 +215,19 @@ export function createAdapter(
197215 }
198216 } ) ;
199217
218+ const subClientPromise = duplicateClient ( redisClient ) ;
219+
220+ controller . signal . addEventListener ( "abort" , ( ) => {
221+ subClientPromise . then ( ( subClient ) => subClient . disconnect ( ) ) ;
222+ } ) ;
223+
200224 return function ( nsp ) {
201- const adapter = new RedisStreamsAdapter ( nsp , redisClient , options ) ;
225+ const adapter = new RedisStreamsAdapter (
226+ nsp ,
227+ redisClient ,
228+ subClientPromise ,
229+ options
230+ ) ;
202231 namespaceToAdapters . set ( nsp . name , adapter ) ;
203232
204233 const defaultClose = adapter . close ;
@@ -229,28 +258,70 @@ function computeStreamName(
229258 }
230259}
231260
232- class RedisStreamsAdapter extends ClusterAdapterWithHeartbeat {
261+ function isEphemeral ( message : ClusterMessage ) {
262+ const isBroadcastWithAck =
263+ message . type === MessageType . BROADCAST &&
264+ message . data . requestId !== undefined ;
265+
266+ return (
267+ isBroadcastWithAck ||
268+ [ MessageType . SERVER_SIDE_EMIT , MessageType . FETCH_SOCKETS ] . includes (
269+ message . type
270+ )
271+ ) ;
272+ }
273+
274+ class RedisStreamsAdapter extends ClusterAdapter {
233275 readonly #redisClient: any ;
234276 readonly #opts: Required < RedisStreamsAdapterOptions > ;
235277 readonly #streamName: string ;
278+ readonly #publicChannel: string ;
236279
237280 constructor (
238- nsp ,
239- redisClient ,
281+ nsp : any ,
282+ redisClient : any ,
283+ subClientPromise : Promise < any > ,
240284 opts : Required < RedisStreamsAdapterOptions > & ClusterAdapterOptions
241285 ) {
242- super ( nsp , opts ) ;
286+ super ( nsp ) ;
243287 this . #redisClient = redisClient ;
244288 this . #opts = opts ;
245289 // each namespace is routed to a specific stream to ensure the ordering of messages
246290 this . #streamName = computeStreamName ( nsp . name , opts ) ;
247291
248- this . init ( ) ;
292+ this . #publicChannel = `${ opts . channelPrefix } #${ nsp . name } #` ;
293+ const privateChannel = `${ opts . channelPrefix } #${ nsp . name } #${ this . uid } #` ;
294+
295+ subClientPromise . then ( ( subClient ) => {
296+ ( this . #opts. useShardedPubSub ? SSUBSCRIBE : SUBSCRIBE ) (
297+ subClient ,
298+ [ this . #publicChannel, privateChannel ] ,
299+ ( payload : Buffer ) => {
300+ try {
301+ const message = decode ( payload ) as ClusterMessage ;
302+ this . onMessage ( message ) ;
303+ } catch ( e ) {
304+ return debug ( "invalid format: %s" , e . message ) ;
305+ }
306+ }
307+ ) ;
308+ } ) ;
249309 }
250310
251311 override doPublish ( message : ClusterMessage ) {
252312 debug ( "publishing %o" , message ) ;
253313
314+ if ( isEphemeral ( message ) ) {
315+ // ephemeral messages are sent with Redis PUB/SUB
316+ const payload = Buffer . from ( encode ( message ) ) ;
317+ ( this . #opts. useShardedPubSub ? SPUBLISH : PUBLISH ) (
318+ this . #redisClient,
319+ this . #publicChannel,
320+ payload
321+ ) ;
322+ return Promise . resolve ( "" ) ;
323+ }
324+
254325 return XADD (
255326 this . #redisClient,
256327 this . #streamName,
@@ -263,8 +334,15 @@ class RedisStreamsAdapter extends ClusterAdapterWithHeartbeat {
263334 requesterUid : ServerId ,
264335 response : ClusterResponse
265336 ) : Promise < void > {
266- // @ts -ignore
267- return this . doPublish ( response ) ;
337+ const responseChannel = `${ this . #opts. channelPrefix } #${
338+ this . nsp . name
339+ } #${ requesterUid } #`;
340+ const payload = Buffer . from ( encode ( response ) ) ;
341+ return ( this . #opts. useShardedPubSub ? SPUBLISH : PUBLISH ) (
342+ this . #redisClient,
343+ responseChannel ,
344+ payload
345+ ) . then ( ) ;
268346 }
269347
270348 private encode ( message : ClusterMessage ) : RawClusterMessage {
@@ -335,6 +413,14 @@ class RedisStreamsAdapter extends ClusterAdapterWithHeartbeat {
335413 return message ;
336414 }
337415
416+ override serverCount ( ) : Promise < number > {
417+ return PUBSUB (
418+ this . #redisClient,
419+ this . #opts. useShardedPubSub ? "SHARDNUMSUB" : "NUMSUB" ,
420+ this . #publicChannel
421+ ) ;
422+ }
423+
338424 override persistSession ( session ) {
339425 debug ( "persisting session %o" , session ) ;
340426 const sessionKey = this . #opts. sessionKeyPrefix + session . pid ;
0 commit comments