You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 14, 2024. It is now read-only.
Copy file name to clipboardExpand all lines: _posts/2024-05-23-scala-redis-websockets-part-2.md
+46-27Lines changed: 46 additions & 27 deletions
Original file line number
Diff line number
Diff line change
@@ -158,7 +158,8 @@ object message {
158
158
```
159
159
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.
160
160
161
-
## 4. Docker
161
+
## 4. Docker for Redis and PostgreSQL
162
+
162
163
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.
163
164
164
165
After installation, we can check if we have everything installed by running the following:
@@ -196,8 +197,7 @@ CREATE TABLE messages (
196
197
```
197
198
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`.
198
199
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:
201
201
202
202
```yaml
203
203
services:
@@ -254,7 +254,8 @@ Then connect to the database and finally list the tables by running the followin
254
254
$ \c websocket
255
255
$ \d
256
256
```
257
-
## 5. Skunk
257
+
## 5. Skunk for PostgreSQL Integration
258
+
258
259
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/).
259
260
260
261
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
294
295
```
295
296
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`.
296
297
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:
300
299
301
300
```scala
302
301
package rockthejvm.websockets
@@ -621,7 +620,8 @@ new PostgresProtocol[F] {
621
620
}.pure[F]
622
621
```
623
622
624
-
## 6. The Redis and Chat Protocols
623
+
## 6. Redis and Chat Protocols
624
+
625
625
Before we dive into the Redis implementation, we'll need an overview of how the schema will be.
626
626
A Redis hash is a record type structured as a collection of field-value pairs, we'll need the following hashes for our application:
627
627
@@ -717,7 +717,8 @@ object ChatProtocol {
717
717
}
718
718
```
719
719
720
-
### 6.1. The register() Function
720
+
### 6.1. Registering Users
721
+
721
722
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:
722
723
723
724
```scala
@@ -812,7 +813,8 @@ We start by checking if the user name exists in Redis, if this is true we return
812
813
813
814
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]`.
814
815
815
-
### 6.2. The enterroom() Function
816
+
### 6.2. Entering a Room
817
+
816
818
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:
817
819
818
820
```scala
@@ -906,7 +908,8 @@ We start by calling `redisP.getRoomFromName(room)`, if the room exists, we compa
906
908
If the user doesn't have a `RoomId`, we add the user to the requested room by calling `addToRoom(redisP, postgresP, user, r)`.
907
909
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.
908
910
909
-
### 6.3. The transferUserToRoom() Function
911
+
### 6.3. Moving to Another Room
912
+
910
913
This is a private function, that transfers the user to a new room:
911
914
912
915
```scala
@@ -928,7 +931,8 @@ object ChatProtocol {
928
931
```
929
932
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.
930
933
931
-
### 6.4. The removeFromCurrentRoom() Function
934
+
### 6.4. Removing a User from a Room
935
+
932
936
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.
933
937
934
938
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
1016
1020
1. We also delete the entry from `userroomid` by calling r`edisP.deleteUserRoomMapping(user.id)`
1017
1021
1. Finally we tell the old room member that the user has left the room.
1018
1022
1019
-
### 6.5. The broadcastMessage() Function
1023
+
### 6.5. Broadcasting a Message
1024
+
1020
1025
To broadcast messages to members in a room, we first need to retrieve a list of user ids from a `room:<roomid>` set:
1021
1026
```scala
1022
1027
new RedisProtocol[F] {
@@ -1105,7 +1110,8 @@ We start by getting a list of user ids by calling `redisP.listUserIds(roomid)`,
1105
1110
1106
1111
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.
1107
1112
1108
-
### 6.6. The addToRoom() Function
1113
+
### 6.6. Adding a User to a Room
1114
+
1109
1115
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:
1110
1116
1111
1117
```scala
@@ -1163,7 +1169,7 @@ Here we add the user to the `room:<roomid>` set, then add the pair to the `userr
1163
1169
1164
1170
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.
1165
1171
1166
-
### 6.7. The fetchRoomMessages() Function
1172
+
### 6.7. Fetching Current Messages
1167
1173
1168
1174
```scala
1169
1175
object ChatProtocol {
@@ -1184,7 +1190,8 @@ object ChatProtocol {
1184
1190
```
1185
1191
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]]`.
1186
1192
1187
-
### 6.8. The createRoom() Function
1193
+
### 6.8. Creating a Chat Room
1194
+
1188
1195
We already looked at the `createRoom()` redis implementation in the `register()` function section, now let's look at how to implement it in `ChatProtocol`:
1189
1196
1190
1197
```scala
@@ -1205,7 +1212,8 @@ object ChatProtocol {
1205
1212
```
1206
1213
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)`.
1207
1214
1208
-
### 6.9. The chat() Function
1215
+
### 6.9. Sending Messages
1216
+
1209
1217
When we receive a chat message, we'll need to save it into Postgres and then broadcast it:
1210
1218
1211
1219
```scala
@@ -1226,7 +1234,8 @@ new ChatProtocol[F] {
1226
1234
```
1227
1235
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]`.
1228
1236
1229
-
### 6.10. The help() Function
1237
+
### 6.10. The Help Prompt
1238
+
1230
1239
The implementation of this function remains unchanged from before:
1231
1240
1232
1241
```scala
@@ -1244,7 +1253,8 @@ new ChatProtocol[F] {
1244
1253
}.pure[F]
1245
1254
```
1246
1255
1247
-
### 6.11. The listRooms Function
1256
+
### 6.11. Listing Rooms
1257
+
1248
1258
Here we'll need to retrieve a list of rooms from Redis:
1249
1259
```scala
1250
1260
new RedisProtocol[F] {
@@ -1269,7 +1279,8 @@ new ChatProtocol[F] {
1269
1279
```
1270
1280
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.
1271
1281
1272
-
### 6.12. The listMembers() Function
1282
+
### 6.12. Listing Members
1283
+
1273
1284
This function gets the list of users in the room the user is in.
1274
1285
```scala
1275
1286
new ChatProtocol[F] {
@@ -1298,7 +1309,8 @@ Otherwise, we pass the roomid to `redisP.listUserIds(roomid)` to get a list of m
1298
1309
1299
1310
Finally, we produce a string of user names which we sequentially pass to the `SendToUser()` apply method
1300
1311
1301
-
### 6.13. The disconnect() Function
1312
+
### 6.13. Disconnecting
1313
+
1302
1314
We'll need to be able to remove a user from the `users` hash in Redis before we implement disconnect:
1303
1315
1304
1316
```scala
@@ -1329,7 +1341,8 @@ new ChatProtocol[F] {
1329
1341
```
1330
1342
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.
1331
1343
1332
-
### 6.14. The chatState() Function
1344
+
### 6.14. Getting State
1345
+
1333
1346
This is the last function to implement, however, to get an overview from Redis, quite a lot has to be done:
1334
1347
1335
1348
```scala
@@ -1392,7 +1405,8 @@ new ChatProtocol[F] {
1392
1405
```
1393
1406
Lastly in ChatProtocol, we simply call `redisP.chatState`
1394
1407
1395
-
## 7. InputMessage
1408
+
## 7. Getting Input
1409
+
1396
1410
Let's create an `InputMessage.scala` file in the websocket folder and add the following contents:
1397
1411
1398
1412
```scala
@@ -1555,7 +1569,8 @@ object InputMessage {
1555
1569
```
1556
1570
The `procesText4Reg()` function also remains unchanged except for the new `ChatProtocol[F]` methods
1557
1571
1558
-
## 8. Routes
1572
+
## 8. The Web App Routes
1573
+
1559
1574
In this section we'll continue to upgrade our application to the new `ChatProtocol[F]`:
The rest of the above 3 functions also remain unchanged.
1732
1747
1733
-
## 9. Server
1748
+
## 9. The Server
1749
+
1734
1750
The server function is also now uses `ChatProtocol[F]`:
1735
1751
1736
1752
```scala
@@ -1768,7 +1784,8 @@ object Server {
1768
1784
}
1769
1785
```
1770
1786
1771
-
## 10. Program
1787
+
## 10. The Main Program
1788
+
1772
1789
In this section, we have several updates that involve initializing Redis and Postgres:
1773
1790
1774
1791
```scala
@@ -1848,7 +1865,8 @@ object Program extends IOApp.Simple {
1848
1865
```
1849
1866
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.
1850
1867
1851
-
## 11. chat.html
1868
+
## 11. Serving HTML
1869
+
1852
1870
Since we upgraded the `User` case class, we'll also need to make changes to `chat.html`:
1853
1871
1854
1872
```javascript
@@ -1865,6 +1883,7 @@ In the `obj.ChatMsg` branch we now add `obj.ChatMsg.from.name.name` to access th
1865
1883
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.
1866
1884
1867
1885
## 12. Conclusion
1886
+
1868
1887
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.
1869
1888
1870
1889
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