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.
This schema defines two models, planet and 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.
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 type and 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 builder (t), which we use to create an array of addRecord operations.
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.
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 involved).
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 CodeSandbox.
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.
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.
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.
See Part 2 of this example in CodeSandbox.
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.
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.
Note that the above code favors using an IndexedDB-based bucket and only falls back to using a LocalStorage-based bucket if necessary.
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.