Building a Trello-like React/Redux App with NunDB with offline and conflict resolution features
This tutorial will demonstrate how to implement NunDB in a Trello-like application, making it a durable and offline-capable database. In this tutorial, we’ll explore how NunDB can be used to create a real-time collaborative app with conflict resolution capabilities. As the major goal of the project is to show how to deal with conflicts in NunDB, we will not try to auto-merge the models.
We used “Trello clone” from marconunnari as a starting point and only making the necessary modifications to integrate NunDB. By using this pre-existing codebase, we can quickly demonstrate the benefits of using NunDB without getting bogged down in the details of building a fully-featured application from scratch.
The goal of the project is to help you understand how NunDB can be used to build real-time collaborative applications and how to deal with conflicts that may arise during concurrent editing.
Storing data
In the original code, the author has already provided a convenient location for adding data storage in the store.js
file, specifically the line: localStorage.setItem("state", serializedState);
. We will be replacing this with storing the data in NunDB by changing the line to: nunDb.setValueSafe("state", state);
. This will allow us to take advantage of the durability and offline capabilities of NunDB in our application. But first we need to connect to an already existed database and import the NunDb library.
Connecting to NunDB at the start
import seed from "./seed"; import NunDb from 'nun-db';// Import here // Connect to the database const nunDb = NunDb("ws://nun-db-1.localhost:3058", "trelo-real-time-arbiter", "$database-pwd"); // Connect to the database here //... const saveState = state => { try { //localStorage.setItem("state", serializedState); //@mateusfreira This is the old code nunDb.setValueSafe("state", state); } catch {//@mateusfreira I don't like this but I will not chage it for now // ignore write errors } }; //...
Watch for updates from the other users
Now with the database connected we must and pushing the updates to NunDB, we must implement the watch feature, with that updates from other clients (other browsers) will be automatically sync with everyone connected.
For that we implemented a new reducer, the most interesting peace here is the UPDATE_STATE
we will use that event to propagate all changes to other clients.
In this code we also adding CONFLICT_RESOLUTION
and CONFLICT_RESOLUTION_RESOLVED
we will speack more about them latter in the chapter “Dealing with Conflicts”. Important point here is that for those events we return { changed: false }
meaning this event does not need to be propagated to NunDB, and that the last event is already coming from the database, this is a important point of attention because if you do not treat that you must like will end up with a infinity loop sending data back and forth to NunDB. In all other cases we return { changed: true }
, sinalizing to push the update to Nun-db at that time.
//store.js const remoteState = (state = { changed: false }, action) => { switch (action.type) { case "UPDATE_STATE": case "CONFLICT_RESOLUTION": case "CONFLICT_RESOLUTION_RESOLVED": return { changed: false, }; default: return { changed: true, }; } };
And now we need to change the code to deal with that and add and subscribe to all events in the store and save the data to Nun-Db, here we check for the remoteState.changed
.
store.subscribe( throttle(() => { const state = store.getState(); if (state.remoteState.changed) { saveState(state); } }, 1000) );
Now we need to change all Next we need to update all stores to deal with the new sate coming from the other users.
// https://github.com/marconunnari/trello-clone/blob/854f5a11f72098df7427a26bb73eea5adf55225c/src/store.js#L6 const board = (state = { lists: [] }, action) => { switch (action.type) { case 'UPDATE_STATE': { return action.state.board; } ... const listsById = (state = {}, action) => { switch (action.type) { case "UPDATE_STATE": { return action.state.listsById; } ... const cardsById = (state = {}, action) => { switch (action.type) { case "UPDATE_STATE": { return action.state.cardsById; } ...
Next, we need to differentiate from a store perspective, changes locally to changes coming from the database made by other users.
//store.js const changed = (state = { changed: false }, action) => { switch (action.type) { case "UPDATE_STATE":// Comming from the database { return { changed: false }; } default: return { changed: true };// Local change } }; //... const reducers = combineReducers({ board, listsById, cardsById, changed // added });
And we Remove data seed since the initial value will now come from the NunDb servers.
// seed.js export default function seed(store) { return; //...
With that we get the basic of it working as expected and we can do quick demo of the data being updated in multiple browsers.
Demo time, Now the basic is working
Lets deal with conflicts now
Next step it to deal with conflicts, lets suppose you are working offline and you collegue is also working on the same project and you both change the same card. Once you come online that would result into a conflict that needs to be solved. We will not implement any smart way to solve project here, instead any conflict (changes in 2 clients that may colide) we will trigger conflicts and ask the user to choose one of them to use.
First we need to tell Nun-db server this client is allow to fix conflicts. For that we need to call the method nun.arbiter()
. This method will be called by each conflict and it may be fired multiple times even before the resolution of the previous call. As in the UI we will be able to solve only one conflict at a time we need to create a queue of promises to support multiple conflicts.
The callback for the arbiter
function must return a Promise.
const resolveQueue = { lastConflict: Promise.resolve(), peddingConflicts: new Map(), }; function resolveConflict(e) { const conflictId = +(new Date()); return new Promise(resolve => { resolveQueue.peddingConflicts.set(conflictId, resolve); store.dispatch({ type: 'CONFLICT_RESOLUTION', conflictResolver: { conflict: e, conflictId, } }); }); } nunDb.becameArbiter((e) => { resolveQueue.lastConflict = resolveQueue.lastConflict.then(v => { return resolveConflict(e); }); return resolveQueue.lastConflict; });
-
Now our basic is working lets start coding around the conflict resolution. First I want to create the UI infra to allow the users to deal with the conflict.
-
Here I am choosing to let the user decide between 2 versions. And showing 2 buttons. As the user clicks on one, that version will show up in the board , clicking in done will apply the showing state as the current state and conclude de conflict resolution.
We implemented a new reducer to solve handle the conflict actions.
const resolveQueue = { lastConflict: Promise.resolve(), pendingConflicts: new Map(), // Fixed typo in variable name }; function resolveConflict(e) { const conflictId = Date.now(); // Simplified creation of conflictId return new Promise((resolve) => { resolveQueue.pendingConflicts.set(conflictId, resolve); store.dispatch({ type: 'CONFLICT_RESOLUTION', // Updated action type for consistency conflictResolver: { conflict: e, conflictId, }, }); }); } nunDb.becameArbiter((e) => { resolveQueue.lastConflict = resolveQueue.lastConflict.then((v) => { return resolveConflict(e); }); return resolveQueue.lastConflict; });
Now the code is already working as we expect.
Conclusion
In this tutorial demonstrate how to implement the conflict resolution on Nun-db using the Trello application as the example. You can see close and test the code by yourself in https://github.com/mateusfreira/trello-clone
- Code Coverage for Rust Projects with GitHub Actions
- NunDb is now referenced in the Database of Databases
- Real-time Medical Image Collaboration POC Made Easy with OHIF and Nun-db
- How to create users with different permission levels in Nun-db
- Match vs Hashmap! Which one is faster in rust?
- Towards a More Secure Nun-db: Our Latest Security Enhancements
- Building a Trello-like React/Redux App with NunDB with offline and conflict resolution features
- Introduction to managing conflicts in NunDB
- Keepin up with Nun-db 2023
- The new storage engine of Nun-db
- Stop procrastinating and just fix your flaky tests, it may be catching nasty bugs
- An approach to hunt and fix non-reproducible bugs - Case study - Fixing a race conditions in Nun-db replication algorithm in rust
- NunDB the debug command
- Keeping up with Nun-db 2021
- Writing a prometheus exporter in rust from idea to grafana chart
- Integration tests in rust a multi-process test example
- Leader election in rust the journey towards implementing nun-db leader election
- How to make redux TodoMVC example a real-time multiuser app with nun-db in 10 steps
- A fast-to-sync/search and space-optimized replication algorithm written in rust, The Nun-db data replication model
- NunDb How to backup one or all databases
- How to create your simple version of google analytics real-time using Nun-db
- Migrating a chat bot feature from Firebase to Nun-db
- Keepin' up with NunDB
- Going live with NunDB