From 6d9121263b11edb3f9cbb993caf6d963725b2cf8 Mon Sep 17 00:00:00 2001
From: Marius Friess <34072851+mariusfriess@users.noreply.github.com>
Date: Fri, 4 Aug 2023 18:52:30 +0200
Subject: [PATCH] Handle closing rooms (#33)

* close open channel if room is deleted

* use events

* format code

* fix

* fix delete & add option to close channel

* remove obsolete code

* Refactor code

---------

Co-authored-by: Florian Raith <florianraith00@gmail.com>
---
 package-lock.json              | 20 ++++++++++++-
 package.json                   |  1 +
 src/app.module.ts              |  2 ++
 src/channel/channel.gateway.ts | 18 ++++++++++++
 src/channel/channel.service.ts | 51 ++++++++++++++++++++++++++--------
 src/room/room.service.ts       | 10 ++++++-
 6 files changed, 88 insertions(+), 14 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 2376428..746ab95 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
         "@nestjs/common": "^9.4.2",
         "@nestjs/config": "^2.3.2",
         "@nestjs/core": "^9.4.2",
+        "@nestjs/event-emitter": "^2.0.0",
         "@nestjs/jwt": "^10.0.3",
         "@nestjs/passport": "^9.0.3",
         "@nestjs/platform-express": "^9.4.2",
@@ -1855,6 +1856,19 @@
         }
       }
     },
+    "node_modules/@nestjs/event-emitter": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.0.tgz",
+      "integrity": "sha512-fZRv3+PmqXcbqCDRXRWhKDa+v3gmPUq4x5sQE5reVlDtEaCoAXwtGrtNswPtqd0msjyo8OWZF9k1sFjeRL6Xag==",
+      "dependencies": {
+        "eventemitter2": "6.4.9"
+      },
+      "peerDependencies": {
+        "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
+        "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
+        "reflect-metadata": "^0.1.12"
+      }
+    },
     "node_modules/@nestjs/jwt": {
       "version": "10.0.3",
       "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.3.tgz",
@@ -4715,6 +4729,11 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/eventemitter2": {
+      "version": "6.4.9",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
+      "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
+    },
     "node_modules/events": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -9964,7 +9983,6 @@
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
       "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
diff --git a/package.json b/package.json
index 4053cd4..c4f79b5 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
     "@nestjs/common": "^9.4.2",
     "@nestjs/config": "^2.3.2",
     "@nestjs/core": "^9.4.2",
+    "@nestjs/event-emitter": "^2.0.0",
     "@nestjs/jwt": "^10.0.3",
     "@nestjs/passport": "^9.0.3",
     "@nestjs/platform-express": "^9.4.2",
diff --git a/src/app.module.ts b/src/app.module.ts
index 76bd509..2cd2707 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -9,6 +9,7 @@ import { RoomModule } from './room/room.module';
 import { ExistsConstraint } from './common/constraints/exists';
 import { ChannelModule } from './channel/channel.module';
 import { BrowserModule } from './browser/browser.module';
+import { EventEmitterModule } from '@nestjs/event-emitter';
 
 /**
  * Main application module where other modules are imported and configured.
@@ -17,6 +18,7 @@ import { BrowserModule } from './browser/browser.module';
   imports: [
     MikroOrmModule.forRoot(),
     ConfigModule.forRoot(),
+    EventEmitterModule.forRoot(),
     AuthModule,
     UserModule,
     CategoryModule,
diff --git a/src/channel/channel.gateway.ts b/src/channel/channel.gateway.ts
index 3f24eda..6e1a2a2 100644
--- a/src/channel/channel.gateway.ts
+++ b/src/channel/channel.gateway.ts
@@ -302,6 +302,24 @@ export class ChannelGateway implements OnGatewayConnection {
     return true;
   }
 
+  /**
+   * Handle an 'close-channel' event to close a channel.
+   * @param client The connected socket client.
+   * @param payload The payload containing channelId.
+   * @returns A boolean indicating success.
+   */
+  @SubscribeMessage('close-channel')
+  @UseRequestContext()
+  public async closeChannel(
+    @ConnectedSocket() client: Socket,
+    @MessageBody() payload: { channelId: string },
+  ) {
+    const channel = await this.channels.fromId(payload.channelId);
+    await this.channels.close(channel);
+
+    return true;
+  }
+
   /**
    * Handle the connection event.
    *
diff --git a/src/channel/channel.service.ts b/src/channel/channel.service.ts
index ed212a4..387e029 100644
--- a/src/channel/channel.service.ts
+++ b/src/channel/channel.service.ts
@@ -6,6 +6,7 @@ import { Channel } from './channel';
 import { RoomService } from '../room/room.service';
 import { Room } from '../room/room.entity';
 import { BrowserService } from '../browser/browser.service';
+import { OnEvent } from '@nestjs/event-emitter';
 
 /**
  * Service for managing channels and real-time communication.
@@ -158,24 +159,50 @@ export class ChannelService {
       this.logger.debug(`Left ${channel} as student ${client.id}`);
     }
 
-    /*
-     * If the channel is empty after the client left, close it after 1 second.
-     * This is a temporary solution to prevent the channel from being
-     * closed when the teacher leaves and rejoins.
-     *
-     * TODO: implement a better solution
-     */
     if (channel?.isEmpty()) {
       channel.clearCloseTimeout();
-      channel.setCloseTimeout(async () => {
-        delete this.channels[channelId];
-        await this.rooms.updateWhiteboard(channel.room.id, channel.canvasJSON);
-        await this.browsers.closeBrowserContext(channelId);
-        this.logger.debug(`Closed ${channel}`);
+      channel.setCloseTimeout(() => {
+        this.close(channel);
       });
     }
   }
 
+  public async close(channel: Channel) {
+    await this.rooms.updateWhiteboard(channel.room.id, channel.canvasJSON);
+    await this.browsers.closeBrowserContext(channel.id);
+
+    await channel.close();
+    delete this.channels[channel.id];
+    this.logger.debug(`Closed ${channel}`);
+  }
+
+  /**
+   * Event listener for when a room is deleted. Closes the associated channel, if it exists.
+   *
+   * @param room - The room entity that was deleted.
+   */
+  @OnEvent('room.deleted')
+  public async onRoomDeleted(room: Room) {
+    const channel = this.getChannelFromRoom(room);
+
+    if (channel) {
+      await this.close(channel);
+    }
+  }
+
+  /**
+   * Gets the channel associated with the given id.
+   *
+   * @param id - The ID of the channel.
+   */
+  public fromId(id: string): Channel {
+    if (!this.exists(id)) {
+      throw new WsException('Channel not found');
+    }
+
+    return this.channels[id];
+  }
+
   /**
    * Gets a channel associated with the given socket client.
    *
diff --git a/src/room/room.service.ts b/src/room/room.service.ts
index d4169fb..1c5f4ae 100644
--- a/src/room/room.service.ts
+++ b/src/room/room.service.ts
@@ -5,6 +5,7 @@ import { Room } from './room.entity';
 import { EntityRepository } from '@mikro-orm/mysql';
 import { Category } from '../category/category.entity';
 import { Note } from '../note/note.entity';
+import { EventEmitter2 } from '@nestjs/event-emitter';
 
 /**
  * Service responsible for managing room entities.
@@ -15,6 +16,7 @@ export class RoomService {
     private readonly em: EntityManager,
     @InjectRepository(Room)
     private readonly repository: EntityRepository<Room>,
+    private eventEmitter: EventEmitter2,
   ) {}
 
   /**
@@ -83,7 +85,13 @@ export class RoomService {
   public async delete(id: number, category: Category): Promise<void> {
     const room = await this.get(id, category);
 
-    await this.em.removeAndFlush(room);
+    if (!room) {
+      throw new Error('Room not found');
+    }
+
+    this.eventEmitter.emit('room.deleted', room);
+
+    await this.repository.nativeDelete({ id });
   }
 
   /**
-- 
GitLab