aragon / aragon-app-dx

This project is meant to discuss and shape the ideal experience we want for Aragon app developers (based on the Aragon React Kit Boilerplate).
8 stars 2 forks source link

Aragon.js “simple” API #2

Open bpierre opened 6 years ago

bpierre commented 6 years ago

⚠️ Work in progress, posting it early to get some feedback.

Some issues that I see with the current API:

RxJS

Users are required to have some knowledge of RxJS. I think we could provide a simpler API that would replace RxJS by using callbacks (node style) and promises. The RxJS API would still be available to users that want to use it.

Messaging Providers

Aragon.js providers don’t appear like something useful to learn as a new Aragon developer. The concept of messaging providers in Aragon.js could only be exposed to users that want to do something specific with it.

Same API in the worker and the app frontend

Aragon.js is exposing the same constructor on the worker side and on the frontend side. This is elegant and powerful, but the “recommended” way to do things might not be obvious at first. For example, users are expected to use aragon.store() in the worker, and aragon.subscribe() in the frontend.

The user-facing API could be designed in a way that makes our intention more obvious, or as Python’s PEP 20 describes it:

There should be one-- and preferably only one --obvious way to do it.

Implicitly based on the main contract

Aragon.js is a general API to interact with:

With the current API, it might not be obvious that call(), events(), store() and state() are referring to the main app contract, while other methods are related to UI features (identify(), notify(), context()), the web3 provider (accounts()), or the Aragon.js caching mechanism (cache()).

Interacting with the main contract could be done at the same level than the other features (i.e. not on the main object directly).

Direct methods

Methods are defined directly on AragonApp instances, which is convenient but could conflict with the other methods available.

It also feels inconsistent with external contracts, where direct methods are used to perform read-only calls, while using .call() on AragonApp.prototype.

Proposed solution: a “simple” API

This API would be built on top of the current API, and would attempt to solve the issues listed above. It would consist of exposing two different constructors: one for the worker, and one for the frontend app. The Aragon.js providers would not be exposed, and interacting with the main contract would be made more explicit. RxJS observers would be replaced by callbacks and promises.

Note: examples are using top level await.

// main.js (frontend)

import { AragonClient } from '@aragon/client'

// initiates a connection with the wrapper (including the handshake)
const { contract, accounts, externalContract } = new AragonClient()

// get event updates from the app contract (note: this should probably move to the worker API)
contract.on('event', event => console.log('app event', event))

// initiate an intent on the contract
contract.intent('increment', 1)

// fetch read-only data from the contract
const counter = await contract.get('value')

// …or we could use .call() to follow the web3 API
// const counter = await contract.call('value')

// use an external contract
const token = externalContract(address, abi)

// get event updates from an external contract (note: this should probably move to the worker API)
token.on('event', event => console.log('token event', event))

// fetch read-only data the same way we do for the main contract
const symbol = await token.get('symbol')

// get accounts updates
accounts.on('update', accounts => console.log('accounts update', accounts)

// get the current accounts
const account = await accounts.get()
// appstate.worker.js (worker)

import { createAppState } from '@aragon/client'

createAppState((state = 0, event) => {
  switch (event.name) {
    case 'Decrement':
      return decrement(state)
    case 'Increment':
      return increment(state)
    default:
      return state
  }
})

function decrement(counter) {
  counter--
  worker.notify('Counter decremented', `The counter was decremented to ${counter}`)
  return counter
}

function increment(counter) {
  counter++
  worker.notify('Counter incremented', `The counter was incremented to ${counter}`)
  return counter
}
luisivan commented 6 years ago

This is amazing. Can we make the repo public and ask in #dev to see what people think about it?

izqui commented 6 years ago

I really like the direction of this.

I think we should keep contract.call('method', ...) even if we also add contract.get('method', ...) which would do the same.

Also do you think we should keep the ability to initiate intents by doing contract.method(...) directly? Renaming contract.intent('method', ...) to contract.send('method', ...) would also be more similar to web3.js semantics.

bpierre commented 6 years ago

I think we should keep contract.call('method', ...) even if we also add contract.get('method', ...) which would do the same.

Cool let’s keep contract.call(), but we should probably kill contract.get() then 😁

Renaming contract.intent('method', ...) to contract.send('method', ...) would also be more similar to web3.js semantics.

OK, let’s use .send().

Also do you think we should keep the ability to initiate intents by doing contract.method(...) directly?

Yes I think it’s ok now that it’s on its own object, and that we are not using direct methods for .call() on the external contract. It will make additions complicated in the future, but I think it’s an acceptable risk.

0x-r4bbit commented 6 years ago

Hey everyone,

@bpierre I'm so happy to see I'm not the only one who thinks this can be improved to avoid confusion! 😅

Some good ideas in there. I have a couple of thoughts on a few points that I'd just like to leave here. Maybe they help making certain design decisions:

Hope this makes sense!

macor161 commented 6 years ago

We really like those ideas!

For me, RxJs is really interesting when dealing with streams of data that are received over time. But most of AragonApp functions return a value only once, so I think returning a Promise would be simpler for the majority of users. Also, converting a Promise to an Observable is trivial so it wouldn't really hamper people who still want to use RxJs.

It is also great for people like us who plan to support both Aragon and web3.js as it will reasonably simplify our codebase if both libraries return Promises.

With that said, maybe the events() function would be a good fit for returning an Observable.

We also like the idea of moving contract functions into a separate object, preventing conflicts and making the interface cleaner.

As for the terminology, I personally have a small preference for function names closer to the web3.js API because a lot of users are coming from an Ethereum background where they already have experience with that library. So I think it would be a little bit easier for them if the syntax is similar to what they are used to work with.

kernelwhisperer commented 6 years ago

~One thing that is not clear for me is the usage of web workers: are they necessary, what problem do they solve? Maybe we can provide an example without them.~ Answer

What I would really love is to use aragon.js with the redux tooling that is out there, also to be able to split the reducer in multiple files, etc. (I have a feeling that is already possible though)