Orbit is a framework for orchestrating access, transformation, and synchronization between data sources.
Orbit is written in Typescript and distributed on npm as packages containing a variety of module formats and ES language levels. Most Orbit packages are isomorphic - they can run in modern browsers as well as in the Node.js runtime.
Orbit was primarily designed to support the data needs of ambitious client-side web applications, including:
Optimistic and pessimistic UX patterns.
Pluggable sources that share common interfaces, to allow similar behavior on different devices.
Connection durability by queueing and retrying requests.
Application durability by persisting all transient state.
Warm caches of data available immediately on startup.
Client-first / serverless development.
Custom request coordination across multiple sources, allowing for priority and fallback plans.
Branching and merging of data caches.
Deterministic change tracking.
Undo / redo editing support.
In order to meet these ambitious goals, Orbit embraces a set of basic constraints related to data sources and interactions between them.
Any number of data sources of varying types and faculties may be required to build any given web application.
Sources of data vary widely in how they internally represent and access that data.
Communication between sources must happen using a compatible set of interfaces.
Data that flows between sources must be normalized to a shared schema.
Sources need a notification system through which changes can be observed. Changes in one source must be able to trigger changes in other sources.
Data flow across sources must be configurable. Flows can be optimistic (successful regardless of their impact) or pessimistic (blocked until dependent changes have resolved).
Mutations, and not just the effects of mutations, must be trackable to allow changes to be logged, diff’d, sync’d, and even reverted.
Orbit’s core primitives were developed to align with the goals and constraints enumerated above.
Records are used to represent data in a normalized form. Each record has a
id, which together establish its identity. Records may also include
other fields, such as attributes and relationships with other records.
Schema defines all the models in a given domain. Each
Model defines the
characteristics for records of a given type.
Every source of data, from an in-memory store to an IndexedDB database to a REST
server, is represented as a
Source. Sources vary widely in their capabilities:
some may support interfaces that for updating and/or querying records, while
other sources may simply broadcast changes. Schemas provide sources with an
understanding of the data they manage.
Transform is used to represent a set of record mutations, or “operations”.
Operation represents a single change to a record or relationship (e.g.
adding a record, updating a field, deleting a relationship, etc.). Transforms
must be applied atomically - all operations succeed or fail together.
The contents of sources can be interrogated using a
Query. Orbit comes with a
standard set of query expressions for finding records and related records. These
expressions can be paired with refinements (e.g. filters, sort order, etc.). A
query builder is provided to improve the ergonomics of composing queries.
Log provides a history of transforms applied to each source.
Every action performed upon sources, whether an update request or a query, is
modeled as a
Task. Tasks are queued by sources and performed asynchronously
Bucket is used to persist application state, such as queued tasks and
Coordinator provides the declarative “wiring” needed to keep an Orbit
application working smoothly. A coordinator observes any number of sources and
applies coordination strategies to keep them in sync, handle problems, perform
logging, and more. Strategies can be customized to observe only certain events
on specific sources.
The primitives in Orbit were developed to be as composable and interchangeable as possible. Every source that can be updated understands transforms and operations. Every source that can be queried understands query expressions. Every bucket that can persist state supports the same interfaces.
Orbit’s primitives allow you to start simple and add complexity gradually without impacting your working code. Need to support real time sockets or SSE? Add another source and coordination strategy. Need offline support? Add another source and coordination strategy. When offline, you can issue the same queries against your in-memory store as you could against a backend REST server.
Not only does Orbit allow you to incur the cost of complexity gradually, that cost can be contained. New capabilities can often be added through declarative upfront “wiring” rather than imperative handlers spread throughout your codebase.
Although Orbit does not pretend to absorb all the complexity of writing ambitious data-driven applications, it’s hoped that Orbit’s composeable and declarative approach makes it not only attainable but actually enjoyable :)