All Projects → ipfs-shipyard → workshop-todo-dapp

ipfs-shipyard / workshop-todo-dapp

Licence: other
A workshop into adding realtime collaboration in a typical To-do app

Programming Languages

javascript
184084 projects - #8 most used programming language
CSS
56736 projects
HTML
75241 projects

Projects that are alternatives of or similar to workshop-todo-dapp

example-orbitdb-todomvc
TodoMVC with OrbitDB
Stars: ✭ 17 (-41.38%)
Mutual labels:  example, p2p, todomvc
Particl Desktop
The GUI application for Particl Markeplace and PART coin wallet. A decentralized peer to peer marketplace –free, secure, private, untraceable.
Stars: ✭ 131 (+351.72%)
Mutual labels:  dapp, p2p
Livepeerjs
JavaScript tools and applications that interact with Livepeer's smart contracts and peer-to-peer network
Stars: ✭ 116 (+300%)
Mutual labels:  dapp, p2p
Gun
An open source cybersecurity protocol for syncing decentralized graph data.
Stars: ✭ 15,172 (+52217.24%)
Mutual labels:  dapp, p2p
Unstoppable Wallet Android
A secure and decentralized Bitcoin and other cryptocurrency wallet for Android phones. Supports Bitcoin, Ethereum, EOS, Binance Chain, Bitcoin Cash, DASH, ...
Stars: ✭ 165 (+468.97%)
Mutual labels:  dapp, p2p
Fibos Todomvc
Todo DApp for FIBOS blockchain by React and fibos.js
Stars: ✭ 12 (-58.62%)
Mutual labels:  dapp, todomvc
Unstoppable Wallet Ios
A secure and decentralized Bitcoin and other cryptocurrency wallet for iPhone. Supports Bitcoin, Ethereum, EOS, Binance Chain, Bitcoin Cash, DASH, ...
Stars: ✭ 180 (+520.69%)
Mutual labels:  dapp, p2p
discussify-browser-extension
Discussify's browser extension app
Stars: ✭ 36 (+24.14%)
Mutual labels:  dapp, p2p
vue-vuex-todomvc
Example TodoMVC Vue.js app with Vuex store and server backend via REST
Stars: ✭ 41 (+41.38%)
Mutual labels:  example, todomvc
Golang Gin Realworld Example App
Exemplary real world application built with Golang + Gin
Stars: ✭ 1,780 (+6037.93%)
Mutual labels:  example, todomvc
todo-graphql-example
Example Todo app on top of json-graphql-server
Stars: ✭ 20 (-31.03%)
Mutual labels:  example, todomvc
demo-ipfs-id-qr-codes
Generates a QR Code of your IPNS URL and Public Key
Stars: ✭ 21 (-27.59%)
Mutual labels:  example, dapp
Todomvc Ddd Cqrs Eventsourcing
Implementation of basic Todo app via tastejs/todomvc in C#/Typescript with eventsourcing, cqrs, and domain driven design
Stars: ✭ 134 (+362.07%)
Mutual labels:  example, todomvc
lens
The official network explorer for Wavelet.
Stars: ✭ 28 (-3.45%)
Mutual labels:  dapp, p2p
contracts
Contracts for FundRequest (platform, token, crowdsale)
Stars: ✭ 56 (+93.1%)
Mutual labels:  dapp
tasit-sdk
A JavaScript / TypeScript SDK for making native mobile Ethereum dapps using React Native
Stars: ✭ 93 (+220.69%)
Mutual labels:  dapp
SplashScreenExample
How to implement a Splash Screen in ReactNative
Stars: ✭ 14 (-51.72%)
Mutual labels:  example
DAOcreator
dApp for creating and interacting with DAOs
Stars: ✭ 34 (+17.24%)
Mutual labels:  dapp
line-fido2-server
FIDO2(WebAuthn) server officially certified by FIDO Alliance and Relying Party examples.
Stars: ✭ 350 (+1106.9%)
Mutual labels:  example
erws
Erlang Websockets example using Cowboy
Stars: ✭ 43 (+48.28%)
Mutual labels:  example

workshop-todo-dapp

This walk-through will guide you into the process of converting a local To-do application into a decentralized application that allows different users to manipulate the To-dos collaboratively and in realtime, while also offering offline support.

The project was bootstrapped with Create React App and is highly based on the TodoMVC project.

Walk-through

Follow the step-by-step walk-through below to complete the workshop.

At any time, you may check the final application in the with-peer-base branch in case you are running into issues.

  1. Installing
  2. Running
  3. Understanding the To-dos store and data-model
  4. Adapting the To-dos store to use peer-base
    1. Install peer-base
    2. Create the app
    3. Re-implement the load method
    4. Get rid of the localStorage
    5. Update add, remove, updateTitle and updateCompleted
  5. Testing if the application works locally
  6. Displaying the number of users
    1. Replicate the subscribe and publishStateChange but for the peers
    2. Keep track of peersCount in the UI
    3. Render peersCount in the UI
    4. Style peersCount in the UI
  7. Testing if the application works with other users
  8. Deploying the application on IPFS
    1. Install IPFS and run a local node
    2. Ensure links are relative
    3. Build and deploy
    4. Using a domain

1. Installing

Be sure to have Node.js in your machine. Install the project by running:

$ npm install

2. Running

Now that the project is installed, you may run the development server:

$ npm start

The application will open automatically in your browser once ready.

3. Understanding the To-dos store and data-model

The App component is our root react component. When mounted, it loads the initial To-dos from the todos-store and subscribes to subsequent updates. This ensures that any change to the To-dos state will trigger a UI update. While you can explore the App component and all its underlying logic, our goal is to change it as little as possible.

The todos-store exposes all the operations necessary to read and manipulate the To-dos. It also continuously persists the state to the localStorage so that the To-dos can be restored on subsequent visits. Moreover, it allows subscribers to receive the new state whenever it's updated. The state looks like this:

[
    { id: "<unique-id>", title: "Buy candies": completed: true },
    { id: "<unique-id>", title: "Walk the dog", completed: false }
]

As you imagine, this application only works locally within the browser. It doesn't allow different users to read and manipulate the To-dos. What architectural pieces would we traditionally need to support CRUD operations amongs several users?

  • A database to store the To-dos
  • A (Restful) API that offers a CRUD for the To-dos
  • A static web-server to serve the application assets
  • A realtime server, using socket.io or similar, to deliver updates to the users without using polling mechanisms

But even so, how do we deal with concurrent updates? What if we want to allow users to performs changes while offline and sync them when online? These are hard problems to solve unless we use the right technologies.

This is where peer-base comes in. It's goal is to provide the primitives for developers to build real-time and offline-first decentralized applications by using (delta) CRDTs and IPFS.

4. Adapting the To-dos store to use peer-base

4.1. Install peer-base

Install peer-base by running:

$ npm install peer-base

4.2. Create the app

Create a new app with the name todo-dapp:

// src/todos-store.js
import createApp from 'peer-base';

// ...

const app = createApp('todo-dapp');

app.on('error', (err) => console.error('error in app:', err));

We will just log error events in the console, but you could display them in the UI instead.

4.3. Re-implement the load method

We need to re-implement the load function which is responsible for loading the todos. In here, we must:

  • Start the app
  • Create a new collaboration for the To-dos
  • Subscribe to the state changed event of the collaboration to receive updates to the underlying To-dos
  • Update todos variable to the last known list of To-dos

In peer-base we may have as many collaborations as we want. Users collaborate on a CRDT type: either a built-in or a custom one. Because the To-dos data-model is an array of objects, we will use the rga (Replicable Growable Array) type.

This allows multiple users to perform concurrent CRUD operations without any conflicts. This works well in most cases but it doesn't allow concurrent updates of the title and complete fields of To-dos. That could be supported by using sub-collaborations but we will skip it for the sake of simplicity.

Update the load function like so:

// src/todos-store.js

// ...
let collaboration;

export default {
    async load() {
        await app.start();

        collaboration = await app.collaborate('todos-of-<github-username>', 'rga');

        collaboration.removeAllListeners('state changed');
        collaboration.on('state changed', () => {
            todos = collaboration.shared.value();
            publishStateChange(todos);
        });

        todos = collaboration.shared.value();

        return todos;
    },

    // ...
};

Be sure to change <github-username> so that the Collaboration ID is unique. This ensures that you start with an empty list of To-dos.

The collaboration.shared is a reference to the CRDT instance. We will use it in next steps to perform changes on the underlying state.

Note that we are calling removeAllListeners('state changed') so that load can be called multiple times during the lifecyle of the app. If we didn't do that, the subscribers would be called multiple times for the same event.

4.4. Get rid of the localStorage

Peer-star already persists the last known state of each collaboration. This means that we can safely remove any code that relates to storing or reading the To-dos from the localStorage.

You may remove all the lines below:

// src/todos-store.js
// ....

import throttle from 'lodash/throttle';

// ....

window.addEventListener('unload', () => saveTodos(todos));

const readTodos = () => JSON.parse(localStorage.getItem('dapp-todos') || '[]');
const saveTodos = () => todos && localStorage.setItem('dapp-todos', JSON.stringify(todos));
const saveTodosThrottled = throttle(saveTodos, 1000, { leading: false });

... and the publishStateChange now becomes simpler:

// src/todos-store.js

const publishStateChange = (todos) => subscribers.forEach((listener) => listener(todos));

Yeah, less code yields profit!

4.5. Update add, remove, updateTitle and updateCompleted

We now must update add, remove, updateTitle and updateCompleted functions to call the CRDT mutations instead of manipulating the todos manually. By doing so, we will trigger a state change event on ourselves and in other replicas (users) as well.

The rga CRDT type has the following methods:

  • push()
  • insertAt(index, value)
  • updateAt(index, value)
  • removeAt(index)

Let's use these functions to mutate the state:

// src/todos-store.js
// ...

export default {
    // ...
    add(title) {
        collaboration.shared.push({ id: uuidv4(), title, completed: false });
    },

    remove(id) {
        const index = todos.findIndex((todo) => todo.id === id);

        if (index === -1) {
            return;
        }

        collaboration.shared.removeAt(index);
    },

    updateTitle(id, title) {
        const index = todos.findIndex((todo) => todo.id === id);
        const todo = todos[index];

        if (!todo || todo.title === title) {
            return;
        }

        const updatedTodo = { ...todo, title };

        collaboration.shared.updateAt(index, updatedTodo);
    },

    updateCompleted(id, completed) {
        const index = todos.findIndex((todo) => todo.id === id);
        const todo = todos[index];

        if (!todo || todo.completed === completed) {
            return;
        }

        const updatedTodo = { ...todo, completed };

        collaboration.shared.updateAt(index, updatedTodo);
    },

    // ...
},

And that's all. Easy huh?

5. Testing if the application works locally

You may test the changes we made locally. The application should behave exactly the same as before but it's now partially decentralized! It's not totally decentralized because we are still serving it using a web server. But more on that later.

6. Displaying the number of users (peers)

The collaboration emits the membership changed event that we can listen to keep track of the peers collaborating. We will use it to display the number of peers in the UI.

6.1. Replicate the subscribe and publishStateChange but for the peers

Lets replicate the subscribe and publishStateChange logic but for the peers:

// src/todos-store.js
// ...
const peersSubscribers = new Set();

const publishPeersChange = (peers) => peersSubscribers.forEach((listener) => listener(peers));

export default {
    async load() {
        // ...

        collaboration.removeAllListeners('membership changed');
        collaboration.on('membership changed', publishPeersChange);
    },

    // ...

    subscribePeers(subscriber) {
        peersSubscribers.add(subscriber);

        return () => peersSubscribers.remove(subscriber);
    },
};

6.2. Keep track of peersCount in the UI

Add peersCount to the App component state and update it whenever it changes:

// src/App.js
// ...

class App extends Component {
    state = {
        // ...
        peersCount: 1,
    }

    async componentDidMount() {
        // ...

        todosStore.subscribePeers((peers) => this.setState({ peersCount: peers.size }));
    }

    // ...
}

6.3. Render peersCount in the UI

Let's render peersCount in the footer:

// src/App.js
// ...

class App extends Component {
    // ...

    render() {
        const { loading, error, todos, peersCount } = this.state;

        return (
            <div className="App">
                { /* ... */ }

                <footer className="App__footer">
                    <div className="App__peers-count">{ peersCount }</div>

                    { /* ... */ }
                </footer>
            </div>
        );
    }
);

6.4. Style peersCount in the UI

Finally, add the App__peers-count CSS class to the bottom of App.css:

/* App.css */
/* ... */

.App__peers-count {
    width: 50px;
    height: 50px;
    margin-bottom: 25px;
    padding: 5px;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    border: 1px solid #cc9a9a;
    background-color: rgba(175, 47, 47, 0.15);
    border-radius: 50%;
    color: #6f6f6f;
    font-size: 15px;
    line-height: 50px;
}

You should now be able to see the number of peers collaborating on the To-dos. Depending on the network, it might take some time to discover peers.

7. Testing if the application works with other users

Open the application in two different browsers, e.g.: Chrome and Chrome incognito. Any changes should replicate seamlessly. Be sure to also make changes while offline and see if they syncronize correctly once online.

8. Deploying the application on IPFS

Instead of using a regular web server to serve the application, we will use IPFS instead. After completing this step, our app will be 100% decentralized!

8.1. Install IPFS and run a local node

We need to have a local IPFS node for the upcoming steps. We will use a JS IPFS node but you could use a Go node instead.

Let's install it globally:

$ npm install -g ipfs

Now, open a new terminal window and start the node:

$ jsipfs init
$ jsipfs daemon

8.2. Ensure links are relative

Everything stored on IPFS is immutable and content-addressable. When a file is stored, IPFS calculates its hash and uses it as an identifier, called cid. These files can be accessed in your browser via IPFS node gateways. Since we are running the JS IPFS node, we can access files via http://localhost:9090/ipfs/<cid>.

Because the cid is unknown at build time, we can't use absolute paths to reference any links or assets. Luckily for us, Create React App allows us to set a homepage property which will be used to prefix every asset:

{
    "name": "workshop-todo-dapp",
    "homepage": ".",
    "...other": "properties"
}

8.3. Build and deploy

Let's create a production-ready version of the website by building it:

$ npm run build

This creates a build folder with all the application assets. Let's deploy it to IPFS by adding that folder to your local IPFS node:

$ jsipfs add -r build

The -r tell ipfs to recursively add all the files. At the end of the command output, you should see the cid of all added files, including the build folder one:

...
added QmcFc6EPhavNSfdjG8byaxxV6KtHZvnDwYXLHvyJQPp3uN public/favicon.ico
added <cid> build

Finally, copy the <cid> and use your local IPFS node to access the website: http://localhost:9090/ipfs/<cid>. The trick here is that other IPFS nodes that pin the same <cid> will also be eligible to serve the website!

8.4. Using a domain

In order to use a domain with your website deployed on IPFS, we need to first understand dnslink. Please watch "Quick explanation of dnslink in IPFS" by @VictorBjelkholm that explains what dnslink is in less than 3 minutes.

First, create to a ALIAS record pointing to a public Gateway (e.g.: gateway-int.ipfs.io) or a A record pointing to the IP address where your IPFS gateway is running. Lastly, create a TXT record named _dnslink.<domain> with a value of dnslink=/ipfs/<cid> (replace <domain> and <cid> with the correct values). We recommend setting a short TTL like 60 seconds.

Note that the <cid> should be pined by one or more IPFS nodes so that there's at least one node available to serve it. To do so, you may run:

$ jsipfs pin add <cid>

Interested in knowing more?

The peer-base library is still in its infancy. We are actively working on adding features such as Identity, Authentication and Authorization.

If you are interested in helping us or even just tracking progress, you may do so via:

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].