Your interactions with the DAOstack subgraph will involve working with the following:
- Query: the graphQL queries sent to graphnode to fetch DAOstack data from the subgraph.
- Observable: object representing the stream of data to which one can subscribe.
- Subscription: invokes a given function every time a new value is emitted for the observable.
The entity methods provided by @daostack/client
for querying the subgraph, by themselves do not actually send the query to the server. Instead, each methods returns an Observable to which we can subscribe, which is what actually initiates the query.
Take a look at the following methods that return observable:
arc.daos()
proposal.state()
Now, in order to query the server we must subscribe
const observable = arc.daos()
const subscription = observable.subscribe(
(daos) => console.log(`we found ${daos.length} results`)
)
const proposal = new Propsal('0x123abc....', arc)
let stateObservable = proposal.state()
stateObservable.subscribe(
(proposal) => console.log(proposal)
)
In this guide we will describe how to create & send query and subscribe to the data requested by the query.
Subscriptions will cause the server to send you an update each time the data changes and are useful for composing asynchronous and event-based programs.
By default, subscribing to an observable will do two things:
- send a query to the server to fetch the data
- send a subscription query to the server for update events
As described in the following sections, you can query the subgraph using either entity methods or raw GraphQL queries.
The entity methods return an Observables which encapsulate some predefined qraphQL queries to fetch Entity data from the subgraph.
// proposals of the DAO
const proposalsObservable = Proposal.search(arc, { where: { dao: "0x123" } })
// members of the given dao
const membersObservable = dao.members()
To have more control over what gets fetched from the subgraph you can also customize the query. These queries will follow the standard graphQL syntax which is used to query graph explorer directly. Though the query must be wrapped inside the gql tag
const gql = require('graphql-tag')
// titles of all proposals of the DAO
let query = gql`query {
proposals ( where: { dao: "0x294f999356ed03347c7a23bcbcf8d33fa41dc830" }) { title }
}`
After creating a query, as we did in the previous section, we need to cause the query to be executed, that is, be sent to the graphnode server.
We can subscribe to an observable with {subscribe: false}
parameter (introduced in the subscribing to a query section) for sending a one-time query without subscribing to further updates from the server for result of the query.
// Get all proposals' Id of the DAO without subscribing to server
const proposalsObs = dao.proposals({}, {subscribe: false})
// send query and subscribe to the cache updates
proposalsObs.subscribe(() => {})
// Unsubscribe to cache once done
proposalsObs.unsubscribe()
Note: Refer to the Subscribe to Apollo Cache changes section to understand difference between cache and server update.
You can submit raw GraphQL queries using the static method arc.sendQuery
. Pass the query designed above as the parameter to sendQuery
.
// Get votes id and outcome of the given Proposal
arc.sendQuery(gql`query {
proposal (id: "0x1245") {
votes { id outcome }
}}`)
Use Subscriptions to invoke the handler that you supply to run every time a new value is emitted by an observable stream. This is useful to keep the app data updated as the value changes.
As we saw the Entity methods do not send the query to the server but return an observable. We must subscribe as follows to send the query as well as subscribe for server updates.
arc.daos().subscribe(() => {})
dao.state().subscribe( () => {})
Note: Refer to the Subscribe to Apollo Cache changes section to understand difference between cache and server updates.
For even more control over what data is being fetched and subscribed to, you can write explicit queries:
arc.subscribe(gql`subscribe { proposal (id: "0x1245") { votes { id outcome }})`)
new Proposal("0x1245").votes().subscribe(
(next) => {
}
)
Since, subscriptions can be expensive, this behavior can be controlled/optimized in several ways. The client library uses Apollo for data management which offers an intelligent caching and declarative approach to data fetching.
- Controlling fetchPolicy: controlling cache interaction.
- Subscribing to Apollo Cache: getting updates from Apollo cache instead of graphnode server.
- FetchAllData and Nested subscription: by querying larger set at top level and subscribing to Apollo cache for nested queries.
We can pass Apollo's fetchPolicy
argument to control how the query interacts with the cache:
- cache-first: default value. Read data from cache first, fetch from network if data is not available in cache.
- cache-and-network: return data from cache first and then always fetch from network to update the cache. It optimizes quick response while also keep cached data updated.
- network-only: will always make a request using network and write data to cache. It optimizes for data consistency with the server.
- cache-only: will never execute a query using your network interface and throw error if data not available in cache.
- no-cache: like
network-only
it will always make a request using your network interface. But, it will not write any data to the cache
arc.daos({}, { fetch-policy: 'cache-first'}) // the default value
arc.daos({where: {stage: "Boosted"}}, { fetch-policy: 'network-only'}) // bypass the cache
As we have seen the client library offers two types of subscription that can be controlled by the subscribe
parameter.
- server and cache
{ subscribe: true }
: explicitly ask for the updates from the graph-node server. Update the cache with the results of the query. - only cache
{ subscribe: false }
: do not subscribe to the updates from the server but still subscribe to the Apollo cache changes.
NOTE:
- By default
subscribe
is set to true. - Apollo cache could change as a result of another query which does subscribe to server changes.
e.g.
In q1 we will not subscribe to the updates from network but will still watch changes in the Apollo cache and return updated results if the cache changes.
arc.daos({}, {fetchAllData: true, subscribe: false}).subscribe(() => {}) // q1
In q2 we subscribe to server updates. The results of these updates are added to the Apollo cache and the observable in q1 will also get the updates if cache changes.
dao.state().subscribe( () => {}) // q2
Most of the Entity methods are implemented in such a way that the queries will fetch (and subscribe to) just as much data as is needed to create the result set. For example, dao.proposals()
will only fetch the proposal IDs. This can be controlled (in a limited way) by setting the parameter fetchAllData
to true
dao.proposals({orderBy: "creationDate"}, {fetchAllData: true})
This is useful for cache handling, where it may be useful to have more complete control over what data is being fetched. Consider the following example, which will get the list of proposals from the dao, and then get the state for each of the proposals.
dao.proposals().subscribe(
(proposals) => {
for (let proposal of proposals) {
proposal.state().subscribe(.....)
}
}
)
The problem with this pattern is that it is very expensive. The (subscription to) dao.proposals(..)
will send a query and create a subscription and then each of the calls to proposal.state()
will create a new query and a separate subscription.
Consider now the following pattern:
dao.proposals({}, { fetchAllData: true }).subscribe(
(proposals) => {
for (let proposal of proposals) {
proposal.state({}, {subscribe: false }).subscribe(.....)
}
}
)
This will resolve two inefficiencies. First of all, the fetchAllData
in the proposals query will make it so that the
dao.proposals
query will fetch (and subscribe to) a much larger query - in particular, it will get all state data for each of the proposals. This means that when prop.state()
is called, it will find all the needed information in the cache (and so it will not send a new query to the server), and we can safely pass it the subscribe: false
flag, because dao.proposals()
already subscribes to updates for all the cached data.