Building a Debate App: Interlude

By akohad Apr26,2023

[ad_1]

Decentralization / Social media

Refactoring and what comes next…

While working on the seventh part in this series, I decided it is time for some refactoring of src/AppData.ts, which has become a bit large, contains lots of classes and also has a lot of repetition. I thought that this would also be a good point to reflect on the purpose of this series and what’s coming up in future articles.

I’m not going to describe every line that changed in refactoring the AppData class because it’s not very interesting. Instead, I’ll describe the end state, which will be useful to know going forwards.

PageData class

Previously, some of the collections would take a pageId argument to provide independent views of the collection for different pages. As I was working on part 7, I found that more collections needed to be partitioned this way because of undesirable interactions with the callbacks from different pages.

I came to the conclusion that it would be much simpler/cleaner for each page to have its own data class. Essentially everything from AppData moves to a new PageData class and AppData contains a PageData instance for each page:

export class AppData {
private _home: PageData = new PageData();
private _messages: PageData = new PageData();
private _presentations: PageData = new PageData();

init(db: IDb, selfPublicKey: string) {
this._home.init(db, selfPublicKey);
this._messages.init(db, selfPublicKey);
this._presentations.init(db, selfPublicKey);
}

get home() { return this._home; }
get messages() { return this._messages; }
get presentations() { return this._presentations; }
}

Instead of passing an AppData instance to each page, we now pass its associated PageData instance in src/App.tsx:

                <IonRouterOutlet>
<Route exact path="/home">
<HomePage pageData={appData.home} />
</Route>
...
<Route path="/debate/:id/messages/:side">
<MessagesPage pageData={appData.messages} />
</Route>
<Route path="/debate/:id/presentations">
<PresentationsPage pageData={appData.presentations} />
</Route>
...
</IonRouterOutlet>

Collection and SubCollection classes

Inside of PageData, instead of repeating a lot of boilerplate code for each collection, that code is encapsulated into two classes:

  • Collection — represents a single named top-level collection (e.g. debates)
  • SubCollection — represents a sub-collection that consists of multiple named collections (e.g. messages) under a top-level named collection

These are then just instantiated inside PageData:

export class PageData {
private _db: IDb | null = null;
private _selfPublicKey: string | null = null;
private _debates: Collection<IDebate> = new Collection('debate', everyoneAppend);
private _messagesFor: SubCollection<IMessage> = new SubCollection('debate', 'messages-for', everyoneAppend);
private _messagesAgainst: SubCollection<IMessage> = new SubCollection('debate', 'messages-against', everyoneAppend);
private _presentations: SubCollection<IPresentation> = new SubCollection('debate', 'presentations', everyoneUpdateOwn);
private _votes: SubCollection<IVote> = new SubCollection('debate', 'votes', everyoneUpdateOwn);

init(db: IDb, selfPublicKey: string) {
this._db = db;
this._selfPublicKey = selfPublicKey;
this._debates.init(db);
this._messagesFor.init(db, selfPublicKey);
this._messagesAgainst.init(db, selfPublicKey);
this._presentations.init(db, selfPublicKey);
this._votes.init(db, selfPublicKey);
}

get selfPublicKey() { return this._selfPublicKey; }

get debates() { return this._debates; }
get messagesFor() { return this._messagesFor; }
get messagesAgainst() { return this._messagesAgainst; }
get presentations() { return this._presentations; }
get votes() { return this._votes; }

...
}

The helpers for aggregating vote counts and determining own vote direction are left at the top level:

export class PageData {
...

// Votes helpers

ownVoteDirection(debateId: string): VoteDirection {
if (!this._selfPublicKey)
return VoteDirection.Undecided;
return this._votes.entry(debateId, this._selfPublicKey)?.direction || VoteDirection.Undecided;
}

votesFor(debateId: string): number {
return this._votes.entries(debateId).reduce((c, v) => c + v.direction == VoteDirection.For ? 1 : 0, 0);
}

votesAgainst(debateId: string): number {
return this._votes.entries(debateId).reduce((c, v) => c + v.direction == VoteDirection.Against ? 1 : 0, 0);
}
}

Initialization callback

Finally, I have added a mechanism for notifying the pages when the DB is initialized:

export class PageData {
...
private _callbacks: Callbacks = new Callbacks();
...

init(db: IDb, selfPublicKey: string) {
...
this._callbacks.notify();
}

onInit(callback: () => void) {
const remover = this._callbacks.on(callback);
if (this._db)
callback();
return remover;
}

This ensures that the page gets notified once with a valid DB. If the page loads before the DB is initialized then the callback will be invoked later when the DB initializes. If the DB was already initialized before the page loads then the callback is invoked immediately.

Potential Bonono improvements

We’ve now got a bunch of fairly generic code relating to managing collections and sub-collections. This begs the question: should this be folded back into Bonono? It’s a bit annoying if everybody ends up rolling their own version of these classes.

This is definitely something I’m considering but perhaps there is a more generic way to query across multiple collections and efficiently manage which collections are loaded without having to explicitly load and unload them.

I’ve been writing this series kind of in the style of a tutorial. I say ‘kind of’ because I think of a tutorial as explaining something that is fairly well established and therefore can be laid out authoritatively.

In truth, this series is more like an exploration — discovering how to build an app with Bonono on the fly. I’ve done it this way because I knew this was going to be a large undertaking and if I was to figure everything out before writing a word, it would be a long time of not writing anything.

From a selfish point of view, writing these articles is very helpful. They act as a kind of code review that forces me to keep rethinking what I’m doing and look for simpler ways to do things while aiming for decent code quality.

The main reason for developing a non-trivial app like this debate app is to increase the chance of attracting users for it. The hope is that a sufficient number of people use the app so it’s possible to test how Bonono scales and to discover and fix performance issues.

Developing this app also gives opportunities to explore a variety of use cases and prove that Bonono can be successfully used for a ‘real’ app. Finally, it’s generally just more interesting and motivating to try and build something that could be useful.

To give you an idea of what’s coming up in the rest of the series, these are the main topics that I’m planning to cover (order may change):

  • Starting and stopping debates (coming soon!)
  • Implementing likes
  • Identifying as a member of a group when voting
  • Allocating usernames
  • Creating a user profile
  • Bookmarking debates
  • Searching for debates
  • Collection entry validation
  • Proof-of-work requirements
  • Collection pinning
  • Decentralized content moderation
  • Pubsub performance
  • UI improvements
  • E2E and unit testing
  • Deployment

[ad_2]

Source link

By akohad

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *