The blockchain-redux experiment continues. State is now shared and preserved.
When I last wrote about this blockchain-backed Redux clone in 105 lines of code, it could:
- build a local blockchain
- add blocks on every action
dispatch
- verify the chain is valid
- replace the chain with a longer chain
All the basic building blocks. But it didn't quite have feature parity with the real redux, and it lacked some way to share the blockchain between nodes. You started a new chain with every call to createStore()
.
That has now been fixed.
Feature parity with Redux
We reached Redux feature parity… I think. We added the ability to subscribe to changes and to use middleware.
blockchain-redux looks and feels the same as real Redux when you're using it, and I think that means it's got feature parity. In theory, you should be able to drop-in any redux plugin or middleware and it's gonna work.
Subscribing to changes
The concept here is simple:
- A method adds functions to an array of
listeners
- Dispatch calls all of them
let listeners = [];
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
listeners.splice(listeners.indexOf(listener), 1);
};
}
We keep a listeners
array. It starts as empty. Calling subscribe
, which is exposed as an API, adds your listener to the list. In return
you get a function that lets you unsubscribe.
Using listeners.splice
to remove functions like that comes straight from Redux source. It's probably faster than building a new list using .filter
.
Middleware support
Adding middleware support was trickier.
Middleware, in essence, wraps the Redux store and changes how its functions work. You can think of it like a higher-order-component or a function decorator. Whichever makes more sense to you.
Redux itself makes this easier with an applyMiddleware
function, but I didn't want to build that just yet. I don't think it's strictly necessary, but maybe that breaks feature parity. 🤔
First part is to add a 3rd argument to createStore
– the enhancer
.
function createStore(reducer, preloadedState, enhancer) {
if (
typeof preloadedState === "function" &&
typeof enhancer === "undefined"
) {
enhancer = preloadedState;
preloadedState = undefined;
}
if (typeof enhancer === "function") {
return enhancer(createStore)(reducer, preloadedState);
}
If enhancer
is a function, we pass the createstore
function to it and say, "Ok, you do this.”
The enhancer, a middleware, then takes care of instantiating the store and changing its behavior. A simple middleware that prints out every call to dispatch
would look like this.
// a logging middleweare
export default function () {
return (createStore) => (...args) => {
const store = createStore(...args);
const _dispatch = store.dispatch;
function dispatch(action) {
_dispatch(action);
console.log("ACTION", action);
}
return Object.assign(store, {
dispatch,
});
};
}
We're using some functional concepts here to do our thing. Currying for the most part.
Our logging middleware returns a function that takes createStore
as its argument. This in turn returns a function that takes arguments, usually reducer
and preloadedState
.
It immediately calls createState
to build the store. This time without passing a middleware.
Then our redux middleware overwrites the dispatch
function with its own function that prints actions as well as calls the original dispatch.
In the end, we return the augmented store.
I borrowed this approach from Redux's applyMiddleware function, and it works great. Where I think my approach might suffer is applying multiple middlewares.
But we can solve that problem when we get to it 😇
Sharing the chain between nodes
We went to all that trouble of supporting middleware so that we can build different strategies for sharing our blockchain. blockchain-redux itself should focus on building the blockchain, dispatching actions, and keeping state.
How that gets shared between nodes lives in a separate layer.
For now, that layer is Firebase. This gives us an easy backend to work against and make sure nodes are communicating. Great way to iron out kinks, but it makes our blockchain centralized.
We'll solve the centralization problem next.
Here's how the Firebase middleware works 👇
Our middleware exports a function that takes an initialized FirebaseApp as its argument. This made it easier to have multiple instances of the store sharing the same Firebase when testing.
Just like before, the only function it augments is dispatch
. It now tries to save new blocks to Firebase.
function dispatch(action) {
_dispatch(action);
const newBlock = store.getLastBlock();
return saveBlock(newBlock);
}
Dispatch the action, get latest block from the store, save block to firebase. 👌
Saving that block is fraught with some difficulty, however.
function saveBlock(block) {
block._data = block._data || {};
return db
.ref(`blockchain/${block.index}`)
.once("value")
.then((snapshot) => {
if (!snapshot.exists()) {
db.ref(`blockchain/${block.index}`).set(block);
return true;
} else {
// collision resolution?
return false;
}
});
}
We save blocks at blockchain/<index>
. On every save, we check if the blocks already exists. If it doesn’t, we save the block and everyone is happy.
However, if somebody else already created the same block successfully, we're in trouble. Right now, the middleware silently drops the save, and that's bad.
I think the easiest solution will be to sync with the Firebase blockchain before dispatching an action and making a new block. Or we could dispatch the action and run a save-dispatch-save cycle until it succeeds.
Neither of those approaches is very optimal from a network utilization perspective. Not sure yet what to do here.
Initing the store from blockchain
Another interesting problem was initializing our store from the blockchain. How do you ensure you have the whole chain before building new blocks?
One approach I found was to read the whole thing from Firebase and replacing the local stub chain with the big one.
function initFromFirebase() {
return db
.ref("blockchain")
.orderByKey()
.once("value")
.then((snapshot) => snapshot.val())
.then((blockchain) => {
blockchain = Object.values(blockchain).map((block) => {
block.data = block._data ? JSON.parse(block._data) : {};
return block;
});
store.replaceChain(blockchain);
return Object.assign(store, {
dispatch,
});
});
}
return Promise.all(store._blockchain.map(saveBlock)).then(initFromFirebase);
initFromFirebase
reads our central blockchain from Firebase, goes through the blocks and makes sure data
is in the right format, then runs replaceChain
.
You'll notice this is now where we return the Redux store. That means createStore
is asynchronous and wreaks havoc on just about everything we hold dear about using Redux.
But I'm not sure how to fix that. We need to have the chain before dispatching our first function. Maybe a waiting mechanism in dispatch 🤔
Oh, and that last part? That's so we create the blockchain stub on Firebase in case it doesn't exist yet. Goes through blocks in the default chain and makes sure they exist on Firebase.
What next
This is still a rough experiment. The blockchain implementation works, it communicates through Firebase so it's distributed, but it has trouble with conflict resolution, has a centralized source of truth, and is very network and memory inefficient.
All of that is bad.
I want to move towards using Firebase as just a communication protocol between nodes so instead of saving the whole blockchain there, we just announce new blocks and other nodes pick them up. That would be more efficient.
I also want to figure out how to avoid saving the whole blockchain locally. But maybe that's fundamentally not possible 🤔
Got suggestions? Yell at me on Twitter.
Continue reading about Blockchain Redux experiment, part 2
Semantically similar articles hand-picked by GPT-4
- A blockchain-backed Redux clone in 105 lines of code
- Blockchain-redux becomes useful, part 1
- Livecoding recap: Blockchain-redux now shares blocks between clients in real-time
- Learning WebRTC peer-to-peer communication, part 1
- How to write tests for XState – CodeWithSwiz 12
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️