support for circular structures (e.g. with JSON)
This commit is contained in:
parent
fea6de3bf9
commit
3ba89edf7d
@ -7,7 +7,7 @@ Yjs is a framework for optimistic concurrency control and automatic conflict res
|
|||||||
|
|
||||||
In the future, we want to enable users to implement their own collaborative types. Currently we provide data types for
|
In the future, we want to enable users to implement their own collaborative types. Currently we provide data types for
|
||||||
* Text
|
* Text
|
||||||
* Json
|
* Json (even circular structures)
|
||||||
* XML
|
* XML
|
||||||
|
|
||||||
Unlike other frameworks, Yjs supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
|
Unlike other frameworks, Yjs supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -132,6 +132,22 @@ module.exports = function() {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Operation.prototype._encode = function(json) {
|
||||||
|
if (json == null) {
|
||||||
|
json = {};
|
||||||
|
}
|
||||||
|
json.type = this.type;
|
||||||
|
json.uid = this.getUid();
|
||||||
|
if (this.custom_type != null) {
|
||||||
|
if (this.custom_type.constructor === String) {
|
||||||
|
json.custom_type = this.custom_type;
|
||||||
|
} else {
|
||||||
|
json.custom_type = this.custom_type._name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
};
|
||||||
|
|
||||||
Operation.prototype.saveOperation = function(name, op) {
|
Operation.prototype.saveOperation = function(name, op) {
|
||||||
if (op == null) {
|
if (op == null) {
|
||||||
|
|
||||||
@ -258,7 +274,7 @@ module.exports = function() {
|
|||||||
this.deleted_by = [];
|
this.deleted_by = [];
|
||||||
}
|
}
|
||||||
callLater = false;
|
callLater = false;
|
||||||
if ((this.parent != null) && !this.isDeleted() && (o != null)) {
|
if ((this.parent != null) && !this.is_deleted && (o != null)) {
|
||||||
callLater = true;
|
callLater = true;
|
||||||
}
|
}
|
||||||
if (o != null) {
|
if (o != null) {
|
||||||
@ -273,12 +289,8 @@ module.exports = function() {
|
|||||||
this.callOperationSpecificDeleteEvents(o);
|
this.callOperationSpecificDeleteEvents(o);
|
||||||
}
|
}
|
||||||
if ((_ref = this.prev_cl) != null ? _ref.isDeleted() : void 0) {
|
if ((_ref = this.prev_cl) != null ? _ref.isDeleted() : void 0) {
|
||||||
this.prev_cl.applyDelete();
|
return this.prev_cl.applyDelete();
|
||||||
}
|
}
|
||||||
if (this.content instanceof ops.Operation) {
|
|
||||||
this.content.applyDelete();
|
|
||||||
}
|
|
||||||
return delete this.content;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Insert.prototype.cleanup = function() {
|
Insert.prototype.cleanup = function() {
|
||||||
@ -298,6 +310,13 @@ module.exports = function() {
|
|||||||
}
|
}
|
||||||
this.prev_cl.next_cl = this.next_cl;
|
this.prev_cl.next_cl = this.next_cl;
|
||||||
this.next_cl.prev_cl = this.prev_cl;
|
this.next_cl.prev_cl = this.prev_cl;
|
||||||
|
if (this.content instanceof ops.Operation && !deleted_earlyer) {
|
||||||
|
this.content.referenced_by--;
|
||||||
|
if (this.content.referenced_by <= 0 && !this.content.is_deleted) {
|
||||||
|
this.content.applyDelete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete this.content;
|
||||||
return Insert.__super__.cleanup.apply(this, arguments);
|
return Insert.__super__.cleanup.apply(this, arguments);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -317,12 +336,16 @@ module.exports = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Insert.prototype.execute = function() {
|
Insert.prototype.execute = function() {
|
||||||
var distance_to_origin, i, o;
|
var distance_to_origin, i, o, _base;
|
||||||
if (!this.validateSavedOperations()) {
|
if (!this.validateSavedOperations()) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
if (this.content instanceof ops.Operation) {
|
if (this.content instanceof ops.Operation) {
|
||||||
this.content.insert_parent = this;
|
this.content.insert_parent = this;
|
||||||
|
if ((_base = this.content).referenced_by == null) {
|
||||||
|
_base.referenced_by = 0;
|
||||||
|
}
|
||||||
|
this.content.referenced_by++;
|
||||||
}
|
}
|
||||||
if (this.parent != null) {
|
if (this.parent != null) {
|
||||||
if (this.prev_cl == null) {
|
if (this.prev_cl == null) {
|
||||||
@ -425,15 +448,14 @@ module.exports = function() {
|
|||||||
return position;
|
return position;
|
||||||
};
|
};
|
||||||
|
|
||||||
Insert.prototype._encode = function() {
|
Insert.prototype._encode = function(json) {
|
||||||
var json, _ref;
|
var _ref;
|
||||||
json = {
|
if (json == null) {
|
||||||
'type': this.type,
|
json = {};
|
||||||
'uid': this.getUid(),
|
}
|
||||||
'prev': this.prev_cl.getUid(),
|
json.prev = this.prev_cl.getUid();
|
||||||
'next': this.next_cl.getUid(),
|
json.next = this.next_cl.getUid();
|
||||||
'parent': this.parent.getUid()
|
json.parent = this.parent.getUid();
|
||||||
};
|
|
||||||
if (this.origin.type === "Delimiter") {
|
if (this.origin.type === "Delimiter") {
|
||||||
json.origin = "Delimiter";
|
json.origin = "Delimiter";
|
||||||
} else if (this.origin !== this.prev_cl) {
|
} else if (this.origin !== this.prev_cl) {
|
||||||
@ -444,7 +466,7 @@ module.exports = function() {
|
|||||||
} else {
|
} else {
|
||||||
json['content'] = JSON.stringify(this.content);
|
json['content'] = JSON.stringify(this.content);
|
||||||
}
|
}
|
||||||
return json;
|
return Insert.__super__._encode.call(this, json);
|
||||||
};
|
};
|
||||||
|
|
||||||
return Insert;
|
return Insert;
|
||||||
@ -458,38 +480,6 @@ module.exports = function() {
|
|||||||
}
|
}
|
||||||
return new this(null, content, uid, prev, next, origin, parent);
|
return new this(null, content, uid, prev, next, origin, parent);
|
||||||
};
|
};
|
||||||
ops.ImmutableObject = (function(_super) {
|
|
||||||
__extends(ImmutableObject, _super);
|
|
||||||
|
|
||||||
function ImmutableObject(custom_type, uid, _at_content) {
|
|
||||||
this.content = _at_content;
|
|
||||||
ImmutableObject.__super__.constructor.call(this, custom_type, uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImmutableObject.prototype.type = "ImmutableObject";
|
|
||||||
|
|
||||||
ImmutableObject.prototype.val = function() {
|
|
||||||
return this.content;
|
|
||||||
};
|
|
||||||
|
|
||||||
ImmutableObject.prototype._encode = function() {
|
|
||||||
var json;
|
|
||||||
json = {
|
|
||||||
'type': this.type,
|
|
||||||
'uid': this.getUid(),
|
|
||||||
'content': this.content
|
|
||||||
};
|
|
||||||
return json;
|
|
||||||
};
|
|
||||||
|
|
||||||
return ImmutableObject;
|
|
||||||
|
|
||||||
})(ops.Operation);
|
|
||||||
ops.ImmutableObject.parse = function(json) {
|
|
||||||
var content, uid;
|
|
||||||
uid = json['uid'], content = json['content'];
|
|
||||||
return new this(null, uid, content);
|
|
||||||
};
|
|
||||||
ops.Delimiter = (function(_super) {
|
ops.Delimiter = (function(_super) {
|
||||||
__extends(Delimiter, _super);
|
__extends(Delimiter, _super);
|
||||||
|
|
||||||
|
@ -105,20 +105,6 @@ module.exports = function() {
|
|||||||
return this._map[property_name];
|
return this._map[property_name];
|
||||||
};
|
};
|
||||||
|
|
||||||
MapManager.prototype._encode = function() {
|
|
||||||
var json;
|
|
||||||
json = {
|
|
||||||
'type': this.type,
|
|
||||||
'uid': this.getUid()
|
|
||||||
};
|
|
||||||
if (this.custom_type.constructor === String) {
|
|
||||||
json.custom_type = this.custom_type;
|
|
||||||
} else {
|
|
||||||
json.custom_type = this.custom_type._name;
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
};
|
|
||||||
|
|
||||||
return MapManager;
|
return MapManager;
|
||||||
|
|
||||||
})(ops.Operation);
|
})(ops.Operation);
|
||||||
@ -319,20 +305,6 @@ module.exports = function() {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
ListManager.prototype._encode = function() {
|
|
||||||
var json;
|
|
||||||
json = {
|
|
||||||
'type': this.type,
|
|
||||||
'uid': this.getUid()
|
|
||||||
};
|
|
||||||
if (this.custom_type.constructor === String) {
|
|
||||||
json.custom_type = this.custom_type;
|
|
||||||
} else {
|
|
||||||
json.custom_type = this.custom_type._name;
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
};
|
|
||||||
|
|
||||||
return ListManager;
|
return ListManager;
|
||||||
|
|
||||||
})(ops.Operation);
|
})(ops.Operation);
|
||||||
@ -407,15 +379,13 @@ module.exports = function() {
|
|||||||
return typeof o.val === "function" ? o.val() : void 0;
|
return typeof o.val === "function" ? o.val() : void 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
ReplaceManager.prototype._encode = function() {
|
ReplaceManager.prototype._encode = function(json) {
|
||||||
var json;
|
if (json == null) {
|
||||||
json = {
|
json = {};
|
||||||
'type': this.type,
|
}
|
||||||
'uid': this.getUid(),
|
json.beginning = this.beginning.getUid();
|
||||||
'beginning': this.beginning.getUid(),
|
json.end = this.end.getUid();
|
||||||
'end': this.end.getUid()
|
return ReplaceManager.__super__._encode.call(this, json);
|
||||||
};
|
|
||||||
return json;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return ReplaceManager;
|
return ReplaceManager;
|
||||||
@ -424,46 +394,13 @@ module.exports = function() {
|
|||||||
ops.Replaceable = (function(_super) {
|
ops.Replaceable = (function(_super) {
|
||||||
__extends(Replaceable, _super);
|
__extends(Replaceable, _super);
|
||||||
|
|
||||||
function Replaceable(custom_type, content, parent, uid, prev, next, origin, is_deleted) {
|
function Replaceable(custom_type, content, parent, uid, prev, next, origin) {
|
||||||
this.saveOperation('parent', parent);
|
this.saveOperation('parent', parent);
|
||||||
Replaceable.__super__.constructor.call(this, custom_type, content, uid, prev, next, origin);
|
Replaceable.__super__.constructor.call(this, custom_type, content, uid, prev, next, origin);
|
||||||
this.is_deleted = is_deleted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Replaceable.prototype.type = "Replaceable";
|
Replaceable.prototype.type = "Replaceable";
|
||||||
|
|
||||||
Replaceable.prototype.val = function() {
|
|
||||||
if ((this.content != null) && (this.content.getCustomType != null)) {
|
|
||||||
return this.content.getCustomType();
|
|
||||||
} else {
|
|
||||||
return this.content;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Replaceable.prototype.applyDelete = function() {
|
|
||||||
var res, _base, _base1, _base2;
|
|
||||||
res = Replaceable.__super__.applyDelete.apply(this, arguments);
|
|
||||||
if (this.content != null) {
|
|
||||||
if (this.next_cl.type !== "Delimiter") {
|
|
||||||
if (typeof (_base = this.content).deleteAllObservers === "function") {
|
|
||||||
_base.deleteAllObservers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof (_base1 = this.content).applyDelete === "function") {
|
|
||||||
_base1.applyDelete();
|
|
||||||
}
|
|
||||||
if (typeof (_base2 = this.content).dontSync === "function") {
|
|
||||||
_base2.dontSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.content = null;
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
Replaceable.prototype.cleanup = function() {
|
|
||||||
return Replaceable.__super__.cleanup.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
Replaceable.prototype.callOperationSpecificInsertEvents = function() {
|
Replaceable.prototype.callOperationSpecificInsertEvents = function() {
|
||||||
var old_value;
|
var old_value;
|
||||||
if (this.next_cl.type === "Delimiter" && this.prev_cl.type !== "Delimiter") {
|
if (this.next_cl.type === "Delimiter" && this.prev_cl.type !== "Delimiter") {
|
||||||
@ -503,39 +440,23 @@ module.exports = function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Replaceable.prototype._encode = function() {
|
Replaceable.prototype._encode = function(json) {
|
||||||
var json;
|
if (json == null) {
|
||||||
json = {
|
json = {};
|
||||||
'type': this.type,
|
|
||||||
'parent': this.parent.getUid(),
|
|
||||||
'prev': this.prev_cl.getUid(),
|
|
||||||
'next': this.next_cl.getUid(),
|
|
||||||
'uid': this.getUid(),
|
|
||||||
'is_deleted': this.is_deleted
|
|
||||||
};
|
|
||||||
if (this.origin.type === "Delimiter") {
|
|
||||||
json.origin = "Delimiter";
|
|
||||||
} else if (this.origin !== this.prev_cl) {
|
|
||||||
json.origin = this.origin.getUid();
|
|
||||||
}
|
}
|
||||||
if (this.content instanceof ops.Operation) {
|
return Replaceable.__super__._encode.call(this, json);
|
||||||
json['content'] = this.content.getUid();
|
|
||||||
} else {
|
|
||||||
if ((this.content != null) && (this.content.creator != null)) {
|
|
||||||
throw new Error("You must not set creator here!");
|
|
||||||
}
|
|
||||||
json['content'] = this.content;
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return Replaceable;
|
return Replaceable;
|
||||||
|
|
||||||
})(ops.Insert);
|
})(ops.Insert);
|
||||||
ops.Replaceable.parse = function(json) {
|
ops.Replaceable.parse = function(json) {
|
||||||
var content, custom_type, is_deleted, next, origin, parent, prev, uid;
|
var content, custom_type, next, origin, parent, prev, uid;
|
||||||
content = json['content'], parent = json['parent'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], is_deleted = json['is_deleted'], custom_type = json['custom_type'];
|
content = json['content'], parent = json['parent'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], custom_type = json['custom_type'];
|
||||||
return new this(custom_type, content, parent, uid, prev, next, origin, is_deleted);
|
if (typeof content === "string") {
|
||||||
|
content = JSON.parse(content);
|
||||||
|
}
|
||||||
|
return new this(custom_type, content, parent, uid, prev, next, origin);
|
||||||
};
|
};
|
||||||
return basic_ops;
|
return basic_ops;
|
||||||
};
|
};
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -145,6 +145,21 @@ module.exports = ()->
|
|||||||
l @_encode()
|
l @_encode()
|
||||||
@
|
@
|
||||||
|
|
||||||
|
#
|
||||||
|
# @private
|
||||||
|
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||||
|
#
|
||||||
|
_encode: (json = {})->
|
||||||
|
json.type = @type
|
||||||
|
json.uid = @getUid()
|
||||||
|
if @custom_type?
|
||||||
|
if @custom_type.constructor is String
|
||||||
|
json.custom_type = @custom_type
|
||||||
|
else
|
||||||
|
json.custom_type = @custom_type._name
|
||||||
|
json
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# @private
|
# @private
|
||||||
# Operations may depend on other operations (linked lists, etc.).
|
# Operations may depend on other operations (linked lists, etc.).
|
||||||
@ -313,7 +328,7 @@ module.exports = ()->
|
|||||||
applyDelete: (o)->
|
applyDelete: (o)->
|
||||||
@deleted_by ?= []
|
@deleted_by ?= []
|
||||||
callLater = false
|
callLater = false
|
||||||
if @parent? and not @isDeleted() and o? # o? : if not o?, then the delimiter deleted this Insertion. Furthermore, it would be wrong to call it. TODO: make this more expressive and save
|
if @parent? and not @is_deleted and o? # o? : if not o?, then the delimiter deleted this Insertion. Furthermore, it would be wrong to call it. TODO: make this more expressive and save
|
||||||
# call iff wasn't deleted earlyer
|
# call iff wasn't deleted earlyer
|
||||||
callLater = true
|
callLater = true
|
||||||
if o?
|
if o?
|
||||||
@ -328,12 +343,6 @@ module.exports = ()->
|
|||||||
# garbage collect prev_cl
|
# garbage collect prev_cl
|
||||||
@prev_cl.applyDelete()
|
@prev_cl.applyDelete()
|
||||||
|
|
||||||
# delete content
|
|
||||||
if @content instanceof ops.Operation
|
|
||||||
@content.applyDelete()
|
|
||||||
delete @content
|
|
||||||
|
|
||||||
|
|
||||||
cleanup: ()->
|
cleanup: ()->
|
||||||
if @next_cl.isDeleted()
|
if @next_cl.isDeleted()
|
||||||
# delete all ops that delete this insertion
|
# delete all ops that delete this insertion
|
||||||
@ -350,6 +359,17 @@ module.exports = ()->
|
|||||||
# reconnect left/right
|
# reconnect left/right
|
||||||
@prev_cl.next_cl = @next_cl
|
@prev_cl.next_cl = @next_cl
|
||||||
@next_cl.prev_cl = @prev_cl
|
@next_cl.prev_cl = @prev_cl
|
||||||
|
|
||||||
|
# delete content
|
||||||
|
# - we must not do this in applyDelete, because this would lead to inconsistencies
|
||||||
|
# (e.g. the following operation order must be invertible :
|
||||||
|
# Insert refers to content, then the content is deleted)
|
||||||
|
# Therefore, we have to do this in the cleanup
|
||||||
|
if @content instanceof ops.Operation and not deleted_earlyer
|
||||||
|
@content.referenced_by--
|
||||||
|
if @content.referenced_by <= 0 and not @content.is_deleted
|
||||||
|
@content.applyDelete()
|
||||||
|
delete @content
|
||||||
super
|
super
|
||||||
# else
|
# else
|
||||||
# Someone inserted something in the meantime.
|
# Someone inserted something in the meantime.
|
||||||
@ -378,6 +398,8 @@ module.exports = ()->
|
|||||||
else
|
else
|
||||||
if @content instanceof ops.Operation
|
if @content instanceof ops.Operation
|
||||||
@content.insert_parent = @ # TODO: this is probably not necessary and only nice for debugging
|
@content.insert_parent = @ # TODO: this is probably not necessary and only nice for debugging
|
||||||
|
@content.referenced_by ?= 0
|
||||||
|
@content.referenced_by++
|
||||||
if @parent?
|
if @parent?
|
||||||
if not @prev_cl?
|
if not @prev_cl?
|
||||||
@prev_cl = @parent.beginning
|
@prev_cl = @parent.beginning
|
||||||
@ -481,15 +503,10 @@ module.exports = ()->
|
|||||||
# Convert all relevant information of this operation to the json-format.
|
# Convert all relevant information of this operation to the json-format.
|
||||||
# This result can be send to other clients.
|
# This result can be send to other clients.
|
||||||
#
|
#
|
||||||
_encode: ()->
|
_encode: (json = {})->
|
||||||
json =
|
json.prev = @prev_cl.getUid()
|
||||||
{
|
json.next = @next_cl.getUid()
|
||||||
'type': @type
|
json.parent = @parent.getUid()
|
||||||
'uid' : @getUid()
|
|
||||||
'prev': @prev_cl.getUid()
|
|
||||||
'next': @next_cl.getUid()
|
|
||||||
'parent': @parent.getUid()
|
|
||||||
}
|
|
||||||
|
|
||||||
if @origin.type is "Delimiter"
|
if @origin.type is "Delimiter"
|
||||||
json.origin = "Delimiter"
|
json.origin = "Delimiter"
|
||||||
@ -500,7 +517,7 @@ module.exports = ()->
|
|||||||
json['content'] = @content.getUid()
|
json['content'] = @content.getUid()
|
||||||
else
|
else
|
||||||
json['content'] = JSON.stringify @content
|
json['content'] = JSON.stringify @content
|
||||||
json
|
super json
|
||||||
|
|
||||||
ops.Insert.parse = (json)->
|
ops.Insert.parse = (json)->
|
||||||
{
|
{
|
||||||
@ -515,47 +532,6 @@ module.exports = ()->
|
|||||||
content = JSON.parse(content)
|
content = JSON.parse(content)
|
||||||
new this null, content, uid, prev, next, origin, parent
|
new this null, content, uid, prev, next, origin, parent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# @nodoc
|
|
||||||
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
|
|
||||||
#
|
|
||||||
class ops.ImmutableObject extends ops.Operation
|
|
||||||
|
|
||||||
#
|
|
||||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
|
||||||
# @param {Object} content
|
|
||||||
#
|
|
||||||
constructor: (custom_type, uid, @content)->
|
|
||||||
super custom_type, uid
|
|
||||||
|
|
||||||
type: "ImmutableObject"
|
|
||||||
|
|
||||||
#
|
|
||||||
# @return [String] The content of this operation.
|
|
||||||
#
|
|
||||||
val : ()->
|
|
||||||
@content
|
|
||||||
|
|
||||||
#
|
|
||||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
|
||||||
#
|
|
||||||
_encode: ()->
|
|
||||||
json = {
|
|
||||||
'type': @type
|
|
||||||
'uid' : @getUid()
|
|
||||||
'content' : @content
|
|
||||||
}
|
|
||||||
json
|
|
||||||
|
|
||||||
ops.ImmutableObject.parse = (json)->
|
|
||||||
{
|
|
||||||
'uid' : uid
|
|
||||||
'content' : content
|
|
||||||
} = json
|
|
||||||
new this(null, uid, content)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# @nodoc
|
# @nodoc
|
||||||
# A delimiter is placed at the end and at the beginning of the associative lists.
|
# A delimiter is placed at the end and at the beginning of the associative lists.
|
||||||
|
@ -79,20 +79,6 @@ module.exports = ()->
|
|||||||
rm.execute()
|
rm.execute()
|
||||||
@_map[property_name]
|
@_map[property_name]
|
||||||
|
|
||||||
#
|
|
||||||
# @private
|
|
||||||
#
|
|
||||||
_encode: ()->
|
|
||||||
json = {
|
|
||||||
'type' : @type
|
|
||||||
'uid' : @getUid()
|
|
||||||
}
|
|
||||||
if @custom_type.constructor is String
|
|
||||||
json.custom_type = @custom_type
|
|
||||||
else
|
|
||||||
json.custom_type = @custom_type._name
|
|
||||||
json
|
|
||||||
|
|
||||||
ops.MapManager.parse = (json)->
|
ops.MapManager.parse = (json)->
|
||||||
{
|
{
|
||||||
'uid' : uid
|
'uid' : uid
|
||||||
@ -279,21 +265,6 @@ module.exports = ()->
|
|||||||
delete_ops.push d._encode()
|
delete_ops.push d._encode()
|
||||||
@
|
@
|
||||||
|
|
||||||
#
|
|
||||||
# @private
|
|
||||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
|
||||||
#
|
|
||||||
_encode: ()->
|
|
||||||
json = {
|
|
||||||
'type': @type
|
|
||||||
'uid' : @getUid()
|
|
||||||
}
|
|
||||||
if @custom_type.constructor is String
|
|
||||||
json.custom_type = @custom_type
|
|
||||||
else
|
|
||||||
json.custom_type = @custom_type._name
|
|
||||||
json
|
|
||||||
|
|
||||||
ops.ListManager.parse = (json)->
|
ops.ListManager.parse = (json)->
|
||||||
{
|
{
|
||||||
'uid' : uid
|
'uid' : uid
|
||||||
@ -381,15 +352,10 @@ module.exports = ()->
|
|||||||
#
|
#
|
||||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||||
#
|
#
|
||||||
_encode: ()->
|
_encode: (json = {})->
|
||||||
json =
|
json.beginning = @beginning.getUid()
|
||||||
{
|
json.end = @end.getUid()
|
||||||
'type': @type
|
super json
|
||||||
'uid' : @getUid()
|
|
||||||
'beginning' : @beginning.getUid()
|
|
||||||
'end' : @end.getUid()
|
|
||||||
}
|
|
||||||
json
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# @nodoc
|
# @nodoc
|
||||||
@ -403,35 +369,12 @@ module.exports = ()->
|
|||||||
# @param {ReplaceManager} parent Used to replace this Replaceable with another one.
|
# @param {ReplaceManager} parent Used to replace this Replaceable with another one.
|
||||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||||
#
|
#
|
||||||
constructor: (custom_type, content, parent, uid, prev, next, origin, is_deleted)->
|
constructor: (custom_type, content, parent, uid, prev, next, origin)->
|
||||||
@saveOperation 'parent', parent
|
@saveOperation 'parent', parent
|
||||||
super custom_type, content, uid, prev, next, origin # Parent is already saved by Replaceable
|
super custom_type, content, uid, prev, next, origin # Parent is already saved by Replaceable
|
||||||
@is_deleted = is_deleted
|
|
||||||
|
|
||||||
type: "Replaceable"
|
type: "Replaceable"
|
||||||
|
|
||||||
#
|
|
||||||
# Return the content that this operation holds.
|
|
||||||
#
|
|
||||||
val: ()->
|
|
||||||
if @content? and @content.getCustomType?
|
|
||||||
@content.getCustomType()
|
|
||||||
else
|
|
||||||
@content
|
|
||||||
|
|
||||||
applyDelete: ()->
|
|
||||||
res = super
|
|
||||||
if @content?
|
|
||||||
if @next_cl.type isnt "Delimiter"
|
|
||||||
@content.deleteAllObservers?()
|
|
||||||
@content.applyDelete?()
|
|
||||||
@content.dontSync?()
|
|
||||||
@content = null
|
|
||||||
res
|
|
||||||
|
|
||||||
cleanup: ()->
|
|
||||||
super
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# This is called, when the Insert-type was successfully executed.
|
# This is called, when the Insert-type was successfully executed.
|
||||||
# TODO: consider doing this in a more consistent manner. This could also be
|
# TODO: consider doing this in a more consistent manner. This could also be
|
||||||
@ -470,30 +413,8 @@ module.exports = ()->
|
|||||||
#
|
#
|
||||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||||
#
|
#
|
||||||
_encode: ()->
|
_encode: (json = {})->
|
||||||
json =
|
super json
|
||||||
{
|
|
||||||
'type': @type
|
|
||||||
'parent' : @parent.getUid()
|
|
||||||
'prev': @prev_cl.getUid()
|
|
||||||
'next': @next_cl.getUid()
|
|
||||||
'uid' : @getUid()
|
|
||||||
'is_deleted': @is_deleted
|
|
||||||
}
|
|
||||||
if @origin.type is "Delimiter"
|
|
||||||
json.origin = "Delimiter"
|
|
||||||
else if @origin isnt @prev_cl
|
|
||||||
json.origin = @origin.getUid()
|
|
||||||
|
|
||||||
if @content instanceof ops.Operation
|
|
||||||
json['content'] = @content.getUid()
|
|
||||||
else
|
|
||||||
# This could be a security concern.
|
|
||||||
# Throw error if the users wants to trick us
|
|
||||||
if @content? and @content.creator?
|
|
||||||
throw new Error "You must not set creator here!"
|
|
||||||
json['content'] = @content
|
|
||||||
json
|
|
||||||
|
|
||||||
ops.Replaceable.parse = (json)->
|
ops.Replaceable.parse = (json)->
|
||||||
{
|
{
|
||||||
@ -503,10 +424,11 @@ module.exports = ()->
|
|||||||
'prev': prev
|
'prev': prev
|
||||||
'next': next
|
'next': next
|
||||||
'origin' : origin
|
'origin' : origin
|
||||||
'is_deleted': is_deleted
|
|
||||||
'custom_type' : custom_type
|
'custom_type' : custom_type
|
||||||
} = json
|
} = json
|
||||||
new this(custom_type, content, parent, uid, prev, next, origin, is_deleted)
|
if typeof content is "string"
|
||||||
|
content = JSON.parse(content)
|
||||||
|
new this(custom_type, content, parent, uid, prev, next, origin)
|
||||||
|
|
||||||
|
|
||||||
basic_ops
|
basic_ops
|
||||||
|
@ -12,17 +12,6 @@ Y = require "../lib/y.coffee"
|
|||||||
Y.Text = require "../lib/Types/Text"
|
Y.Text = require "../lib/Types/Text"
|
||||||
Y.List = require "../lib/Types/List"
|
Y.List = require "../lib/Types/List"
|
||||||
|
|
||||||
compare = (o1, o2)->
|
|
||||||
if o1._name? and o1._name isnt o2._name
|
|
||||||
throw new Error "different types"
|
|
||||||
else if o1._name is "Object"
|
|
||||||
for name, val of o1.val()
|
|
||||||
compare(val, o2.val(name))
|
|
||||||
else if o1._name?
|
|
||||||
compare(o1.val(), o2.val())
|
|
||||||
else if o1 isnt o2
|
|
||||||
throw new Error "different values"
|
|
||||||
|
|
||||||
Test = require "./TestSuite"
|
Test = require "./TestSuite"
|
||||||
|
|
||||||
class JsonTest extends Test
|
class JsonTest extends Test
|
||||||
@ -36,12 +25,13 @@ class JsonTest extends Test
|
|||||||
|
|
||||||
type: "JsonTest"
|
type: "JsonTest"
|
||||||
|
|
||||||
getRandomRoot: (user_num, root)->
|
getRandomRoot: (user_num, root, depth = @max_depth)->
|
||||||
root ?= @users[user_num]
|
root ?= @users[user_num]
|
||||||
types = @users[user_num].types
|
types = @users[user_num].types
|
||||||
if _.random(0,1) is 1 # take root
|
if depth is 0 or _.random(0,1) is 1 # take root
|
||||||
root
|
root
|
||||||
else # take child
|
else # take child
|
||||||
|
depth--
|
||||||
elems = null
|
elems = null
|
||||||
if root._name is "Object"
|
if root._name is "Object"
|
||||||
elems =
|
elems =
|
||||||
@ -58,7 +48,7 @@ class JsonTest extends Test
|
|||||||
root
|
root
|
||||||
else
|
else
|
||||||
p = elems[_.random(0, elems.length-1)]
|
p = elems[_.random(0, elems.length-1)]
|
||||||
@getRandomRoot user_num, p
|
@getRandomRoot user_num, p, depth
|
||||||
|
|
||||||
getGeneratingFunctions: (user_num)->
|
getGeneratingFunctions: (user_num)->
|
||||||
super(user_num).concat [
|
super(user_num).concat [
|
||||||
@ -105,7 +95,19 @@ class JsonTest extends Test
|
|||||||
l = y.val().length
|
l = y.val().length
|
||||||
y.val(_.random(0,l-1), _.random(0,42))
|
y.val(_.random(0,l-1), _.random(0,42))
|
||||||
types : [Y.List]
|
types : [Y.List]
|
||||||
|
,
|
||||||
|
f : (y)=> # SET Object Property (circular)
|
||||||
|
y.val(@getRandomKey(), @getRandomRoot user_num)
|
||||||
|
types: [Y.Object]
|
||||||
|
,
|
||||||
|
f : (y)=> # insert Object mutable (circular)
|
||||||
|
l = y.val().length
|
||||||
|
y.val(_.random(0, l-1), @getRandomRoot user_num)
|
||||||
|
types: [Y.List]
|
||||||
]
|
]
|
||||||
|
###
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
describe "JsonFramework", ->
|
describe "JsonFramework", ->
|
||||||
@timeout 500000
|
@timeout 500000
|
||||||
@ -135,7 +137,7 @@ describe "JsonFramework", ->
|
|||||||
u1._model.engine.applyOp ops2, true
|
u1._model.engine.applyOp ops2, true
|
||||||
u2._model.engine.applyOp ops1, true
|
u2._model.engine.applyOp ops1, true
|
||||||
|
|
||||||
expect(compare(u1, u2)).to.not.be.undefined
|
expect(@yTest.compare(u1, u2)).to.not.be.undefined
|
||||||
|
|
||||||
it "can handle creaton of complex json (1)", ->
|
it "can handle creaton of complex json (1)", ->
|
||||||
@yTest.users[0].val('a', new Y.Text('q'))
|
@yTest.users[0].val('a', new Y.Text('q'))
|
||||||
@ -291,5 +293,17 @@ describe "JsonFramework", ->
|
|||||||
expect(last_task).to.equal("observer2")
|
expect(last_task).to.equal("observer2")
|
||||||
u.unobserve observer2
|
u.unobserve observer2
|
||||||
|
|
||||||
|
it "can handle circular JSON", ->
|
||||||
|
u = @yTest.users[0]
|
||||||
|
u.val("me", u)
|
||||||
|
@yTest.compareAll()
|
||||||
|
u.val("stuff", new Y.Object({x: true}))
|
||||||
|
u.val("same_stuff", u.val("stuff"))
|
||||||
|
u.val("same_stuff").val("x", 5)
|
||||||
|
expect(u.val("same_stuff").val("x")).to.equal(5)
|
||||||
|
@yTest.compareAll()
|
||||||
|
u.val("stuff").val("y", u.val("stuff"))
|
||||||
|
@yTest.compareAll()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ module.exports = class Test
|
|||||||
@time = 0 # denotes to the time when run was started
|
@time = 0 # denotes to the time when run was started
|
||||||
@ops = 0 # number of operations (used with @time)
|
@ops = 0 # number of operations (used with @time)
|
||||||
@time_now = 0 # current time
|
@time_now = 0 # current time
|
||||||
|
@max_depth = 4
|
||||||
|
|
||||||
@debug = false
|
@debug = false
|
||||||
|
|
||||||
@ -97,21 +98,21 @@ module.exports = class Test
|
|||||||
getContent: (user_num)->
|
getContent: (user_num)->
|
||||||
throw new Error "implement me!"
|
throw new Error "implement me!"
|
||||||
|
|
||||||
compare: (o1, o2)->
|
compare: (o1, o2, depth = (@max_depth+1))->
|
||||||
if o1 is o2
|
if o1 is o2 or depth <= 0
|
||||||
true
|
true
|
||||||
else if o1._name? and o1._name isnt o2._name
|
else if o1._name? and o1._name isnt o2._name
|
||||||
throw new Error "different types"
|
throw new Error "different types"
|
||||||
else if o1._name is "Object"
|
else if o1._name is "Object"
|
||||||
for name, val of o1.val()
|
for name, val of o1.val()
|
||||||
@compare(val, o2.val(name))
|
@compare(val, o2.val(name), depth-1)
|
||||||
else if o1._name?
|
else if o1._name?
|
||||||
@compare(o1.val(), o2.val())
|
@compare(o1.val(), o2.val(), depth-1)
|
||||||
else if o1.constructor is Array and o2.constructor is Array
|
else if o1.constructor is Array and o2.constructor is Array
|
||||||
if o1.length isnt o2.length
|
if o1.length isnt o2.length
|
||||||
throw new Error "The Arrays do not have the same size!"
|
throw new Error "The Arrays do not have the same size!"
|
||||||
for o,i in o1
|
for o,i in o1
|
||||||
@compare o, o2[i]
|
@compare o, o2[i], (depth-1)
|
||||||
else if o1 isnt o2
|
else if o1 isnt o2
|
||||||
throw new Error "different values"
|
throw new Error "different values"
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user