Racer

repository·master·Indexed 22 days ago

https://github.com/derbyjs/racer

A realtime model synchronization engine for Node.js (v2.3.1) that enables multiple users to interact with shared data using Operational Transformation (OT). Built on ShareDB, Racer provides a unified model interface for server and client environments, supporting offline capabilities, automatic conflict resolution, and persistent storage via MongoDB and Redis.

Tokens
8.1K
Snippets
32
Records
47
Agent score
76%

What's inside racer

  1. What is Racer?

    master

    Racer is a realtime model synchronization engine for Node.js. It uses ShareDB and Operational Transformation (OT) to allow multiple users to interact with the same data in realtime with sophisticated conflict resolution.

    Key capabilities include:

    • Realtime updates: Model methods propagate changes between browser clients and Node servers automatically.
    • Realtime query subscriptions: Clients can subscribe to specific documents or query sets (currently supporting arbitrary Mongo queries).
    • Conflict resolution: Uses JSON Operational Transformation to ensure eventual consistency across clients.
    • Immediate interaction & Offline support: Model methods take effect locally immediately, allowing for offline work that automatically syncs upon reconnection.
    • Unified interface: The same model interface works on both the server (for initial rendering) and the client (for interaction).
    • Persistent storage: Uses ShareDB to journal operations and persist documents, primarily via MongoDB.
  2. How Racer handles realtime synchronization

    master

    Racer achieves realtime synchronization through a combination of local optimistic updates and server-side conflict resolution:

    1. Immediate Local Execution: When a model method is called, it appears to take effect immediately on the local client. This enables a responsive UI and supports offline usage.
    2. Server Synchronization: Racer sends the update to the server and checks for conflicts using ShareDB's JSON Operational Transformation algorithm.
    3. Conflict Resolution: If a conflict is detected, Racer emits events to bring all client states into eventual consistency.
    4. Callbacks: In addition to synchronous API calls, model methods provide callbacks that allow you to handle the resolved state after the server has responded.
  3. Install Racer

    master

    To install Racer, use npm. Ensure you have Node v16, MongoDB, and Redis running on your machine, as Racer relies on ShareDB which requires these services for data persistence and PubSub.

    $ npm install racer
  4. Understand ShallowCopiedValue and partial immutability

    master

    The ShallowCopiedValue<T> type is used when you want top-level properties of an object to remain mutable, but want all nested arrays or basic objects to be treated as ReadonlyDeep.

    Warning: Like ReadonlyDeep, this does not guarantee runtime immutability; it is a type-level hint for static analysis.

  5. Use eventContext to manage groups of listeners

    master

    The eventContext(id) method allows you to create a child model that tags all event listeners registered on it with a specific id. This is useful for grouping listeners (e.g., during a component render) so they can be removed all at once.

    1. Create a scoped model: const scopedModel = model.eventContext('my-context-id');
    2. Register listeners on scopedModel.
    3. Remove all those listeners later using model.removeContextListeners(); (called on the root model or the model that owns the context).

    Note: This is distinct from the context(contextId) method.

    const scopedModel = model.eventContext('component-render-123');
    
    // This listener is now tagged with 'component-render-123'
    scopedModel.on('change', 'some.path', (event) => {
      // ...
    });
    
    // Later, remove all listeners tagged with 'component-render-123'
    model.removeContextListeners();
  6. How contexts work in Racer

    master

    Contexts are used to track the origin of data fetches and subscriptions. By grouping data loading under a named contextId, you can unload all data associated with that specific context at once, rather than manually tracking and unloading every individual document or query.

    Contexts exist in a global namespace for each root model. This means if you call .context('my-context') from different parts of your application, they will all refer to the same underlying context. This allows different components to share a lifecycle for a specific set of data.

  7. Understand the Model hierarchy: RootModel and ChildModel

    master

    Racer uses a hierarchical model structure to manage data and connectivity:

    1. RootModel: The top-level model that holds all application data and maintains connection information (via backend and connection). It serves as the source of truth and the entry point for the application's data layer.
    2. ChildModel: A model representing a subset of the data. Child models are lightweight and share properties with their parent via the root reference. This allows for efficient inheritance and extensibility.
    3. Model: The base class for both RootModel and ChildModel. It provides core properties like data and root.

    To create a child model from an existing model instance, you can use the internal _child() mechanism (though in practice, developers typically instantiate ChildModel by passing the parent model to its constructor).

  8. Configure event listener captures with wildcards

    master

    When using { useEventObjects: true }, Racer provides a captures array containing the segments of the path that matched your wildcards (* or **).

    • If you use *, the segment at that position is added to the captures array.
    • If you use ** at the end of a pattern, the remaining part of the path is added as a single string to the captures array.

    Example pattern: notes.*.author.** on path notes.abc.author.name results in captures: ['abc', 'name'].

  9. How Collection and Doc management works

    master

    In Racer, data is organized into Collections, which contain multiple Docs (documents).

    • Collection: A named container for documents. It tracks its own size and manages the lifecycle of its documents. When the last document is removed from a collection via collection.remove(id), the collection itself is destroyed.
    • Doc: An individual unit of data within a collection, identified by a unique id.

    You can interact with these via the Model instance (e.g., model.getCollection(name) or model.getDoc(name, id)) or directly if you have a reference to a Collection object.

  10. Understand ReadonlyDeep and immutability in Racer

    master

    Racer uses ReadonlyDeep<T> to provide static type-checking for immutable data. This type transforms a JSON-compatible type T so that it and all its nested arrays or objects are marked as readonly.

    Important Warnings:

    • Not Runtime Immutability: This only affects TypeScript static analysis. Values can still be modified at runtime via any casting, untyped JavaScript, or functions with any signatures.
    • Class Instances: Most class instances can still be modified via their methods. Built-in Arrays are transformed into ReadonlyArrays with no mutator methods.
    • How to mutate: To get a fully mutable copy of a ReadonlyDeep value, use the deepCopy(value) function.
  11. Listen to Racer model events with path patterns

    master

    You can listen to specific mutation events (like change, insert, remove, etc.) on a Model using the .on() or .once() methods. These methods support path patterns to filter events based on the part of the model being modified.

    Path Pattern Syntax

    • Direct path: 'notes.abc-123.author' triggers only on direct modifications to that specific property.
    • Single segment wildcard (*): 'notes.*.author' triggers on any note's author, but not on sub-properties of the author or if the entire note is replaced.
    • Multi-segment wildcard (**): 'notes.*.author.**' triggers on the author or any of its sub-properties. Note that ** must only appear at the end of a pattern.

    Using useEventObjects: true

    By default, Racer uses a legacy var-args style for event listeners. To use the modern, cleaner API, pass { useEventObjects: true } in the options. This changes the listener signature to receive a single event object and an array of captures (the segments matched by wildcards).

    // Modern API with useEventObjects
    model.on('change', 'notes.*.author.**', { useEventObjects: true }, (event, captures) => {
      console.log('New value:', event.value);
      console.log('Captured segments:', captures);
    });
    
    // Legacy API (default)
    model.on('change', 'notes.*.author', (capture1, capture2, value, previous, passed) => {
      // ...
    });
    model.on('change', 'notes.*.author.**', { useEventObjects: true }, (event, captures) => {
      console.log('New value:', event.value);
      console.log('Captured segments:', captures);
    });