This brief tutorial walks through using Orbit to manage data in a client-side application. Sticking with the “orbit” theme, this application will track some objects orbiting in our own solar system.
Schemas are used to define the models and relationships for an application.
Let’s start by defining a schema for our solar system’s data:
This schema defines two models,
moon, as well as the attributes
and relationships that are possible for each. A moon can have one planet, while
a planet can have many moons. By setting an
inverse for each relationship,
we’re telling Orbit that changes to one side of the relationship should be
reflected in the other.
Sources provide interfaces to access data. To ensure that they have the same understanding of data, every source in an application should share the same schema.
Let’s create an in-memory store as our first data source:
We can now load some data into our store and then query its contents:
The following output should be logged:
There’s a lot going on here, so let’s break it down.
First of all, each record is represented by a POJO that aligns with its
corresponding model definition in the schema. These representations conform with
the JSONAPI specification. Every record has an identity
established by a
id pair. Relationship linkage is specified in a
data object via identities.
In order to add records to the store, we call
store.update() and pass an array
of operations. Passing a function to
update provides us with a transform
t), which we use to create an array of
Note that we added the relationship between the moon and the planet on just the moon record. However, when we query the planet, we can see that the inverse relationship has also been added. This is because every operation that’s applied to the store’s cache passes through a schema consistency check.
Let’s look at how the store is queried:
Because we pass a function to
query, Orbit provides us with a query builder
q) which we can use to compose a query expression. We’re creating a simple
findRecords query that’s sorted by
name. Internally, query expressions are
represented in an AST form
that allows for nearly limitless expressivity (the only limit being that all
sources involved in processing a query need to understand the expressions
Here’s an example of a more complex query that filters, sorts, and paginates:
store.query is asynchronous and thus returns results wrapped in a
promise. This may seem strange at first because the store’s data is “in memory”.
In fact, if you want to just “peek” into the contents of the store’s memory,
you can issue the same queries synchronously against the store’s
By querying the cache instead of the store, you’re not allowing other sources to participate in the fulfillment of the query. Continue reading to understand how requests to sources can be “coordinated”.
Want to experiment with some of the concepts presented so far? See Part 1 of this example in WebpackBin.
Our in-memory data store is quite isolated at the moment. If a scientist is using our application to track their discoveries, a browser refresh might lose a whole planet or moon! 😱
Let’s create an browser storage source to keep data around locally:
Every time a source is transformed, it emits a
transform event. It’s simple
to observe these events directly:
It’s possible to pipe changes that occur in one source into another via the
Like all mutation and query methods on sources, the
sync call returns a
promise. If we want to guarantee that transforms can’t be applied to our
store without also being backed up, we should return the promise in the event
Or more simply:
With this single line of code we’ve guaranteed that every change to the in-memory store will be sync’d with the backup IndexedDB source. Furthermore, we’ve configured this synchronization to be “blocking”, so that changes to the store can’t be made at all unless they are also backed up.
Orbit provides another layer of abstraction on top of direct event
observation and handling: a
Coordinator. A coordinator manages a set of
sources to which it applies a set of coordination strategies.
A coordinator could be configured to handle the above scenario as follows:
Although this might seem like an unnecessary amount of complexity compared with the simple event handler, there are a number of benefits to using a coordinator:
You can easily add preconfigured strategies, such as an event logging strategy and a log truncation strategy (to keep the size of in-memory logs to a minimum). You can also create your own strategies and share them across applications.
Strategies can be activated and deactivated all together by simply calling
coordinator.deactivate(). Deactivating event handlers directly requires careful tracking of handler functions, which can be tedious. However, it’s important to do this to avoid leaking memory.
Coordinators can share a log-level across all strategies. Sometimes you want to see debug info and sometimes only errors.
Although we’re now backing up our store to browser storage, we have not yet set up a process to restore that backed up data.
If we want our app to restores all of its data from browser storage when it first boots, we could perform the following:
This code first pulls all the records from backup and then syncs them with the main store before activating the coordinator. In this way, the coordination strategy that backs up the store won’t be enabled until after the restore is complete.
We now have an application which has data fully contained in the browser. Any data that’s entered can be accessed while offline and will even persist across browser refreshes.
Want to experiment more? See Part 2 of this example in WebpackBin.
Most apps can’t exist in the vacuum of a browser - data tends to be far more useful when it’s shared with a server.
Let’s say that we have a web server that conforms with the
JSON:API specification. We can use Orbit’s
JSONAPISource to allow our app to communicate with that server.
We’ll start by creating a new
Next let’s add the source to the coordinator:
And then we can add strategies to ensure that queries and updates made against the store are processed by the remote server:
These strategies are all non-blocking, which means that the store will be updated / queried optimistically without waiting for responses from the server. Once the server responses are received, they will then be sync’d back with the store.
This set of coordination strategies is certainly not yet production ready. We will need exception handling in our strategies to tell Orbit how to handle network errors (e.g. retry after X secs) as well as other types of exceptions.
Optimistic server requests paired with an in-browser backup can work well for some kinds of applications. For other applications, it’s more appropriate to use blocking strategies that tie the success of store requests to a successful round trip to the server. Still other applications might choose to mix strategies, so that only certain updates are blocking (e.g. a store purchase).
Orbit allows for filtering, exception handling, and more in strategies to enable any of these options. We’ll dive deeper into these topics in the rest of this guide, the API docs, and sample applications.
At any given time, our Orbit application may have different kinds of state in-flight and unpersisted. This state may include tasks that are queued for processing, logs of transforms that have been applied, or other source-specific state that we’d like to reify if our application was closed unexpectedly.
In order to persist this state, we can create a “bucket” that can be shared among our sources:
Note that the above code favors using an IndexedDB-based bucket and only falls back to using a LocalStorage-based bucket if necessary.
bucket can be passed as a setting to any and all of our sources.
Each source will use the bucket to initialize its queues, logs, and other state. And as their state changes, sources will use buckets to persist those changes.
Of course, buckets can also be used for ad-hoc state persistence of any kind by other parts of your application. The possibilities are extensive!
That concludes a brief run-through of some of the key aspects of Orbit. Please continue reading the guides to gain a deeper understanding of how Orbit works and how to make the most of it.