IRC, part 3

I've made a good bit of progress, including figuring out things I noted in the last post.

RFC differences

Now that I'm looking at the IRC RFC that modern clients & servers are using, things are making more sense and what I'm recording on the IRC client I'm using to connect to Libera.Chat is matching up with RFC 2812.

I still don't get how sending the user's nick back to them is documented in the semi-BNF for messages in the RFC document, but at least it seems consistent enough for me to model my implementation after. One of the goals of this project was to see how far I could get just with the RFC, so this is an interesting experience, and I'm not discouraged by having to look at logs for existing client/server interaction.

Code improvements

In the last article, I noted how I was confused about having to use Tasks to wrap intra-GenServer communications. I didn't understand why I could make some cross-process calls without a Task but others would give an error along the lines of "process cannot call itself". The issue was pretty simple, and I'll break down the code flow for you here so you don't have to go searching through the code.

  1. The user (the person using a client) connects to the TCP server
  2. They type a message and send it through their client (an actual IRC client or telnet client, doesn't matter)
  3. The IRC server I'm writing gets that in the process for that user's TCP socket, which I call IRC.ClientConnection, at handle_info with a {:tcp, socket, message} shape
  4. That function does the parsing of the message and puts it in a tuple that's easier to work with down the line
  5. The client process then sends it to the single server process
  6. The server gets the command, builds the module name for the command, and calls it, passing in the command information along with the calling client's state and the server's state
  7. The individual module's function handles other processing and process communication

With this setup, I needed to wrap any process calls in the command modules with Task.start(...), otherwise I'd get the aforementioned error. It's important to note that steps 3, 5, and 7 use cross-process communication, while step 6 uses a direct function call. Since most of these steps are already using cross-process communication, I didn't need to do anything to enable that. Since step 6 is using a direct function call, it's not taking advantage of any of the cross-process communication features of the language and runtime.

To fix this, I simply wrapped that single call from the server to the command module with a Task.start(...) so that it would align with the rest of the code flow, and that worked. I was able to remove all Task process usage in the command modules and am able to use cross-process communication with any other process. Instead of running the command from the server process and wrapping messages back to the server with a Task, I wrap the command processing in a Task so that it is a process and can communicate with other processes like the rest of the application.