collaboration

the @cyxth/colab provides you with APIs to easily add real time collaboration to your application in minutes.

prerequisites

first make sure you have created a cyxth account and created a cyxth instance, and make sure to check out authorization and concepts.

next download @cyxth/core and @cyxth/colab packages

# cyxth core
npm install @cyxth/core

# cyxth colab
npm install @cyxth/colab

colab also requires the colab-wasm package. you can download this from npm too and add it locally to your project or use the cdn to load it on connect

This guide gives a high level overview and colab concepts, feel free to check the colab api reference and colab examples after this guide. for a deeper understanding of various topics.

initialization

colab is a cyxth core plugin and is instantiated like the other plugins to get the colab use colab() function from cyxth core module.

import Cyxth from '@cyxth/core';
import Colab from '@cyxth/colab';

const cyxth = new Cyxth("YOUR_APP_URL");
await cyxth.connect(USER_TOKEN);

cyxth.register([Colab]);
const wasmUrl = "https://cdn.cyxth.com/[email protected]";
const colab: Colab = await cyxth.colab(wasmUrl);

Ensure the user has been authenticated and connected to your cyxth instance before calling cyxth.colab(wasmUrl).

channels

colab channels extends the functionality of cyxth channels. all actions that can be done on a cyxth channel can be done on colab channels. users can create() , join() , manage and configure channels. we have went through this extensively in the concepts guide.

colab channels adds a load() functionality to sync state for users who have been offline and have enabled offline storage functionality. load can be used in place of create for users who are already part of the colab instance. this is useful when combined with the offline features.

data types

cyxth colab uses CRDTs (Conflict free Replicated Data Types) under the hood. CRDTs are data types that allow for independent mutation of shared state across multiple users while automatically resolving any inconsistency that may occur during any change. although users may have different states at a given time they are always guranteed to eventually converge on the same state. you can read more about crdts here.

cyxth colab implements various CRDTs in the colab-wasm package allowing you to change any state to a collaborative state.

  • values - a simple value (number, string or boolean)
  • maps - key value pairs
  • lists - an ordered sequence of elements
  • text - collaborative text editing type
  • counter - a simple counter with only increment & decrement operations

values, maps and lists can be nested together to form a tree 🌳

for example

{
    tasks: [
        {
            id: 'tsk-01-009',
            name:'write the docs',
            descrition: 'write colab docs'
        },
        {
            id: 'tsk-01-010',
            name:'update docs',
            descrition: 'update docs in v 0.0.6',
            tags: ["docs","tov1"]
        }
    ]
}

text and counter can not be deeply nested within other nodes since they are separate CRDTs with different change semantics.

the top level element a special tree that can contain a single tree and any number counters and text nodes. the keys are always strings. note text or counter keys can have the same names as tree nodes but must be explicitly accessed as text or counter.

i.e

// accessing various data types with same key
let ctx: Context = colab.getChangeContext();

// text
ctx.text('docs').insert(0,"hello world");

// counter
ctx.counter('docs').inc();

// tree (functional)
ctx.tree()
    .path('docs')
    .list()
    .push({id:'doc',description:'doc desc' })

// ..or (direct change)
let state: {docs : DocDescs[]} = ctx.getTreeContext();
state.docs.push({{id:'doc',description:'doc desc' }})
// state.docs[0].id = 'updated doc'

lets go through the various operations supported by these data types in the change context

change context

the main way to interface with the underlying CRDT is through colab’s getChangeContext() function. getChangeContext() returns a Context which provides various methods to easily change state.

let ctx: Context = colab.getChangeContext();

// insert into a text node
ctx.text('docs').insert(0,"hello world");

text

the text(key) function return a Text node at key for collaborative text editing similar to google docs but using CRDTs thus a better support for offline editing. multiple users can edit the same document at the same time and it will always be consistent.

the text node provides 3 main methods insert() , delete() and value().

insert(index, text) - insert text value at a given index.

delete(index, length) - delete text of a given length starting at a given index .

value() returns the text value.

let ctx: Context = colab.getChangeContext();

// insert into a text node
ctx.text('docs').insert(0,"hello world"); //hello world

// delete "hello "
ctx.text('docs').delete(0,6) //world

let val = ctx.value() //"world"

with these simple functions combined with user presence broadcasts like user selection, cursor positions or even voice|video|normal chat you have a great colaborative text editing. check out this great text editing example application in our demos

counter

a simple increment decrement Counter with methods inc(), dec() and value().

let ctx: Context = colab.getChangeContext();
ctx.counter('clicks').inc() // increment
ctx.counter('clicks').inc()
let val = ctx.counter('clicks').value() // 2

ctx.counter('clicks').dec() // decrement
let val = ctx.counter('clicks').value() // 1

tree

a key value map that can be deeply nested with other maps or lists, lists can be deeply nested with other maps or lits too. this data structure gives the flexibility to easily represent your application state to cyxth collaborative state which roughly has the format below (rust btw 😆).

/// a value can be number, string or bool
enum Value{
    /// number
    Number(f32),
    /// string
    String(String),
    /// boolean 
    Boolean(bool)
}

/// a list of elements
struct List(Vec<Element>);

/// tree a map of elements
struct Tree(Map<String, Element>)

/// an element can either be a simple value, a list or a tree
enum Element {
    /// value
    Value,
    /// list
    List,
    /// tree
    Tree
}

the tree function returns a Tree that provides the following methods to easily modify a tree.

path(keyPath) returns a nested tree in path .i.e path(['tasks',0]) returns the first task in tasks list.

update(value) update element at path put element if not exists. if the value is an object partial values can be provided too

delete() delete element at path

value() return value at path

list() returns a List at path for list specific operations such as insert, push, pop etc…

these methods can be chained together.

here is an example showing all the methods in use


let ctx: Context = colab.getChangeContext();

// insert in a list
ctx.tree()
    .path('tasks')
    .list()
    .push({id:'task0',description:'teski u1' })

// update 
ctx.tree().path(["tasks",0,"title"]).update("new task title")
//... same as
ctx.tree()
    .path("tasks")
    .list()
    .index(0)
    .update({"title": "new task title"})

// delete task 1
ctx.tree().path(["tasks",0]).delete()
// ..same as
ctx.tree().path("tasks").list().index(0).delete()

chaining the operations can be very long for deeply nested trees. and if you are using typescript this loses the types. using getTreeContext() gives an interface to change state directly similar to how you do javascript.

let ctx: Context = colab.getChangeContext();
let state: {tasks : Task[]} = ctx.getTreeContext();

/// push to list
state.tasks.push({id:'task0',description:'teski u1' })

// update
state.tasks[0].title = "new task title"
// partials
state.tasks[0].update({title: "new task title"})

// insert element in list
state.tasks.insert(1, {id:'docify',description:'update docs' })

// delete task 0
state.tasks[0].delete()

Note though the getTreeContext() interface gives you an interface similar to javascript it is still a cyxth colab tree so most javascript methods won’t actually work. this inteface also adds a delete() and update() to any value supported by cyxth.

change event

using the on("change") function in colab you can listen for all changes happening in colab instance. this is very important and is required to ensure the state shown to the user is always consistent with all other. the change event will also return local changes which can all be applied in one place check the is isLocal() method. you can detect that an event has been skipped using the event sequence and either replay missing events or use the latest state.

users can be temporarily out of sync for example if a user goes offline or then network state changes. since cyxth colab is based on CRDTs they will be eventually consitent when the user comes back online and state will be as if the user never left, NOTE no change is lost even the changes made when offline.

colab.on("change", (changeEvent) => {
    let {userId, action, change,sequence} = changeEvent;
    //... handle action
})

check the ChangeMap for all change events you can listen for

presence

Intent is key for a fluid collaboration experience, letting other users know which action a user is about to take immensly improves the overall user experience while making every thing predictable to users collaborating on the same piece of state.

examples include showing mouse cursor positions and object selections in a design app, user position in a text document or text selection

using the colab presence() method you can send any user intent to all connected users and listen for on presence event to handle it. You can take this a step further with cyxth calls voice chat or integrate chat for an even better experience.

here is an example from the tasks demo

// send task selection in the presence channel
colab.presence({
    ty: "task-selection",
    indices: [1,2,4]
});

// listen on the presence channel
colab.on("presence",(pr: PresenceData) => {
    let {data, userId} = pr;

    if (data.ty === "task-selection"){
        // ...show selected tasks by user in ui
    }
})

more events

all events on cyxth channels such as user:join and user:leave are also available for cyxth colab channels. colab adds presence and change events. presence is for user intent sharing as discussed above and change is for the data changes in real time. you have to listen for change event to get collaboration going.