Creating a Chat Client using Web Sockets

This tutorial covers the following parts of Magic and Hyperlambda.

In addition to plain CRUD Magic also supports web sockets. If you are new to web sockets, then realize that a web socket is a bidirectional communication channel through which your web server can “push” data to clients. A typical use case for this is chat applications, but obviously this is useful in a lot of other scenarios too. In this tutorial, we will create a chat client in Angular and Hyperlambda, using the server side SignalR plugin, to let multiple users chat with each other, having the messages transmitted in real time to all clients.

Our Angular frontend

First we’ll need a frontend. Use the Angular CLI to create a new Angular project and name it “chat”. Open a terminal on your development machine and execute the following command in it.

ng new chat

You can choose whatever settings you wish as you generate your application, but the following are probably fine.

  1. No routing
  2. SCSS

Change into this folder in your terminal using cd chat, and use for instance VS Code to open the folder. If you have the VS Code command line binding, you can do this by typing code ./ into your terminal. Then we’ll need a terminal window in VS Code. You can open a new terminal window in the “Terminal” menu item of VS Code. Type the following command into your VS Code terminal window.

npm link

This ensures we link in all packaged our project depends upon. This might take some time. Afterwards we’ll need to add the client side SignalR package. Execute the following command in your terminal window.

npm install @aspnet/signalr

Now we can serve our project with the following command.

ng serve --port 4201

We’ll need to override the port because the Magic dashboard is probably already running on the default port which is 4200. You can now visit localhost:4201 to see your app. Use VS Code and open the “app.component.html” file in the folder “/src/app/” and replace its existing code with the following HTML.

<div>
  <h1>Chat client</h1>
  <textarea id="" cols="30" rows="10" [(ngModel)]="message"></textarea>
  <br />
  <button (click)="send()">Submit</button>
  <br />
  <textarea id="" cols="30" rows="10" [(ngModel)]="content"></textarea>
</div>

The UI isn’t going to win a design contest, but to keep the tutorial relevant to web sockets, we’ll ignore the UI for now. If you wish to create a better UI you might benefit from reading up on Angular Material. If you saved your above HTML file, you’ve now got a couple of compiler errors. This is because we are using fields on our AppComponent class that still doesn’t exist. Modify your “app.component.ts” file to resemble the following.

import {
  Component,
  OnDestroy,
  OnInit
} from '@angular/core';

import {
  HttpTransportType,
  HubConnection,
  HubConnectionBuilder
} from '@aspnet/signalr';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {

  content = '';
  message = '';
  hubConnection: HubConnection;

  ngOnInit() {

    let builder = new HubConnectionBuilder();
    this.hubConnection = builder.withUrl('http://localhost:5000/sockets', {
      skipNegotiation: true,
      transport: HttpTransportType.WebSockets,
    }).build();

    this.hubConnection.start();
    this.hubConnection.on('chat.new-message', (args) => {
      this.content += JSON.parse(args).message + '\r\n\r\n';
    });

  }
  
  ngOnDestroy() {
    this.hubConnection.stop();
  }

  send() {

    this.hubConnection.invoke(
      'execute',
      '/tutorials/add-chat',
      JSON.stringify({
        message: this.message,
      }));

    this.message = '';

  }
}

You might have to change the URL above to “http://localhost:4444/sockets” if you installed Magic using the docker images. The above code ensure we are initialising SignalR as the component is initialised, and that we are disconnecting SignalR as the component is destroyed - In addition to that we are transmitting new chat messages over our SignalR connection as the user clicks the “Submit” button. There are 3 methods in the above Angular TypeScript, and they do the following.

  1. ngOnInit - Initialising our SignalR socket and connects to our backend
  2. ngOnDestroy - Stops our socket connection
  3. send - Transmits the chat input textbox’ content to the backend file “/modules/tutorials/add-chat.socket.hl” file

Then we’ll need to import the FormsModule which you can do by modifying your “app.module.ts” file to contain the following code.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

And we are done with our frontend. Save all your files, and let’s move onwards to our backend.

Your Hyperlambda SignalR backend

If you click the “Submit” button, you will see that your console window gives you an error. This is because we have still not yet created our backend file responsible for executing as we submit chat messages to the server. Open up your Magic Dashboard with the localhost:4200 URL in a different browser window, log in with your root account, and open the “Hyper IDE” menu item. Then do exactly as follows.

  1. Click the “modules” folder
  2. Click the “New” button
  3. Type “tutorials” into the textbox
  4. Check the “Folder” checkbox to make sure we create a folder and not a file
  5. Click “Create”

Notice, if you’ve followed some of the previous hands on tutorials, you might already have this folder, at which point you can ignore the above, and move onwards to the next task. The above creates a new folder called “tutorials” inside your “modules” folder. Then do exactly as follows.

  1. Click the “tutorials” folder
  2. Click the “New” button
  3. Type “add-chat.socket.hl” into the textbox
  4. Click “Create”

This creates a new Hyperlambda file for you. This file will be resolved as the above send() Angular method is invoked over our SignalR web socket connection. Paste in the following Hyperlambda into your file.

.arguments
   message:string

unwrap:x:+/*/*
sockets.signal:chat.new-message
   args
      message:x:@.arguments/*/message

Then save your file, and switch back to your chat client browser window, and try writing something into the textarea and click “Submit”. Your app should resemble the following screenshot now.

Chat client

Internals

As I started out with, web sockets are a bidirectional transport channel, implying we can both send and receive data over a socket. In the above send() method we are pushing data to the server. The above [sockets.signal] Hyperlambda slot transmits this message to all subscribers of our “chat.new-message” message. In our above Angular code we are subscribing to these messages with the following TypeScript.

this.hubConnection.on('chat.new-message', (args) => {
  this.content += JSON.parse(args).message + '\r\n\r\n';
});

This ensures that all clients having connected to our web socket backend registering interest in messages of the above type will be notified automatically every time this message is published by our backend. You can open multiple browser windows simultaneously and point them to localhost:4201, and write something into any chat, and see how the messages are instantly received in all browser windows.

Endpoint resolving

One crucial point that separates the Hyperlambda web sockets implementation from other SignalR implementations, is the dynamic endpoint resolver. What this implies is that the file called “add-chat.socket.hl” is dynamically executed when our following Angular code is executed.

this.hubConnection.invoke('execute', '/tutorials/add-chat', JSON.stringify({
   message: this.message,
}));

Notice how the second argument resembles the relative path of the file. What the endpoint resolver will do, is roughly to add “.sockets.hl” to the relative URL specified, load this file dynamically, add the input arguments, and execute its Hyperlambda. This gives us a way to dynamically execute Hyperlambda files to respond to incoming SignalR messages. In addition it gives us the same method to declare arguments and pass in arguments to our SignalR invocations as we would use for normal HTTP invocations.

The endpoint resolver works almost exactly the same way any HTTP Hyperlambda file resolves, except instead of ending with the HTTP verb, it ends with “.socket.hl”. Besides from that, it loads arguments and converts these the same way, it dynamically resolves the files the same way, etc. You don’t have access to the HTTP context, such as response, status code, etc - But besides from that, the socket parts of Magic is similar enough to the HTTP Hyperlambda endpoint resolver that you can most of the time interchange these by simply changing the extension of your Hyperlambda files. You can also mix and match socket Hyperlambda files and HTTP Hyperlambda files as you see fit, such as for instance publish a message using [socket.signal] from an HTTP backend Hyperlambda file, etc.