New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aragon.js “simple” API #2

Open
bpierre opened this Issue Sep 11, 2018 · 6 comments

Comments

Projects
None yet
6 participants
@bpierre
Copy link
Member

bpierre commented Sep 11, 2018

⚠️ 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:

  • Aragon Core (e.g. notifications).
  • The active web3 provider (e.g. accounts).
  • The app contract (e.g. events).
  • Other contracts (using external()).

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
}

@bpierre bpierre changed the title Aragon.js simple API Aragon.js “simple” API Sep 11, 2018

@luisivan

This comment has been minimized.

Copy link
Member

luisivan commented Sep 12, 2018

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

@izqui

This comment has been minimized.

Copy link
Member

izqui commented Sep 12, 2018

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

This comment has been minimized.

Copy link
Member

bpierre commented Sep 12, 2018

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.

@PascalPrecht

This comment has been minimized.

Copy link

PascalPrecht commented Sep 12, 2018

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:

  • While contract is a contract instance, externalContract is a factory function. It may make sense to make that a bit more obvious. So I'd propose something like createExternalContract or rather getExternalContract (as we're not really creating here)

  • On contract.get() vs contract.call() - I think here you might want to take into account what the goal is: do you want this API to look similar to web3 APIs, or do you want an API that just makes sense? I personally have to say that calling contract.call('value') doesn't look like reading a value at all. Obviously, if you're used to web3 APIs, this might be different. Maybe having an alias isn't so bad after all. However .get() is a little ambiguous as well... getProperty()?

  • On intent() vs send() - Here I'd also go with an API that makes sense as opposed to one that is similar to web3 APIs. I personally wouldn't mind having intent() at least as an alias as it's official aragon terminology. Or, and this is going to be funny, call() would make sense here as well :D

  • Can you add some more thoughts on how you'd support Observable APIs and callback/promise APIs? While I do get the reasoning that many people struggle with using Observables (we had this a lot in the Angular community), one can always transform an Observable to a promise using toPromise() operator (although, not recommended). I think if you'd find a way to have first class support for both APIs, that'd be great. I would not replace observable APIs for promise based APIs, just for the sake of being able to use async/await. Observables have tons of powerful operators that developers may want to take advantage of when building apps. Also, as mentioned earlier you can always map to a promise eventually. E.g.

     contract.get('value').pipe(
      map(...),
      filter(...)
     ).subscribe(value => /* do something with transformed value */)
    

    With await:

     await contract.get('value').pipe(
      map(...),
      filter(...),
      toPromise()
     )
    

    I think it'd be cool to keep this power.

  • I have mixed feelings about the createAppState() name but I also don't have a better alternative at hand right now (plus I'd avoid bikeshedding too much) :)

Hope this makes sense!

@macor161

This comment has been minimized.

Copy link

macor161 commented Sep 15, 2018

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.

@0x6431346e

This comment has been minimized.

Copy link
Member

0x6431346e commented Oct 5, 2018

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment