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.