Build a decentralized chat
In this tutorial, we are going to build a small chat app using Actyx. You might be tempted to skip it because you are not building chats in real-life — give it a chance. The techniques that you will learn in this tutorial are fundamental to building anything on the platform, and mastering them will give you a good understanding of its capabilities.
Before we start
The tutorial is divided into three sections:
- We will get set up for the tutorial
- We will quickly introduce Actyx
- We will get to it and build the chat
You don’t have to complete all of the sections at once to get value out of this tutorial. Try to get as far as you can — even if it’s just one or two sections.
Prerequisites
To get the most out of this tutorial, it is helpful if you have basic knowledge of HTML, CSS, and TypeScript. But since this is a fairly simple example you should be able to follow along even if you are coming from a different programming language.
If you haven't worked in TypeScript before, we can recommend this guide to learn the basics.
Set up for the tutorial
Now, let's get right to it. The only thing you need to do before starting the tutorial is installing Actyx. If you haven't done so yet, please follow this guide.
Set up the project
In order to run, test and build the chat app you are going to need Node.js and npm, which you can install from here.
We are now going to set up a simple web app project using Parcel. Somewhere on your computer create a directory called chat
.
In that directory create a file called package.json
and add the following content:
{
"name": "decentralized-chat",
"version": "1.0.0",
"description": "A decentralized chat",
"scripts": {
"build": "tsc && parcel build index.html --public-url .",
"start": "tsc && parcel index.html",
"test": "server-test start 1234 'cypress run'"
},
"dependencies": {
"@actyx/pond": "^3.4.0"
},
"devDependencies": {
"@types/node": "^14.0.27",
"@types/node-fetch": "^2.5.10",
"cypress": "^6.8.0",
"parcel": "^2.0.0-beta.2",
"process": "^0.11.10",
"start-server-and-test": "^1.12.1",
"typescript": "^3"
}
}
Create another file called tsconfig.json
with the following content:
{
"compilerOptions": {
"esModuleInterop": true,
"sourceMap": true,
"resolveJsonModule": true
},
}
Now create a file called index.html
and add the following:
<html>
<head></head>
<body>
<p>A chat is coming soon!</p>
</body>
<script src="./index.js"></script>
</html>
Finally, create a file named index.ts
with the following content:
console.log('Hello, world!');
To test that everything works, open a terminal, navigate to the chat
directory and run npm install
and then npm run start
. This is what you should see in your terminal.
If you now navigate to http://localhost:1234 in your browser and open the Developer Tools you should see this:
Help, I’m stuck!
If you get stuck, ping us on our Discord Chat or e-mail the mailing list at [email protected].
What is Actyx?
Actyx is a platform for building highly resilient software systems distributed across a swarm of networked devices. Specifically you can:
- Implement business logic as twins
- Include your twins in apps running on devices
- Forget about the network and data persistence
Actyx enables a completely decentralized architecture that allows you to build apps that always runs. Your apps always run because they run locally on the device and only interact with the locally running Actyx software.
Enough theory! Let's jump right in.
Building the chat
Let's now have a look at how Actyx makes it super simple to build a decentralized chat.
To build and run our chat we need to do three things:
Install Actyx on each deviceAlready done!- Implement our chat logic as a twin
- Integrate the twin into an app
Chat logic
Our chat logic is simple. Anyone can send messages and receives messages sent by all others. When someone joins the chat, they receive all past messages that were sent before they joined.
The way to implement this using Actyx is to write a so-called twin. A twin is a state-machine. It has a state which it updates when it receives information from elsewhere.
Let's start by defining types for the chat twin's state and the events it can receive. In this case, we have one type of event which is simply a message (string). The state of the twin is an array of strings. In the index.ts
file, add the following two lines of code:
type ChatEvent = string;
type ChatState = ChatEvent[];
When a twin first starts up, it won't have any messages in memory. So let's give it an empty array as the initial state:
const INITIAL_STATE: ChatState = [];
Now to the chat twin's logic, i.e. how to calculate the chat state, which we will show to the user, from the events we have received. We implement it as a so-called onEvent
function. This function simply adds any message it received as a ChatEvent
to our ChatState
:
function onEvent(state: ChatState, event: ChatEvent) {
state.push(event);
return state;
}
This is the complete chat logic. Let's now turn this into a fish.
The chat fish
We use these two terms interchangeably: local twins take digital twins from the cloud into edge computing while Actyx enables edge computing by offering a lake of data in which programs can live and breathe — like fishes.
A fish, aka twin, is defined as an object with a couple of properties. You must provide the fish with an ID, an initial state, the onEvent
function and information about where to get the chat messages from, a so-called event stream tags.
First, add the following imports to the top of the index.ts
file:
import { FishId, Pond, Fish, Tag } from '@actyx/pond';
Now that we have done that, we create the tag for our chat messages and then define the fish itself:
const chatTag = Tag<ChatEvent>('ChatMessage');
const ChatFish: Fish<ChatState, ChatEvent> = {
fishId: FishId.of('ax.example.chat', 'MyChatFish', 0),
initialState: INITIAL_STATE,
onEvent: onEvent,
where: chatTag,
};
That's it. You can now import this fish into any application and it will automatically be synchronized across your network.
The user interface
Lastly, we need to build a user interface and hook up our fish. Let's implement a very simple user interface showing the chat messages, an input field to type a message and a button to send the message.
Open up the index.html
file and adjust the contents of the head
and body
sections as follows:
<html>
<head>
<title>Chat App</title>
<style>
body {
padding: 20px;
}
pre {
height: 300px;
padding: 10px;
background-color: #d9d9d9;
overflow-y: auto;
}
button {
margin-top: 10px;
}
button,
input {
width: 100%;
height: 30px;
}
</style>
</head>
<body>
<pre id="messages"></pre>
<input id="message" type="text" />
<button id="send">send</button>
</body>
<script src="./index.js" type="module"></script>
</html>
The last thing we have to do is to hook up the user interface to the fish. We want to
- Show all chat messages, i.e. fish's state in the
pre
element - Send out a chat message event when the user clicks the Send button
In the index.ts
file, add the following code:
Pond.default({
appId: "com.example.chat",
displayName: "Chat App",
version: "2.0",
})
.then((pond) => {
// Select UI elements in the DOM
const messagesTextArea = document.getElementById('messages');
const messageInput = <HTMLInputElement>document.getElementById('message');
const sendButton = document.getElementById('send');
function clearInputAndSendToStream() {
// When click on send button get the text written in the input field
const message = messageInput.value;
messageInput.value = '';
// Send the message to a stream tagged with our chat tag
pond.emit(chatTag, message);
}
sendButton.addEventListener('click', clearInputAndSendToStream);
// Observe our chat fish. This means that our callback function will
// be called anytime the state of the fish changes
pond.observe(ChatFish, (state) => {
// Get the `pre` element and add all chat messages to that element
messagesTextArea.innerHTML = state.join('\n');
// Scroll the element to the bottom when it is updated
messagesTextArea.scrollTop = messagesTextArea.scrollHeight;
});
})
.catch(console.log);
To test that everything works navigate to the chat
directory, run npm run start
and open http://localhost:1234. You should now see the chat app.
At this point you are chatting by yourself. What's the point?
The power of local-first
What Actyx allows you to do now is run this chat app on as many computers as you want in your network. Actyx will automatically synchronize the chat twin across all those devices.
The power of this is that you do not have to worry about programming the network, nor about setting up some central database for storage. You only think: local-first.
And, your user can use the app whenever their device is on. Even when they are in airplane mode. Actyx will sync the twin when they get back into the network. This is local-first.
And just like that you have built a decentralized app that would traditionally have required a web server and a shared database or pub-sub broker 🎉
Next steps
Here is what you can try out now:
- Run the chat app on another computer in the network
- Restart the app: do you see the chat history?
- Airplane mode: what happens if you chat while offline?
- Reconnecting: what happens when you disable airplane mode again?
We hope that you are now starting to experience the power of local-first cooperation and the Actyx Platform. If you are keen to dive a bit deeper, check out the following further resources.
Download the code
You can download the complete code for this tutorial from GitHub here.
Further resources
- Visit our how-to guides to get started immediately
- Dive into how it works with the conceptual guides
Feel free to join our Actyx Developer Chat on Discord. We would love to hear about what you want to build on the Actyx Platform.