Livegollection-example-app - A simple web-chat app that demonstrates how the Golang livegollection library can be used for live data synchronization

Overview

livegollection-example-app

livegollection-example-app is a simple web-chat app that demonstrates how the Golang livegollection library can be used for live data synchronization between multiple web clients and the server. The app allows to exchange text messages inside a chat room, livegollection will take care of notifying each client when a new message has been sent and keep everything consistent with the collection of messages stored in a SQLite database by the server.

Step-by-step guide

The following guide will explain step-by-step how to create this web-app and how to use livegollection.

Project setup

Create the directory that will house project:

mkdir livegollection-example-app
cd livegollection-example-app

Initialize the Golang module

Use go mod init to initialize the Golang module for the app:

go mod init module-name

In my case module-name is github.com/m1gwings/livegollection-example-app.

Implement the Chat collection

In order to use the livegollection library we need to implement a collection that satisfies the livegollection.Collection interface. We'll define this collection inside the chat package.

Create a directory for the chat package:

mkdir chat
cd chat

As we said our backend will store the messages of the chat inside a SQLite database, so, first of all, add queries.go inside the chat package in order to define the needed SQL query templates:

cat queries.go
package chat

const createChatTable = `CREATE TABLE IF NOT EXISTS chat (
	id INTEGER PRIMARY KEY AUTOINCREMENT,
	sender VARCHAR,
	sent_time DATETIME,
	text TEXT
);`

const insertMessageIntoChat = `INSERT INTO chat(sender, sent_time, text)
	VALUES(?, ?, ?);`

const updateMessageInChat = `UPDATE chat
	SET text = ?
	WHERE id = ?;`

const deleteMessageFromChat = `DELETE from chat
	WHERE id = ?;`

const getAllMessagesFromChat = `SELECT * FROM chat;`

const getMessageFromChat = `SELECT * FROM chat WHERE id = ?;`

To execute the queries above we need a SQLite driver. Let's download it and add to our dependencies:

go get github.com/mattn/go-sqlite3

Now it's time to implement the actual collection inside chat.go:

cat chat.go
package chat

import (
	"database/sql"
	"fmt"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

type Message struct {
	Id       int64     `json:"id,omitempty"`
	Sender   string    `json:"sender,omitempty"`
	SentTime time.Time `json:"sentTime,omitempty"`
	Text     string    `json:"text,omitempty"`
}

func (mess *Message) ID() int64 {
	return mess.Id
}

type Chat struct {
	db *sql.DB
}

const dbFilePath = "./chat.db"

func NewChat() (*Chat, error) {
	db, err := sql.Open("sqlite3", dbFilePath)
	if err != nil {
		return nil, fmt.Errorf("error while opening the database: %v", err)
	}

	_, err = db.Exec(createChatTable)
	if err != nil {
		return nil, fmt.Errorf("error while creating chat table: %v", err)
	}

	return &Chat{db: db}, nil
}

func (c *Chat) All() ([]*Message, error) {
	rows, err := c.db.Query(getAllMessagesFromChat)
	if err != nil {
		return nil, fmt.Errorf("error while executing query in All: %v", err)
	}

	messages := make([]*Message, 0)
	for rows.Next() {
		var id int64
		var sender string
		var sentTime time.Time
		var text string
		if err := rows.Scan(&id, &sender, &sentTime, &text); err != nil {
			return nil, fmt.Errorf("error while scanning a row in All: %v", err)
		}

		messages = append(messages, &Message{
			Id:       id,
			Sender:   sender,
			SentTime: sentTime,
			Text:     text,
		})
	}

	return messages, nil
}

func (c *Chat) Item(ID int64) (*Message, error) {
	row := c.db.QueryRow(getMessageFromChat, ID)
	var id int64
	var sender string
	var sentTime time.Time
	var text string
	if err := row.Scan(&id, &sender, &sentTime, &text); err != nil {
		return nil, fmt.Errorf("error while scanning the row in Item: %v", err)
	}

	return &Message{
		Id:       id,
		Sender:   sender,
		SentTime: sentTime,
		Text:     text,
	}, nil
}

func (c *Chat) Create(mess *Message) (*Message, error) {
	res, err := c.db.Exec(insertMessageIntoChat, mess.Sender, mess.SentTime, mess.Text)
	if err != nil {
		return nil, fmt.Errorf("error while inserting the message in Create: %v", err)
	}

	// It's IMPORTANT to set the ID of the message before returning it
	id, err := res.LastInsertId()
	if err != nil {
		return nil, fmt.Errorf("error while retreiving the last id in Create: %v", err)
	}

	mess.Id = id

	return mess, nil
}

func (c *Chat) Update(mess *Message) error {
	_, err := c.db.Exec(updateMessageInChat, mess.Text, mess.Id)
	if err != nil {
		return fmt.Errorf("error while updating the message in Update: %v", err)
	}

	return nil
}

func (c *Chat) Delete(ID int64) error {
	_, err := c.db.Exec(deleteMessageFromChat, ID)
	if err != nil {
		return fmt.Errorf("error while deleting the message in Delete: %v", err)
	}

	return nil
}

I'd like to emphasize (as mentioned in the comment) that it's IMPORTANT to set the message ID retreived from the database when we add a new message to the chat in Create.

Implement server executable

We need to write backend logic in order to serve static content (that will be added to the project in the next step) and register livegollection handler to the HTTP server.

In particular for the second point we need to download and add to our dependencies the livegollection library:

go get github.com/m1gwings/livegollection

Create a directory for the server executable:

cd .. # (If you are inside chat directory)
mkdir server
cd server

Write the code for the server inside main.go:

cat main.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/m1gwings/livegollection"
	"module-name/chat"
)

func main() {
	for r, fP := range map[string]string{
		"/":          "../static/index.html",
		"/bundle.js": "../static/bundle.js",
		"/style.css": "../static/style.css",
	} {
		// Prevents passing loop variables to a closure.
		route, filePath := r, fP
		http.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
			http.ServeFile(w, r, filePath)
		})
	}

	coll, err := chat.NewChat()
	if err != nil {
		log.Fatal(fmt.Errorf("error when creating new chat: %v", err))
	}

	// When we create the LiveGollection we need to specify the type parameters for items' id and items themselves.
	// (In this case int64 and *chat.Message)
	liveGoll := livegollection.NewLiveGollection[int64, *chat.Message](context.TODO(), coll, log.Default())

	// After we created liveGoll, to enable it we just need to register a route handled by liveGoll.Join.
	http.HandleFunc("/livegollection", liveGoll.Join)

	log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

As you can see, once you have implemented the collection, it's very easy to start using livegollection: you just need to create an istance of LiveGollection (with NewLiveGollection factory function) and register a route handled by liveGoll.Join.

Add static content

We need to add an HTML page and some CSS to display the chat to our users.

Create a directory for static content:

cd .. # (If you are inside server directory)
mkdir static
cd static

Add the following HTML page in index.html:

cat index.html
<!DOCTYPE html>
<html>
<head>
    <title>livegollection Chat</title>
    <script src="bundle.js"></script>
    <link rel="stylesheet" href="style.css" />
</head>
<body>
    <div class="chat">
        <p>livegollection Chat</p>
        <div class="inbox" id="inbox-div"></div>
        <div class="send-bar">
            <input type="text" id="message-text-input" value="" />
            <input type="button" id="send-button" value="Send" />
        </div>
        <div class="bottom-white-space"></div>
    </div>
</body>
</html>

and some styling in style.css:

cat style.css
* {
    color: #22223B;
}

html, body {
    width: 100%;
    height: 100%;
    margin: 0%;
}

body {
    background-color: #F2E9E4;
}

.chat {
    display: flex;
    flex-direction: column;
    height: 100%;
    width: 100%;
    align-items: center;
}

.inbox {
    display: flex;
    flex-direction: column;
    width: 500px;
    height: 100%;
    overflow: scroll;
}

.mine {
    align-self: flex-end;
}

.others {
    align-self: flex-start;
}

.message {
    width: fit-content;
    margin-bottom: 10px;
    border-radius: 5px;
    background-color:#C9ADA7;
    padding: 5px;
}

.sender {
    font-size: small;
    margin: 0px;
}

.time {
    font-size: xx-small;
    margin: 0px;
}

.send-bar {
    display: flex;
    flex-direction: row;
    height: 50px;
    width: 250px;
    justify-content: center;
    border-radius: 5px;
    background-color: #C9ADA7;
    padding: 5px;
}

.bottom-white-space {
    height: 50px;
}

input {
    border: 0px;
    margin-left: 10px;
    border-radius: 5px;
}

input[type=text] {
    background-color: transparent;
}

input[type=button] {
    background-color: #9A8C98;
}

Implement client-side logic

To make the chat actually working we need to implement the JavaScript script that will interact with the server and manage the DOM.

To interact with the server we'll use livegollection-client which is the JavaScript/TypeScript client library for livegollection.

livegollection-client is distributed on npm, furthermore we'll use webpack to bundle our script. So let's setup npm and add our dependencies:

cd .. # (If you are inside static directory)
npm init -y
npm install livegollection-client
npm install webpack webpack-cli --save-dev

Create the directory where we'll write our script:

mkdir src
cd src

and add the following JavaScript code inside index.js:

cat index.js
import LiveGollection from "../node_modules/livegollection-client/dist/index.js";

function generateClientTag() {
    return Math.random().toString(36).substr(2, 6);
}

const clientTag = generateClientTag();
const me = `Client#${clientTag}`;

function getMessageDivId(id) {
    return  `message-${id}`;
}

let inboxDiv = null;
let liveGoll = null;

function addMessageToInbox(message) {
    const sentByMe = me == message.sender;

    const messageDiv = document.createElement("div");
    messageDiv.id = getMessageDivId(message.id);
    messageDiv.className = sentByMe ? "mine" : "others";
    messageDiv.className += " message";

    if (!sentByMe) {
        const senderP = document.createElement("p");
        senderP.className = "sender";
        senderP.innerHTML = message.sender;
        messageDiv.appendChild(senderP);
    }

    const messageTextInput = document.createElement("input");
    messageTextInput.type = "text";
    messageTextInput.value = message.text;
    messageDiv.appendChild(messageTextInput);

    if (sentByMe) {
        const editButton = document.createElement("input");
        editButton.type = "button";
        editButton.value = "Edit";
        editButton.onclick = () => {
            message.text = messageTextInput.value;
            liveGoll.update(message);
        };
        messageDiv.appendChild(editButton);

        const deleteButton = document.createElement("input");
        deleteButton.type = "button";
        deleteButton.value = "Delete";
        deleteButton.onclick = () => {
            liveGoll.delete(message);
        };
        messageDiv.appendChild(deleteButton);
    }

    const sentTimeP = document.createElement("p");
    sentTimeP.className = "time";
    sentTimeP.innerHTML = new Date(message.sentTime).toLocaleTimeString();
    messageDiv.appendChild(sentTimeP);

    inboxDiv.appendChild(messageDiv);
}

window.onload = () => {
    liveGoll = new LiveGollection("ws://localhost:8080/livegollection");

    const messageTextInput = document.getElementById("message-text-input");
    const sendButton = document.getElementById("send-button");

    sendButton.onclick = () => {
        liveGoll.create({
            sender: me,
            sentTime: new Date(),
            text: messageTextInput.value,
        });
    };

    inboxDiv = document.getElementById("inbox-div");

    liveGoll.oncreate = (message) => {
        addMessageToInbox(message, inboxDiv);
    };

    liveGoll.onupdate = (message) => {
        const messageToUpdateTextInput = document.getElementById(getMessageDivId(message.id))
            .getElementsByTagName('input')[0];
        messageToUpdateTextInput.value = message.text;
    };

    liveGoll.ondelete = (message) => {
        const messageToDeleteDiv = document.getElementById(getMessageDivId(message.id));
        messageToDeleteDiv.remove();
    };
};

livegollection-client is as simple to use as livegollection: we need to create a LiveGollection object passing to the constructor the url to the livegollection server-side handler (in this case ws://localhost:8080/livegollection).

After the LiveGollection object has been created we need to set the event handlers oncreate, onupdate and ondelete to handle the correspondant events relative to messages. In this case to handle the events we need to update the DOM by adding a div for the new message or by modifying/deleting the existing one.

We can send events to the server with the following methods: create, update, delete. In this case we use create to send a new message and update/delete to edit/delete an existing one sent by us.

Run the app!

To run the app we need to bundle our script with webpack and start the server:

cd .. # (If you are inside src directory)
npx webpack --mode production --entry ./src/index.js --output-filename bundle.js -o ./static
cd server
go run main.go

Try the app!

To try the app you need to open two browser windows and connect them to http://localhost:8080 .

You can send some messages:

There are two different browser windows opened: each one is on http://localhost:8080 where the chat app is deployed. Two messages get sent: one from the left window which says "Hi!" and one from the right window which says "Hey!".

edit:

The message sent from the right window gets edited: the new text is "How are you?"

or delete a message that you have sent:

The message sent from the left window gets deleted

You might also like...
Get live cricket score right in your terminal.
Get live cricket score right in your terminal.

cric Get cricket score right in your terminal. How to use?! Make sure you have Node.js installed on your machine and just type the following command w

Simple todo app web application created with Go and MySQL.

TodoApp This is a project I've always wanted to do, it's simple, but to be says it involves to use different techniques in order to have a: authentica

A simple single-file executable to pull a git-ssh repository and serve the web app found to a self-contained browser window

go-git-serve A simple single-file executable to pull a git-ssh repository (using go-git library) and serve the web app found to a self-contained brows

Todo-cmd: an app you can add your tasks , edit or delete them

TODO CMD APP! 🧙‍♂️ Table of contents General info Update Requirements set-up usage General info todo-cmd is an app you can add your tasks , edit or d

There is a certain amount of work to be done before you can implement the features of your Go powered CLI app

go-project-template-cli There is a certain amount of work to be done before you can implement the features of your Go powered CLI app. A few of those

Brigodier is a command parser & dispatcher, designed and developed for command lines such as for Discord bots or Minecraft chat commands. It is a complete port from Mojang's "brigadier" into Go.

brigodier Brigodier is a command parser & dispatcher, designed and developed to provide a simple and flexible command framework. It can be used in man

Connect to a Twitch channel's chat from your terminal

CLI tool to connect to Twitch chat

basic terminal based chat application written in go
basic terminal based chat application written in go

this app uses websocket protocol to communicate in real time. both the client and the server are written in golang. it uses: gorilla/websocket package

Terminal chat with multiroom support over custom protocol.

Terminal Chat Content Content Overview Download Commands Protocol Room URL Platforms Examples Overview It is a multiroom terminal chat. It allows comm

Owner
Cristiano Migali
This is my GitHub trash bin🗑️
Cristiano Migali
Pi-hole data right from your terminal. Live updating view, query history extraction and more!

Pi-CLI Pi-CLI is a command line program used to view data from a Pi-Hole instance directly in your terminal.

Reece Mercer 41 Dec 12, 2022
linenoise-classic is a command-line tool that generates strings of random characters that can be used as reasonably secure passwords.

linenoise-classic is a command-line tool that generates strings of random characters that can be used as reasonably secure passwords.

Mark Cornick 0 Aug 21, 2022
This package can parse date match expression, which used by ElasticSearch

datemath-parser this package is pure go package, this package can parse date match expression, which used by ElasticSearch. Date Math Definition you c

zhuliquan 0 Jan 8, 2022
Chalk is a Go Package which can be used for making terminal output more vibrant with text colors, text styles and background colors.

Chalk Chalk is a Go Package which can be used for making terminal output more vibrant with text colors, text styles and background colors. Documentati

null 6 Oct 29, 2022
Bofin - A command line tool that can be used by to make Weblink development more productive

Bofin A command line tool that can be used by to make Weblink development more p

Gavin Bannerman 0 Jan 13, 2022
sample-go-test-app-vaibhav is a simple example of a production ready RPC service in Go

sample-go-test-app-vaibhav sample-go-test-app-vaibhav is a simple example of a production ready RPC service in Go. Instead of attempting to abstract a

Segment 0 Dec 2, 2021
CLI and web app to convert HTML markup to go-app.dev's syntax.

HTML to go-app Converter CLI and web app to convert HTML markup to go-app.dev's syntax. Installation CLI Static binaries are also available on GitHub

Felix Pojtinger 12 Dec 18, 2022
Terminal stock ticker with live updates and position tracking

Ticker Terminal stock watcher and stock position tracker Features Live stock price quotes Track value of your stock positions Support for multiple cos

Ani Channarasappa 4.4k Jan 8, 2023
Live streaming from your terminal

TStream - Streaming from terminal ??‍♂️ Come stream at tstream.club How to start streaming Please refer to this link Upcoming features One command to

Quang Ngoc 248 Jan 2, 2023
A twitch focused command line tool for producing, archiving and managing live stream content. Built for Linux.

twinx is a live-streaming command line tool for Linux. It connects streaming services (like Twitch, OBS and YouTube) together via a common title and description.

Kris Nóva 26 Oct 17, 2022