witnet / sheikah

A Witnet compatible desktop wallet and smart contracts development environment
https://witnet.io
GNU General Public License v3.0
37 stars 23 forks source link

Discussion: front<>back architecture #49

Closed aesedepece closed 6 years ago

aesedepece commented 6 years ago

Abstract

We will be making the most of Electron and React to ensure the app feels as native and responsive as possible. To do so, we need first to define a clear architecture that sets forth what will be happening in every part of our app. Clearly stating the responsabilities of both the Electron renderer and the main process is paramount to achieve a consistent and reliable architecture.

Background

Electron conveniently uses two separate processes to isolate the code that will be run in a node-like context (the main process) from the code that will be run inside a web-like context (the renderer process).

The main process

The main process has superpowers. Your code is run in a node-like context, so you have access to everything in the userspace of the host machine. This means you can have native access to the filesystem, hardware devices, etc. without having to deal with restrictive web APIs.

A node-like context also gives you the possibility to run Javascript bindings of natively implemented functions (through N-API), which is specially convenient for implementing performance-intensive code (cryptosystems and the like).

The renderer process

The renderer process is nothing more than web content running in a browser (WebKit). The APIs available in this context are exactly the same that you could expect when running Javascript code in a modern browser (including all ES6 and some ES Next).

Current architecture

Our current high level architecture design relies on the main process to act as a sort of back-end that will be responsible for taking care of persisting data in a local database (see #33), performing all the expensive operations related to crypto and brokering communication with rust-witnet, our full node implementation.

On the other hand, the renderer process acts as front-end or user interface. It is powered by React and relies on Redux to handle app state mutations in a consistent and robust manner.

The front-end back-end can communicate by passing messages to each other thanks to Electron's native IPC modules (ipcMain and ipcRenderer). The IPC connection itself is conveniently wrapped by a dedicated API with a series of modular handlers (see #27).

The dilemma

From this point, we can take to different roads:

Option A

Option B

aesedepece commented 6 years ago

My personal thesis

In order to illustrate my thesis, I will use an anatomical analogy: the different parts of the human nervous system.

I see the back-end as the central nervous system, which integrates information it receives from all parts of the body and coordinates and influences their activity.

In the other hand, I compare the front-end to the peripheral nervous system, which is responsible for communicating sensory stimuli to the central nervous system and translating motor impulses back. It also has some degree of autonomy, providing instant feedback to time-sensitive stimuli (reflexes).

You can surely see my point here. We can replicate the optimal design that natural selection has come up with over million of iterations (:skull:).

This seams making the back-end act as the "brain" of our app, concentrating most of the logic. In the other hand, the front-end would be able to handle some interactions on its own, but it would simply pass most of the user input to the back-end for it to react upon and then visibly show the result.

Summing up, I advocate option B. What's your opinion, @mmartinbar, @kronolynx, @mariocao and @anler? Let's discuss pros and cons for both options!

mmartinbar commented 6 years ago

I am no expert in the matter but Option B (back-end being the "brain" of the app) seems more reasonable to me as well. As stated by @aesedepece, in Option A almost all business logic would remain in the front-end, but not everything, creating a coupling between the two that could be unnecessary. Besides, network latencies in communication between the back-end and the front-end are no problem in our case, as both back-end and front-end run in the same machine and communicate through IPC.

anler commented 6 years ago

I also back Option B :) I think it will give us more freedom for refactoring/redesign in the future.

aesedepece commented 6 years ago

This is the architecture I've been thinking of recently:

This is similar to how Redux works under the hood, indeed.

One cool thing about this design is that updaters, being plain Javascript objects, can easily be serialized and fearlessly sent over the IPC.

EDITS:

aesedepece commented 6 years ago

Here's an API example.

// On the front-end
function Backend.App.getState(): Promise<State>
function Backend.Wallets.loadList(): Promise<Updater>
function Backend.Wallets.unlock
  (id: Id<Wallet>, password: string): Promise<SubWallet, Updater>
function Backend.Addresses.getNew(id: Id<SubWallet>): Promise<Address, Updater>
function Backend.Transactions.getNewP2PKH
  (address: Bech32<Address>, amount: number): Promise<Transaction, Updater>
function Backend.Transactions.broadcast(txid: Id<Transaction>): Promise<Updater>
function Backend.Wallets.close(id: Id<Wallet>): Promise<Updater>
function Backend.App.close(): Promise<void>

// On the back-end
function Frontend.App.pushStateUpdater(updater: Updater): Promise<void>
function Frontend.Transactions.pushNewIncoming
  (transaction: Transaction, updater: Updater): Promise<void>

This affects #28 and #29.

aesedepece commented 6 years ago

As a side note, as a contractual convention for methods like the ones depicted in the previous comment, all methods named getSomething() would always return Promise<Something, Updater> except Backend.App.getState, which would only return Promise<State>.

Also, in those examples, failures of any kind would be thrown and would need to be catched in the .catch clause of each promise.

Actions triggered by the user should wait for success and recover from failure, handling the error elegantly and giving the user helpful feedback and the chance to retry the action.

Actions which autonomously run in the background should be able to recover from failure too, but their use of automatic retrial should depend on each specific action type. Failing quietly or loudly (some kind of UI feedback or notification) should also be optional, but dependant on user settings and the execution environment (dev/test/main).