Custom Types

Kevin Jahns edited this page Aug 23, 2016 · 5 revisions

Note: This documentation only applies to Yjs 0.5. It is hard to update these docs on a regular basis, so please feel free to contact me if you want to implement a custom type.

In the past, OT friends put a lot of effort in creating a library of operation types for different types of data. The ottypes organization holds repositories for JSON, Text, and Rich Text, that can be used by any compatible OT engine (e.g. ShareJs, or OTjs). This is a step in the right direction to make shared editing more applicable in different scenarios. But, as nearly 30 years of research show, implementing OT operations that really fit your needs is very complex and error prone.

The Yjs approach is to compose your own custom types out of bits and pieces that are known to work correctly, rather than reinventing the wheel in a different manner. Currently, you can use the ListManager, and the MapManager to build custom types. I'm going to add more operations for special scenarios in the future. Still, I claim that you can create arbitrary complex data types with these two operations!

Existing operations

I strongly try to distinguish between data type and data structure. This will really help to understand what is going on.

Data Structure

The data structure denotes to the way that data is organized, in order to use it efficiently. Every data structure has advantages and disadvantages with respect to space complexity, and time complexity for specific operations that can be executed on the data.

Data Type

A data type is a classification of properties that can be assigned to data. A data type may support specific methods on the data, whereby the representation and implementation is abstracted. A data type example is the Object in Javascript: There are several data structures that can realize an Object, and every browser may implement it differently. But in the end, every implementation has the same functionality.

It is sometimes hard to distinguish between data structure, and data type. But we will say the following: A custom type is a data type, and it is realized with operations (Map-/ListManager) - the data structure. I won't go to much into detail on how the operations are realized. But I'll show you what functionality they provide, and how you can use them. If ever used custom types in Yjs, the operations will seem very familiar to you ;)

ListManager

Manages list structures. The Y.List is actually a ListManager with some extra functionality. You can insert any serializable Object, or custom type in the ListManager. The inserted element is hold by an Insertion which has a predecessor and successor. Furthermore, you can address elements also by its position in the list. Since a ListManager can contain other ListManagers (even a reference to itself), you can create trees and graphs with it as well!

Method Description
.insert(position,contents) Insert a set of content at a position. This expects that contents is an array of content. NOTE: In the Y.List, insert expects only a single element!
.push(content) Insert content at the end of the list
.delete(position,length) Delete content. The length parameter is optional and defaults to 1
.val() Retrieve all content as an Array Object
.val(position) Retrieve content from a position
.observe(f) The observer is called whenever something on this list changed. (throws insert, and delete events)
.unobserve(f) Delete an observer
MapManager

A MapManager is like a Javascript Object. The Y.Object actually provides the same functionality as the MapManager. It is a key/value store on which you can put any serializable Object, or custom type. If two or more users set a value with the same key, then only one value will prevail. Therefore, the MapManager also provides some semantics for Replacement functionality.

Method Description
.val() Retrieve all properties of this type as a JSON Object
.val(name) Retrieve the value of a property
.val(name, value) Set/update a property.
.delete(name) Delete a property
.observe(observer) The observer is called whenever something on this object changes. Throws add, update, and delete events
.unobserve(f) Delete an observer
Complexity

Every operation on the operations is executed in constant time, and will create one message that is propagated to the other users.

Create a Custom Type

The Javascript world came up with several approaches on how to create classes. I strongly recommend to create custom types with a function as a constructor, and methods added via the prototype. E.g. like this:

// Function as a constructor
function MyCustomType(){
}

// Methods added on the prototype
MyCustomType.prototype.getBanana = function(){
  return "banana";
}

// Yjs requires that custom types are created with the *new* keyword!
custom_type = new MyCustomType()

A custom type must have the following properties

Property Description
._name Defines the name of your custom type. Furthermore, Yjs expects that you save your custom type on the Y Object with this keyname. E.g. Y[_name] = MyCustomType
._getModel(Y,Operations) A custom type is modelled by an operation (i.e. the Map-/ListManager). Yjs expects an instantiated, and executed operation here. The MapManager, and the ListManager are stored on the Operations object. Y is the currently used Y object (you don't have it on global scope if you use Yjs as a NodeJs module)
._setModel(operation) Yjs sets an model here. Yjs expects that you save the model as this._model. You may want to initialize your type, or set your observers here.

There are two parties who can instantiate (with the new keyword) a custom type: Either Yjs, when it receives a message from another collaborator, or you. You probably already read how to instantiate a custom type

// create an instance
var my_type = new Y.MyCustomType(1, 2, 3, "more parameters");

// As soon as you put it on the Y object, it will be send to the other users.
// Internally, Yjs will call `my_type._getModel(Y, Operations)`,
// in order to get the model here.
y.val("my_type",my_type);

When Yjs instantiates a custom type, it will create it with no parameters. It is important, that your custom type does not expect parameters in the constructor! But if you really need to throw an error, when a user sets no parameters, you can throw them in the _setModel method (this will only be called if the user create the custom type)

new Y.MyCustomType()._setModel(received_model)

Example

So lets create a basic Y.List type

/*
  Manage list data with this shareable list type.
  You can insert and delete arbitrary objects
  (also custom types for Yjs) in the list type.

  We give the function an explicit name, so we get more meaningful error messages.
*/
Y.List = function YList(list){
  // You can instantialize the List with an existing Array.
  // Save it until _getModel is called!
  if(list == null){
    this._list = [];
  } else if(list.constructor === Array){
    this._list = list;
  } else {
    throw new Error("Y.List expects an Array as a parameter!")
  }
}

/*
  This is required by Yjs. @Note: Remember to change `_name`
  if you change the name of the function
*/
Y.List.prototype._name = "List"

/*
  This is called by Yjs if this List is instantiated by the user/you (not Yjs)
*/
Y.List.prototype._getModel = function(Y, Operation){
  if(this._model == null){
    // create a ListManager
    this._model = new Operation.ListManager(this).execute();
    // Note: You can create a MapManager the same way:
    // this._model = new Operation.MapManager(this).execute()

    // insert the arguments from the constructor
    this._model.insert(0, this._list)

    // and delete them
    delete this._list;
  }
  return this._model;
}


/*
  This is called by Yjs, if this is created by another collaborator.
*/
Y.List.prototype._setModel = function(model){
  delete this._list; // delete the saved argument from the constructor
  this._model = model;
}

/*
  Insert an element at a position.
  Note: If you create your own types, you should check the parameters by yourself!
*/
Y.List.prototype.insert = function(position, content){
  if(typeof position === "number"){
    this._model.insert(position, [content])
  }
}

/*
  Delete one element at a position.
*/
Y.List.prototype.delete = function(position){
  if(typeof position === "number"){
    this._model.delete(position, 1)
  }
}

/*
  Delete one element at a position.
*/
Y.List.prototype.observe = function(f){
  if(f.constructor === Function){
    // do you want the standard events? Don't you think they suck?
    // Proxie the events here, so that they fit your needs.
    this._model.observe(f);
  }
}
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.