Documents (CRuntime and AbstractDoc)

In Collabs, a document is a “unit of collaboration” - some collaborative state that is shared together. This could be a single shared whiteboard, a rich-text document, a recipe, etc.

Each document comes in two parts:

Using CRuntime

To use a document, first construct a CRuntime:

import { CRuntime } from "@collabs/collabs";

const doc = new CRuntime();

Next, “register” its Collabs using CRuntime.registerCollab. E.g. for a shared whiteboard document:

const boardState: CValueMap<[x: number, y: number], Color> = doc.registerCollab(
  "boardState",
  (init) => new CValueMap(init)
);
// Other calls to doc.registerCollab...

There are a few weird things going on in this registerCollab call:

  1. You have to give a name to your Collab - here "boardState". It must be unique among all calls to doc.registerCollab but is otherwise arbitrary. We suggest using the same name as the Collab’s variable, like here.

  2. Instead of constructing the CValueMap directly, you supply a callback that does so: (init) => new CValueMap(init). Internally, registerCollab will invoke this callback and return the constructed Collab. That lets doc configure boardState.

Registering Collabs essentially defines a “schema” for your document. That schema tells CRuntime how to interpret updates from remote collaborators and persistent storage.

To ensure that they can understand each other, collaborators must all use the same “schema”. Specifically, they must make the same calls to registerCollab, with the same names, Collab classes, and Collab constructor arguments. This is easy to guarantee if they all run the same code.

Once you register all Collabs, your document is ready to use. You can handle changes, connect providers, and perform collaborative operations on its Collabs.

Using AbstractDoc

In the above example, our document consisted of two variables: doc of type CRuntime, and boardState of type CValueMap. If you have many Collabs in the same document, or many documents in the same app, it becomes convenient to group each document into a single object.

You can do that by extending the AbstractDoc class, as shown below.

import { AbstractDoc, DocOptions } from "@collabs/collabs";

class MyWhiteboardDoc extends AbstractDoc {
  /** The whiteboard's state, exposed publicly for convenience. */
  readonly boardState: CValueMap<[x: number, y: number], Color>;

  constructor(options?: DocOptions) {
    super(options);

    // this.runtime is a CRuntime provided by our superclass.
    this.boardState = this.runtime.registerCollab(
      "boardState",
      (init) => new CValueMap(init)
    );
  }
}

The AbstractDoc superclass gives MyWhiteboardDoc a similar API to CRuntime - in particular, you can still pass it as an argument to providers. But it hides internal methods like registerCollab, and subclassing lets you encapsulate your Collabs in a type-specific API. (For example, you could make boardState private and instead expose getPixel and setPixel methods.)

Multiple Documents

You can use as many documents as you like in a single app. For example, in a sticky notes app, each sticky note could have its own document. That way, the user can choose to share each sticky note with a different group of collaborators. Or, you can wait to load each sticky note from storage until it is scrolled into view.

Our network and storage providers use docIDs (arbitrary strings) to keep track of which documents are supposed to be kept in sync. For example, when using our Websocket client and server, documents on different devices that subscribe to the same docID will be kept in sync via the server.

Next Steps

Continue following the Guide with Handling Changes.