1
1
import { EventEmitter } from "events" ;
2
+ import { yeast } from "./contrib/yeast" ;
2
3
4
+ /**
5
+ * A public ID, sent by the server at the beginning of the Socket.IO session and which can be used for private messaging
6
+ */
3
7
export type SocketId = string ;
8
+ /**
9
+ * A private ID, sent by the server at the beginning of the Socket.IO session and used for connection state recovery
10
+ * upon reconnection
11
+ */
12
+ export type PrivateSessionId = string ;
13
+
4
14
// we could extend the Room type to "string | number", but that would be a breaking change
5
15
// related: https://github.com/socketio/socket.io-redis-adapter/issues/418
6
16
export type Room = string ;
@@ -20,6 +30,15 @@ export interface BroadcastOptions {
20
30
flags ?: BroadcastFlags ;
21
31
}
22
32
33
+ interface SessionToPersist {
34
+ sid : SocketId ;
35
+ pid : PrivateSessionId ;
36
+ rooms : Room [ ] ;
37
+ data : unknown ;
38
+ }
39
+
40
+ export type Session = SessionToPersist & { missedPackets : unknown [ ] [ ] } ;
41
+
23
42
export class Adapter extends EventEmitter {
24
43
public rooms : Map < Room , Set < SocketId > > = new Map ( ) ;
25
44
public sids : Map < SocketId , Set < Room > > = new Map ( ) ;
@@ -331,4 +350,132 @@ export class Adapter extends EventEmitter {
331
350
"this adapter does not support the serverSideEmit() functionality"
332
351
) ;
333
352
}
353
+
354
+ /**
355
+ * Save the client session in order to restore it upon reconnection.
356
+ */
357
+ public persistSession ( session : SessionToPersist ) { }
358
+
359
+ /**
360
+ * Restore the session and find the packets that were missed by the client.
361
+ * @param pid
362
+ * @param offset
363
+ */
364
+ public restoreSession (
365
+ pid : PrivateSessionId ,
366
+ offset : string
367
+ ) : Promise < Session > {
368
+ return null ;
369
+ }
370
+ }
371
+
372
+ interface PersistedPacket {
373
+ id : string ;
374
+ emittedAt : number ;
375
+ data : unknown [ ] ;
376
+ opts : BroadcastOptions ;
377
+ }
378
+
379
+ type SessionWithTimestamp = SessionToPersist & { disconnectedAt : number } ;
380
+
381
+ export class SessionAwareAdapter extends Adapter {
382
+ private readonly maxDisconnectionDuration : number ;
383
+
384
+ private sessions : Map < PrivateSessionId , SessionWithTimestamp > = new Map ( ) ;
385
+ private packets : PersistedPacket [ ] = [ ] ;
386
+
387
+ constructor ( readonly nsp : any ) {
388
+ super ( nsp ) ;
389
+ this . maxDisconnectionDuration =
390
+ nsp . server . opts . connectionStateRecovery . maxDisconnectionDuration ;
391
+
392
+ const timer = setInterval ( ( ) => {
393
+ const threshold = Date . now ( ) - this . maxDisconnectionDuration ;
394
+ this . sessions . forEach ( ( session , sessionId ) => {
395
+ const hasExpired = session . disconnectedAt < threshold ;
396
+ if ( hasExpired ) {
397
+ this . sessions . delete ( sessionId ) ;
398
+ }
399
+ } ) ;
400
+ for ( let i = this . packets . length - 1 ; i >= 0 ; i -- ) {
401
+ const hasExpired = this . packets [ i ] . emittedAt < threshold ;
402
+ if ( hasExpired ) {
403
+ this . packets . splice ( 0 , i + 1 ) ;
404
+ break ;
405
+ }
406
+ }
407
+ } , 60 * 1000 ) ;
408
+ // prevents the timer from keeping the process alive
409
+ timer . unref ( ) ;
410
+ }
411
+
412
+ override persistSession ( session : SessionToPersist ) {
413
+ ( session as SessionWithTimestamp ) . disconnectedAt = Date . now ( ) ;
414
+ this . sessions . set ( session . pid , session as SessionWithTimestamp ) ;
415
+ }
416
+
417
+ override restoreSession (
418
+ pid : PrivateSessionId ,
419
+ offset : string
420
+ ) : Promise < Session > {
421
+ const session = this . sessions . get ( pid ) ;
422
+ if ( ! session ) {
423
+ // the session may have expired
424
+ return null ;
425
+ }
426
+ const hasExpired =
427
+ session . disconnectedAt + this . maxDisconnectionDuration < Date . now ( ) ;
428
+ if ( hasExpired ) {
429
+ // the session has expired
430
+ this . sessions . delete ( pid ) ;
431
+ return null ;
432
+ }
433
+ const index = this . packets . findIndex ( ( packet ) => packet . id === offset ) ;
434
+ if ( index === - 1 ) {
435
+ // the offset may be too old
436
+ return null ;
437
+ }
438
+ const missedPackets = [ ] ;
439
+ for ( let i = index + 1 ; i < this . packets . length ; i ++ ) {
440
+ const packet = this . packets [ i ] ;
441
+ if ( shouldIncludePacket ( session . rooms , packet . opts ) ) {
442
+ missedPackets . push ( packet . data ) ;
443
+ }
444
+ }
445
+ return Promise . resolve ( {
446
+ ...session ,
447
+ missedPackets,
448
+ } ) ;
449
+ }
450
+
451
+ override broadcast ( packet : any , opts : BroadcastOptions ) {
452
+ const isEventPacket = packet . type === 2 ;
453
+ // packets with acknowledgement are not stored because the acknowledgement function cannot be serialized and
454
+ // restored on another server upon reconnection
455
+ const withoutAcknowledgement = packet . id === undefined ;
456
+ const notVolatile = opts . flags ?. volatile === undefined ;
457
+ if ( isEventPacket && withoutAcknowledgement && notVolatile ) {
458
+ const id = yeast ( ) ;
459
+ // the offset is stored at the end of the data array, so the client knows the ID of the last packet it has
460
+ // processed (and the format is backward-compatible)
461
+ packet . data . push ( id ) ;
462
+ this . packets . push ( {
463
+ id,
464
+ opts,
465
+ data : packet . data ,
466
+ emittedAt : Date . now ( ) ,
467
+ } ) ;
468
+ }
469
+ super . broadcast ( packet , opts ) ;
470
+ }
471
+ }
472
+
473
+ function shouldIncludePacket (
474
+ sessionRooms : Room [ ] ,
475
+ opts : BroadcastOptions
476
+ ) : boolean {
477
+ const included =
478
+ opts . rooms . size === 0 || sessionRooms . some ( ( room ) => opts . rooms . has ( room ) ) ;
479
+ const notExcluded = sessionRooms . every ( ( room ) => ! opts . except . has ( room ) ) ;
480
+ return included && notExcluded ;
334
481
}
0 commit comments