Orbit Documentation

repository·main·Indexed 25 days ago

https://github.com/orbitjs/orbit

Orbit is a composable data framework and client-side ORM for building offline-first web applications. It provides a flexible data access and synchronization layer capable of interacting with REST servers, WebSocket streams, IndexedDB, and in-memory stores. Key features include unified data modeling, deterministic change tracking, immutable state management, and a Coordinator class for managing sources and strategies such as ConnectionStrategy, RequestStrategy, and SyncStrategy.

Tokens
51.5K
Snippets
75
Records
288
Agent score
80%

What's inside Orbit

  1. What is Orbit?

    main

    Orbit is a composable data framework designed for managing complex web application data needs. While primarily used as a flexible client-side ORM, it is also compatible with Node.js for server-side use.

    Key capabilities include:

    • Multi-source interaction: Seamlessly work with REST servers, WebSocket streams, IndexedDB, or in-memory stores.
    • Offline-first support: Transition smoothly between offline and online modes.
    • Flexible UX patterns: Support both optimistic and pessimistic user experiences.
    • Unified Data Modeling: Use a common schema and a consistent set of query/update expressions regardless of the underlying data source.
    • Deterministic Change Tracking: Track changes and support undo/redo functionality.
    • Immutable State Management: Fork immutable stores and merge changes back when ready.
  2. What is Orbit.js?

    main

    Orbit is a framework designed to orchestrate access, transformation, and synchronization between disparate data sources. It is written in TypeScript and distributed via npm. Most Orbit packages are isomorphic, meaning they can run in both modern web browsers and the Node.js runtime.

    Key capabilities include:

    • Optimistic and pessimistic UX patterns.
    • Pluggable sources with common interfaces.
    • Connection durability via request queuing and retries.
    • Application durability by persisting transient state.
    • Warm caches available immediately on startup.
    • Custom request coordination (priority and fallback plans).
    • Branching and merging of data caches.
    • Deterministic change tracking and undo/redo support.
  3. Overview of Orbit Core Libraries

    main

    Orbit is a modular ecosystem distributed via the @orbit npm organization. The core functionality is split into several specialized packages that handle asynchronous tasks, data modeling, synchronization, and persistence.

    Core Primitives

    • @orbit/core: Provides the foundation for asynchronous task processing, including an event system, a task queue, a change log for tracking history, and a bucket interface for state persistence.
    • @orbit/data: Applies core primitives to data sources. It introduces the Source base class, Transform (transactional mutations), and Query (data requests) abstractions.
    • @orbit/records: Extends @orbit/data specifically for record-based models. It provides RecordSchema for defining attributes and relationships, RecordSource, and specialized operations like addRecord or findRecord via chainable builders.
    • @orbit/coordinator: Manages data flow and synchronization strategies between different Orbit Data sources.
    • @orbit/identity-map: Manages model instances using a simple identity map.
    • @orbit/serializers: Provides base classes for serializing and deserializing data types.
    • @orbit/validators: Contains utilities for validating primitive data and building higher-order validators.
  4. Understand Orbit Data Sources

    main

    Sources provide access to data in Orbit and vary in capability. Some support updating or querying records, while others only broadcast changes.

    Orbit provides several standard sources:

    • @orbit/memory: An in-memory source.
    • @orbit/jsonapi: A JSON:API client.
    • @orbit/indexeddb: For accessing IndexedDB.
    • @orbit/local-storage: For accessing LocalStorage.

    Custom sources can be implemented to connect to any data provider. All sources must be instantiated with a RecordSchema to understand the domain-specific data they manage.

  5. Understand Orbit primitives

    main

    Orbit's architecture is built on several core primitives that allow for composable and interchangeable data management:

    • Source: Represents any data provider (e.g., in-memory store, IndexedDB, or a REST server). Sources vary in capability, such as supporting updates, queries, or simply broadcasting changes.
    • Transform: A set of record mutations or "operations" that must be applied atomically (all succeed or all fail).
    • Operation: A single mutation within a Transform.
    • Query: An interrogation of source contents composed of one or more QueryExpressions.
    • Log: A history of transforms applied to a source.
    • Task: An asynchronous, serial action performed on sources (e.g., an update request or a query).
    • Bucket: Used to persist application state, such as change logs and queued tasks.
    • Coordinator: The declarative "wiring" of the application. It observes sources and applies coordination strategies to handle synchronization, logging, and error handling.

    Record-specific primitives

    Orbit uses a normalized data format called records.

    • Record: An entity with a unique identity established by a type and id. Records can contain attributes and relationships.
    • RecordSchema: Defines all models within a specific domain.
    • ModelDefinition: Defines the specific characteristics for records of a given type.
  6. How transforms and operations work to update data

    main

    Data in an Orbit source is updated by applying a Transform. A transform consists of one or more Operations, where each operation represents a single atomic change to a record or a relationship (e.g., adding a record, updating a field, or deleting a relationship).

    Key Concepts:

    • Atomicity: Transforms must be applied atomically—either all operations within a transform succeed, or they all fail together.
    • Operations: The building blocks of a transform. They define specific actions like addRecord, updateRecord, or replaceAttribute.
    • Transforms: A collection of operations identified by a unique id. You typically use a TransformBuilder to construct these rather than creating them manually.

    To apply changes, use a source's update or push method and pass in a builder function.

  7. Manage record identity and mapping local IDs to remote keys

    main

    A record's identity is defined by the combination of its type (string) and id (string). There are three common strategies for managing identity:

    1. Auto-generate IDs locally: Use UUIDs locally and remotely. This is the simplest approach.
    2. Reference remote IDs: Only use IDs generated by the server. This works if you don't need to create new records locally.
    3. Map local IDs to remote keys: Auto-generate IDs locally and map them to canonical IDs (keys) generated remotely. This is the most flexible for complex applications.

    When using the third approach, store remote IDs in a keys object at the root of the record. You can use a RecordKeyMap to manage these mappings.

    {
      type: 'planet',
      id: '34677136-c0b7-4015-b9e5-57f6fdd16bd2',
      keys: {
        remoteId: '123456'
      }
    }
  8. How the Coordinator abstraction works

    main

    A Coordinator is a high-level abstraction in Orbit that manages a set of sources by applying coordination strategies to them. Instead of manually attaching event handlers to every source, you use a coordinator to orchestrate interactions between sources.

    Key benefits include:

    • Preconfigured Strategies: Easily apply complex behaviors like event logging or log truncation.
    • Lifecycle Management: You can activate or deactivate all strategies at once using coordinator.activate() and coordinator.deactivate(), which helps prevent memory leaks.
    • Unified Configuration: Coordinators can share a single logLevel across all attached strategies.
    import Coordinator from '@orbit/coordinator';
    
    const coordinator = new Coordinator({
      sources: [memory, backup],
      strategies: [backupMemorySync]
    });
  9. Understand Query Expressions in Orbit

    main

    Orbit uses QueryExpression objects to interrogate the contents of a source. A query expression is defined by an op (operation) and optional options. Standard record-specific operations include finding single records, finding related records, or finding collections of records based on type.

    Common standard operations include:

    • findRecord: Locates a specific record by its identity.
    • findRelatedRecord: Locates a single record via a specific relationship from a base record.
    • findRelatedRecords: Locates multiple records via a relationship (supports sorting, filtering, and pagination).
    • findRecords: Locates all records of a specific type (supports sorting, filtering, and pagination).
    interface FindRecord extends QueryExpression {
      op: 'findRecord';
      record: RecordIdentity;
    }
    
    interface FindRelatedRecord extends QueryExpression {
      op: 'findRelatedRecord';
      record: RecordIdentity;
      relation: string;
    }
    
    interface FindRelatedRecords extends QueryExpression {
      op: 'findRelatedRecords';
      record: RecordIdentity;
      relation: string;
      sort?: SortSpecifier[];
      filter?: FilterSpecifier[];
      page?: PageSpecifier;
    }
    
    interface FindRecords extends QueryExpression {
      op: 'findRecords';
      type?: string;
      sort?: SortSpecifier[];
      filter?: FilterSpecifier[];
      page?: PageSpecifier;
    }
  10. Manage multiple sources with a Coordinator

    main

    A Coordinator manages a set of sources and applies coordination strategies. This is a higher-level abstraction than manual event handling.

    Using a Coordinator allows you to:

    • Use preconfigured strategies like SyncStrategy.
    • Activate or deactivate all strategies at once using .activate() and .deactivate().
    • Share a single log level across all strategies.

    To use it, create a Coordinator with your sources, add strategies (like SyncStrategy), and call await coordinator.activate().

    import { Coordinator, SyncStrategy } from '@orbit/coordinator';
    
    const coordinator = new Coordinator({
      sources: [memory, backup]
    });
    
    const backupMemorySync = new SyncStrategy({
      source: 'memory',
      target: 'backup',
      blocking: true
    });
    
    coordinator.addStrategy(backupMemorySync);
    await coordinator.activate();
  11. Structure Orbit records using JSON:API

    main

    Orbit records are lightweight, serializable Plain Old JavaScript Objects (POJOs) that follow the JSON:API specification. A record must contain type and id to establish identity, and can optionally contain attributes, relationships, and keys.

    Important Constraints:

    • All fields in a record share the same namespace and must be unique.
    • You cannot have an attribute or relationship with the same name as another field.
    • You cannot name an attribute or relationship type or id.
    {
      type: 'planet',
      id: 'earth',
      attributes: {
        name: 'Earth',
        classification: 'terrestrial',
        atmosphere: true
      },
      relationships: {
        solarSystem: {
          data: { type: 'solarSystem', id: 'theSolarSystem' }
        },
        moons: {
          data: [
            { type: 'moon', id: 'theMoon' }
          ]
        }
      }
    }
  12. How source method events work

    main

    Interfaces like Updatable and Queryable follow a consistent event pattern. For any method x, the source emits three specific events:

    1. beforeX: Emitted before the method is processed. Listeners can return a Promise to block processing. If a listener's promise fails, the source emits xFail and stops processing.
    2. x: Emitted after the method is successfully processed.
    3. xFail: Emitted if an error occurs during the beforeX phase or during the internal processing of x.

    Additionally, any mutations caused by calling x will trigger the general transform event.

    Important Lifecycle Detail: While a failure in a beforeX listener can block the method, failures in listeners attached to the transform or x events will not prevent further processing.