Skip to content
This repository was archived by the owner on Nov 14, 2024. It is now read-only.

Commit b658c65

Browse files
websockets headers
1 parent ac59f8f commit b658c65

File tree

1 file changed

+46
-27
lines changed

1 file changed

+46
-27
lines changed

_posts/2024-05-23-scala-redis-websockets-part-2.md

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ object message {
158158
```
159159
The `FetchMessage` case class contains a `MessageText` and a `User` from whom the `message` was sent. The reason we need another case class is that only want to fetch two columns from Postgres.
160160

161-
## 4. Docker
161+
## 4. Docker for Redis and PostgreSQL
162+
162163
We'll be using Docker images for Redis and Postgres. To follow along, you'll need [Docker](https://www.docker.com) and [Docker Compose](https://docs.docker.com/compose/) installed. We can install them by installing [Docker desktop](https://www.docker.com/products/docker-desktop/) on your system.
163164

164165
After installation, we can check if we have everything installed by running the following:
@@ -196,8 +197,7 @@ CREATE TABLE messages (
196197
```
197198
The first command creates a database called `websocket`, `\c websocket` connects to it, then we create a `users` and `rooms` table each with an `id` of type `UUID` and `name` of type `VARCHAR(255)`, and finally we create the messages table with an `id`, `message` and `time` columns and it also references the `users` and `rooms` table `id`.
198199

199-
### 4.1 Docker-Compose
200-
In this section, we'll manage our docker stack using docker-compose. Let's create a `docker-compose.yaml` file in the root folder of our application and add the following:
200+
Then we need manage our docker stack using docker-compose. Let's create a `docker-compose.yaml` file in the root folder of our application and add the following:
201201

202202
```yaml
203203
services:
@@ -254,7 +254,8 @@ Then connect to the database and finally list the tables by running the followin
254254
$ \c websocket
255255
$ \d
256256
```
257-
## 5. Skunk
257+
## 5. Skunk for PostgreSQL Integration
258+
258259
In this section, we'll implement the protocols necessary for interacting with Postgres in our application using [Skunk](https://blog.rockthejvm.com/skunk-complete-guide/).
259260

260261
First, we'll need to implement `Codec`s for the types in our domain. Create a `codecs.scala` file in the following path, `src/main/scala/rockthejvm/websockets/codecs/codecs.scala` and add the following code:
@@ -294,9 +295,7 @@ Skunk provides several important codecs through the `skunk.codec.all.*` import s
294295
```
295296
Here we provide the methods from case class to postgres and the reverse. Here we provide codecs for case classes that take native types, however, later we shall see how to join codecs for types such as `InsertMessage`.
296297

297-
### 5.1 The Postgres Protocols
298-
299-
In this section, we'll create various methods that will interact with the Postgres database through `Skunk`. Let's create a `PostgresProtocol.scala` file under the `websockets` and add the following code:
298+
Now we'll create various methods that will interact with the Postgres database through `Skunk`. Let's create a `PostgresProtocol.scala` file under the `websockets` and add the following code:
300299

301300
```scala
302301
package rockthejvm.websockets
@@ -621,7 +620,8 @@ new PostgresProtocol[F] {
621620
}.pure[F]
622621
```
623622

624-
## 6. The Redis and Chat Protocols
623+
## 6. Redis and Chat Protocols
624+
625625
Before we dive into the Redis implementation, we'll need an overview of how the schema will be.
626626
A Redis hash is a record type structured as a collection of field-value pairs, we'll need the following hashes for our application:
627627

@@ -717,7 +717,8 @@ object ChatProtocol {
717717
}
718718
```
719719

720-
### 6.1. The register() Function
720+
### 6.1. Registering Users
721+
721722
Let's start with the `register()` function in `ChatProtocol`. To register someone, we'll need to first check if the user name exists in Redis, then add it to both the Redis and Postgres databases:
722723

723724
```scala
@@ -812,7 +813,8 @@ We start by checking if the user name exists in Redis, if this is true we return
812813

813814
If the creation was successful, we also create the user in Redis by calling `redisP.createUser(u)` which returns `SuccessfulRegistration(u).pure[F]`. In case of an error we return, `ParsingError(None, err).pure[F]`.
814815

815-
### 6.2. The enterroom() Function
816+
### 6.2. Entering a Room
817+
816818
To enter a room, we'll need to first get the user's current room, and then make the transfer to the new room. We'll receive the room name inform of a `String`, which we'll use to check against the Redis database:
817819

818820
```scala
@@ -906,7 +908,8 @@ We start by calling `redisP.getRoomFromName(room)`, if the room exists, we compa
906908
If the user doesn't have a `RoomId`, we add the user to the requested room by calling `addToRoom(redisP, postgresP, user, r)`.
907909
Now, if the room name doesn't exist, we create that room by calling `createRoom(redisP, postgresP, room)`, then transfer the user to the new room.
908910

909-
### 6.3. The transferUserToRoom() Function
911+
### 6.3. Moving to Another Room
912+
910913
This is a private function, that transfers the user to a new room:
911914

912915
```scala
@@ -928,7 +931,8 @@ object ChatProtocol {
928931
```
929932
To do this we remove the user from the current room by calling `removeFromCurrentRoom()`, then add the user to the requested room by calling `addToRoom(redisP, postgresP, user, room)`. This happens both in Redis and Postgres to keep everything in sync.
930933

931-
### 6.4. The removeFromCurrentRoom() Function
934+
### 6.4. Removing a User from a Room
935+
932936
To remove a user from the current room, we'll need to remove the user from the Redis `room:<roomid>` set and remove the entry from the `userroomid` hash.
933937

934938
Note that in Redis when the last member is deleted from a set, the entire set is deleted, therefore if this occurs, we'll also need to update the `rooms` hash:
@@ -1016,7 +1020,8 @@ Here we start by getting the user's current roomid by calling `redisP.getUsersRo
10161020
1. We also delete the entry from `userroomid` by calling r`edisP.deleteUserRoomMapping(user.id)`
10171021
1. Finally we tell the old room member that the user has left the room.
10181022

1019-
### 6.5. The broadcastMessage() Function
1023+
### 6.5. Broadcasting a Message
1024+
10201025
To broadcast messages to members in a room, we first need to retrieve a list of user ids from a `room:<roomid>` set:
10211026
```scala
10221027
new RedisProtocol[F] {
@@ -1105,7 +1110,8 @@ We start by getting a list of user ids by calling `redisP.listUserIds(roomid)`,
11051110

11061111
Otherwise, we retrieve the list of `User`s by calling `redisP.getSelectedUsers(userlist.head, userlist.tail)`. The rest of the implementation hasn't changed from before.
11071112

1108-
### 6.6. The addToRoom() Function
1113+
### 6.6. Adding a User to a Room
1114+
11091115
This function adds a user to a room, however, in Redis we'll need to add the user id to the `room:<roomid>` set, and add the `userid -> roomid` pair to the `userroomid` hash:
11101116

11111117
```scala
@@ -1163,7 +1169,7 @@ Here we add the user to the `room:<roomid>` set, then add the pair to the `userr
11631169

11641170
We then inform all the room members that the new user has joined the room and pass all the previous messages to the new user.
11651171

1166-
### 6.7. The fetchRoomMessages() Function
1172+
### 6.7. Fetching Current Messages
11671173

11681174
```scala
11691175
object ChatProtocol {
@@ -1184,7 +1190,8 @@ object ChatProtocol {
11841190
```
11851191
To fetch messages from Postgres, we call `postgresP.fetchMessages(roomid)`, this returns an `F[Stream[F, FetchMessage]]` which we map on to convert to `ChatMsg`, we then compile to list to return an `F[List[OutputMessage]]`.
11861192

1187-
### 6.8. The createRoom() Function
1193+
### 6.8. Creating a Chat Room
1194+
11881195
We already looked at the `createRoom()` redis implementation in the `register()` function section, now let's look at how to implement it in `ChatProtocol`:
11891196

11901197
```scala
@@ -1205,7 +1212,8 @@ object ChatProtocol {
12051212
```
12061213
Here we start by calling `postgresP.createRoom(room)` which returns an `F[Either[String, Room]]`, if the creation is successful, we call `redisP.createRoom(r)` then return a `Right(r)` otherwise we return a `Left(err)`.
12071214

1208-
### 6.9. The chat() Function
1215+
### 6.9. Sending Messages
1216+
12091217
When we receive a chat message, we'll need to save it into Postgres and then broadcast it:
12101218

12111219
```scala
@@ -1226,7 +1234,8 @@ new ChatProtocol[F] {
12261234
```
12271235
We start by getting the user's roomid, then calling `postgresP.saveMessage()` followed by the `broadcastMessage()` function. In case the user has no room id, we inform the user by returning `List(SendToUser(user, "You are not currently in a room")).pure[F]`.
12281236

1229-
### 6.10. The help() Function
1237+
### 6.10. The Help Prompt
1238+
12301239
The implementation of this function remains unchanged from before:
12311240

12321241
```scala
@@ -1244,7 +1253,8 @@ new ChatProtocol[F] {
12441253
}.pure[F]
12451254
```
12461255

1247-
### 6.11. The listRooms Function
1256+
### 6.11. Listing Rooms
1257+
12481258
Here we'll need to retrieve a list of rooms from Redis:
12491259
```scala
12501260
new RedisProtocol[F] {
@@ -1269,7 +1279,8 @@ new ChatProtocol[F] {
12691279
```
12701280
The `listRooms` `ChatProtocol[F]` method starts by calling `redisP.listRooms` function, then we sort and make a String from the resulting list and finally pass this value to the `SendToUser()` apply method.
12711281

1272-
### 6.12. The listMembers() Function
1282+
### 6.12. Listing Members
1283+
12731284
This function gets the list of users in the room the user is in.
12741285
```scala
12751286
new ChatProtocol[F] {
@@ -1298,7 +1309,8 @@ Otherwise, we pass the roomid to `redisP.listUserIds(roomid)` to get a list of m
12981309

12991310
Finally, we produce a string of user names which we sequentially pass to the `SendToUser()` apply method
13001311

1301-
### 6.13. The disconnect() Function
1312+
### 6.13. Disconnecting
1313+
13021314
We'll need to be able to remove a user from the `users` hash in Redis before we implement disconnect:
13031315

13041316
```scala
@@ -1329,7 +1341,8 @@ new ChatProtocol[F] {
13291341
```
13301342
We first delete the user from Postgres and Redis by calling `postgresP.deleteUser(user.id)` and `redisP.deleteUser(user.id)` then we finally call `removeFromCurrentRoom()` to remove the user from the current room.
13311343

1332-
### 6.14. The chatState() Function
1344+
### 6.14. Getting State
1345+
13331346
This is the last function to implement, however, to get an overview from Redis, quite a lot has to be done:
13341347

13351348
```scala
@@ -1392,7 +1405,8 @@ new ChatProtocol[F] {
13921405
```
13931406
Lastly in ChatProtocol, we simply call `redisP.chatState`
13941407

1395-
## 7. InputMessage
1408+
## 7. Getting Input
1409+
13961410
Let's create an `InputMessage.scala` file in the websocket folder and add the following contents:
13971411

13981412
```scala
@@ -1555,7 +1569,8 @@ object InputMessage {
15551569
```
15561570
The `procesText4Reg()` function also remains unchanged except for the new `ChatProtocol[F]` methods
15571571

1558-
## 8. Routes
1572+
## 8. The Web App Routes
1573+
15591574
In this section we'll continue to upgrade our application to the new `ChatProtocol[F]`:
15601575

15611576
```scala
@@ -1730,7 +1745,8 @@ class Routes[F[_]: Files: Temporal] extends Http4sDsl[F] {
17301745
```
17311746
The rest of the above 3 functions also remain unchanged.
17321747

1733-
## 9. Server
1748+
## 9. The Server
1749+
17341750
The server function is also now uses `ChatProtocol[F]`:
17351751

17361752
```scala
@@ -1768,7 +1784,8 @@ object Server {
17681784
}
17691785
```
17701786

1771-
## 10. Program
1787+
## 10. The Main Program
1788+
17721789
In this section, we have several updates that involve initializing Redis and Postgres:
17731790

17741791
```scala
@@ -1848,7 +1865,8 @@ object Program extends IOApp.Simple {
18481865
```
18491866
Finally, in the `program` function, we took out `ChatState` and `Protocol` and added the `PostgresProtocol` and `RedisProtocol` which we provide as arguments to the `ChatProtocol` `make()` function.
18501867

1851-
## 11. chat.html
1868+
## 11. Serving HTML
1869+
18521870
Since we upgraded the `User` case class, we'll also need to make changes to `chat.html`:
18531871

18541872
```javascript
@@ -1865,6 +1883,7 @@ In the `obj.ChatMsg` branch we now add `obj.ChatMsg.from.name.name` to access th
18651883
Now to run our application, we first need to start our Redis and Postgres Docker containers using Docker-Compose, and finally our application server. The application should function closely to the original.
18661884

18671885
## 12. Conclusion
1886+
18681887
In conclusion, this article has gone in-depth on how to implement Redis and Postgres in a Scala application using the redis4cats and skunk libraries. Now we can persist our messages, and rip all the benefits of storing our information in Redis such as high availability and persistence.
18691888

18701889
In this version we simply dump all the previous messages to the new user but this should be done progressively whenever the user scrolls up, however, this was beyound the scope of this tutorial.

0 commit comments

Comments
 (0)