Gotchas

Things that might go wrong

Collabs is designed to make collaboration “just work”. However, it’s possible to misuse the library in ways that cause inconsistencies or errors. This page lists some gotchas to watch out for.

Varying Initialization

All in-sync copies of a Collab must be initialized identically. In particular:

  1. All copies of a document must make the same calls to CRuntime.registerCollab, with the same names, Collab classes, and Collab constructor arguments (in the init callback)

  2. Likewise for calls to registerCollab within a CObject.

  3. Constructor arguments, and the logic inside Collab constructors or valueConstructor callbacks, must not depend on values that can differ across users - e.g., the user’s current time, random numbers, or CRuntime.replicaID.

So long as all users are running the same code, 1 and 2 should be automatic.

Delayed Initialization

All of a document’s CRuntime.registerCollab calls must happen before you use that document in any way: connecting providers, performing collaborative operations on its Collabs, or calling other CRuntime/AbstractDoc methods.

To ensure this, you should make all CRuntime.registerCollab calls right after constructing the CRuntime. (In an AbstractDoc subclass, make all calls in its constructor.)

Likewise, within a CObject, you should make all calls to this.registerCollab within the constructor. If you need to add children dynamically, you should instead use a collection of Collabs, e.g., CSet.

Non-unique Names

All names passed to CRuntime.registerCollab must be unique. Likewise, within a CObject, all names passed to this.registerCollab must be unique. A simple way to ensure this is to use the corresponding variable’s name. Note that there is a network cost to longer names, so you might instead prefer to assign names in order of length (“”, “0”, “1”, etc.).

Non-Serializable Types

Not all values can be serialized by default (e.g., functions). Also, some values might not deserialize the way you expect - e.g., an object reference on one user is meaningless to other users, so by default, objects are deserialized as deep clones of the input object.

You can work around serialization issues by using a custom Serializer, which many Collabs accept as a constructor option.

Collection Equality Semantics

Values in CValueSet, and keys in CValueMap, CMap, and CLazyMap, are compared using “serialization equality”: two values/keys are considered equal if they serialize to equal Uint8Arrays. By default, this is effectively a deep-equals comparison, not JavaScript’s usual === comparison.

For example, if you add distinct but deep-equal objects to a JavaScript Set, you’ll get a set of size 2:

const set: Set<any>;
set.add({ foo: "bar" });
set.add({ foo: "bar" });
console.log(set.size); // 2

If you do the same to a Collabs CValueSet, you’ll get a set of size 1:

const set: CValueSet<any>;
set.add({ foo: "bar" });
set.add({ foo: "bar" });
console.log(set.size); // 1

Treating Events as Consistent

Although the state of a Collab is eventually consistent, the events that it emits are not. Each user sees events describing their own view of how the state changed over time; this can differ across users if they receive updates in different orders.

Examples:

  • When using a CValueSet, one user calls set.add("foo") while another concurrently calls set.add("bar"). Then some users will see an “Add” event for “foo” followed by an “Add” event for “bar”, while others see the opposite order.

  • When using a CVar, one user sets theVar.value = "foo" while another concurrently sets theVar.value = "bar". Suppose “foo” wins under the arbitrary tiebreaker rule. Then users who receive the “bar” message and then the “foo” message will see two “Set” events: one for “bar”, then one for “foo”. Meanwhile, users who receive the “foo” message and then the “bar” message will only see one “Set” event, for “foo”.

Treating Iterator Orders as Consistent

CSet, CValueSet, CMap, CLazyMap, and CValueMap iterators might not yield elements in the same orders on different users, even when the set/map states are consistent. Instead, the iterators will yield elements in the order they were added/set locally. If you need a consistent iterator order, either use a CList/CValueList, or sort the collection manually.

Operations in Event Handlers or Initializers

Do not perform Collab operations in event handlers or initializers (including Collab constructors and collection valueConstructor callbacks. These operations will end up running on each user as collaborative operations, i.e., each user will broadcast a copy of the operation to every other user. So the operation will be performed (# users) times in total - probably not what you want.

If Collabs detects this, it will throw an error:

Error: CRuntime.send called during a receive/load call; did you try to perform an operation in an event handler?

You might be tempted into doing this because you are trying to set the initial value of a Collab - e.g., inserting placeholder text in a new document. See Initial Values for safe ways to do this.

Adding Event Handlers during Collection Events

To handle events on a Collab within a collection of Collabs, you should register event handlers within the valueConstructor callback, not during the new value’s “Add”/”Set”/”Insert” event.

Those events might not run exactly once per Collab instance - e.g., restoring an archived CList value will emit a second “Insert” event for that value. So you could get duplicate or missing event handlers on the values.

Misusing InitTokens

Do not make your own InitTokens (the init argument passed to each Collab’s constructor).

Only use a given InitToken once, in the way intended by its creator. E.g., a collection’s valueConstructor must return the Collab created using its init parameter.

In a custom Collab’s constructor, only use the InitToken in your super(init) call.

Expecting Strong Consistency (Database-Style Transactions)

Do not attempt operations that require strong consistency, e.g., transferring money between bank accounts. Strong consistency is impossible within the library, and any workaround you find will be flawed. Instead, you should either change your application so that it only requires eventual consistency, or use an external service for such operations (e.g., a dedicated server to manage account balances).

See also: CAP theorem.

Next Steps

You’ve now finished the Guide and learned Collabs’s main concepts!

To learn even more, check out the other sections on this site:

Or, ask questions in our Matrix chat room.