DECENTRALIZATION / SOCIAL MEDIa

Building a Debate App: Part 15

Bye bye js-ipfs, hello Helia! (sort of)

Jeremy Orme
Coinmonks
Published in
4 min readSep 3, 2023

--

Photo by Fabrice Nerfin on Unsplash

The library on which Bonono is based, js-ipfs, has been deprecated and a new, leaner, implementation called Helia now replaces it. Therefore, Bonono is dropping js-ipfs.

Helia takes a more modular approach where just the required functionality can be included and plugged in. To complement this approach, Bonono is changing to no longer be responsible for setting up ipfs/libp2p.

Bonono has a very small contact area with ipfs/libp2p currently via a pair of interfaces that allow it to:

  • Put/get content by content it
  • Publish/subscribe to a particular topic.

Going forwards, the client shall provide callback functions implementing these functionalities so that Bonono doesn’t assume any particular implementation and the client has complete control over their p2p network setup.

In this part, we’ll update our debate app to use the Bonono version 0.7.0 that includes the changes described above and implement the required callbacks using Helia.

We pick up from where we left off last time:

git clone https://github.com/jeremyorme/debate.git
cd debate
git checkout release/0.0.13

To Bonono 0.7, and beyond!

Ok, not beyond, just to 0.7.0, which is the latest at the time of writing. This version brings the changes to drop the dependency on js-ipfs from bonono.

Let’s install the latest version using npm:

npm install bonono

Bonono-react begone!

An IDbClient used to be exposed via the onDbClient event of the BononoDb web component. A react wrapper was provided for that component to make it a 1st class react component. All of this is now dispensed with and instead the DbClient class is exposed by bonono.

This means we can say goodbye to bonono-react:

npm remove bonono-react

There are several files in our project that import types from bonono-react. These can all be changed to import the same types from bonono. The affected files are:

  • src/app-data/AppData.ts
  • src/app-data/Collection.ts
  • src/app-data/CollectionManager.ts
  • src/app-data/PageData.ts
  • src/app-data/SubCollection.ts

Lots of script tags

As I said, Helia has a modular design. Therefore, we need to include numerous Helia and libp2p modules in place of the single ipfs module we had previously in public/index.html:

     <script src="https://cdn.jsdelivr.net/npm/helia@^2.0.1/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@helia/json@^1.0.2/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/blockstore-core@^4.3.3/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/datastore-core@^9.2.2/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/libp2p@^0.46.8/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@libp2p/webrtc@^3.1.8/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@chainsafe/libp2p-noise@^13.0.0/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@chainsafe/libp2p-yamux@^5.0.0/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@chainsafe/libp2p-gossipsub@^10.1.0/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/multiformats@^12.1.0/dist/index.min.js"></script>

Helia/libp2p components

The above modules load a bunch of stuff into the global namespace so we’ll grab some of those bits to set up ipfs/libp2p. We add the following to src/App.tsx:

 const g = globalThis as any;
const createHelia = g.Helia.createHelia;
const json = g.HeliaJson.json;
const MemoryBlockstore = g.BlockstoreCore.MemoryBlockstore;
const MemoryDatastore = g.DatastoreCore.MemoryDatastore;
const createLibp2p = g.Libp2P.createLibp2p;
const webRTCDirect = g.Libp2PWebrtc.webRTCDirect;
const noise = g.ChainsafeLibp2PNoise.noise;
const yamux = g.ChainsafeLibp2PYamux.yamux;
const gossipsub = g.ChainsafeLibp2PGossipsub.gossipsub;
const CID = g.Multiformats.CID;

Replace the BononoDb component

The BononoDb component is no more so we need to remove this line:

<BononoDb address="/dns4/nyk.webrtc-star.bonono.org/tcp/443/wss/p2p-webrtc-star/" onDbClient={e => loadDb(e.detail)} />

Now we need to generate our own event to trigger loadDb. We can do this by adding an effect:

useEffect(() => {
loadDb();
}, []);

We don’t have a db client anymore so the IDbClient argument in loadDb is dropped. Instead, we’ll create and connect the db client inside loadDb.

    const loadDb = async () => {

// ...

const dbClient: IDbClient = new DbClient(
peerId, publish, subscribe, addMessageListener, getObject, putObject);

if (!await dbClient.connect())
return;

appData.init(db, publicKey);
};

The DbClient constructor requires a peer id and several callbacks implementing pubsub and content storage/retrieval. To implement those we first need to create a libp2p instance:

         const address = "/dns4/nyk.webrtc-star.bonono.org/tcp/443/wss/p2p-webrtc-star/";

// the blockstore is where we store the blocks that make up files
const blockstore = new MemoryBlockstore()

// application-specific data lives in the datastore
const datastore = new MemoryDatastore()

// libp2p is the networking layer that underpins Helia
const libp2p = await createLibp2p({
datastore,
addresses: {
swarm: [address]
},
transports: [
webRTCDirect()
],
connectionEncryption: [
noise()
],
streamMuxers: [
yamux()
],
services: {
pubsub: gossipsub()
}
});

Then we can create a Helia instance with a Helia-json wrapper to read and write json objects:

// Set up Helia and create call-back functions
const helia = await createHelia({
datastore,
blockstore,
libp2p
});

const j = json(helia);

Now we have Helia providing content id based object access and libp2p providing our pubsub implementation, all that remains is to hook these up to Bonono by implementing the callbacks:

         const peerId = libp2p.peerId.toString();

const publish = (channel: string, content: string) => {
libp2p.services.pubsub.publish(channel, new TextEncoder().encode(content));
}

const subscribe = (channel: string) => {
libp2p.services.pubsub.subscribe(channel);
}

const addMessageListener = (listener: (channel: string, content: string) => void) => {
return libp2p.services.pubsub.addEventListener('message', (e: any) => {
listener(e.detail.topic, new TextDecoder().decode(e.detail.data));
});
}

const getObject = async (cid: string): Promise<any> => {
const obj = await j.get(CID.parse(cid));
}

const putObject = async (obj: any): Promise<string> => {
const cid = await j.add(JSON.stringify(obj));
return cid.toString();
}

That’s it! You can get the full source to the end of this article from the 0.0.15 release branch:

git clone https://github.com/jeremyorme/debate.git
cd debate
git checkout release/0.0.15

--

--

Jeremy Orme
Coinmonks

Software engineer. Experimenting with database decentralization