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
|
||||
* Text
|
||||
* Json
|
||||
* Json (even circular structures)
|
||||
* 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.
|
||||
|
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;
|
||||
};
|
||||
|
||||
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) {
|
||||
if (op == null) {
|
||||
|
||||
@ -258,7 +274,7 @@ module.exports = function() {
|
||||
this.deleted_by = [];
|
||||
}
|
||||
callLater = false;
|
||||
if ((this.parent != null) && !this.isDeleted() && (o != null)) {
|
||||
if ((this.parent != null) && !this.is_deleted && (o != null)) {
|
||||
callLater = true;
|
||||
}
|
||||
if (o != null) {
|
||||
@ -273,12 +289,8 @@ module.exports = function() {
|
||||
this.callOperationSpecificDeleteEvents(o);
|
||||
}
|
||||
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() {
|
||||
@ -298,6 +310,13 @@ module.exports = function() {
|
||||
}
|
||||
this.prev_cl.next_cl = this.next_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);
|
||||
}
|
||||
};
|
||||
@ -317,12 +336,16 @@ module.exports = function() {
|
||||
};
|
||||
|
||||
Insert.prototype.execute = function() {
|
||||
var distance_to_origin, i, o;
|
||||
var distance_to_origin, i, o, _base;
|
||||
if (!this.validateSavedOperations()) {
|
||||
return false;
|
||||
} else {
|
||||
if (this.content instanceof ops.Operation) {
|
||||
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.prev_cl == null) {
|
||||
@ -425,15 +448,14 @@ module.exports = function() {
|
||||
return position;
|
||||
};
|
||||
|
||||
Insert.prototype._encode = function() {
|
||||
var json, _ref;
|
||||
json = {
|
||||
'type': this.type,
|
||||
'uid': this.getUid(),
|
||||
'prev': this.prev_cl.getUid(),
|
||||
'next': this.next_cl.getUid(),
|
||||
'parent': this.parent.getUid()
|
||||
};
|
||||
Insert.prototype._encode = function(json) {
|
||||
var _ref;
|
||||
if (json == null) {
|
||||
json = {};
|
||||
}
|
||||
json.prev = this.prev_cl.getUid();
|
||||
json.next = this.next_cl.getUid();
|
||||
json.parent = this.parent.getUid();
|
||||
if (this.origin.type === "Delimiter") {
|
||||
json.origin = "Delimiter";
|
||||
} else if (this.origin !== this.prev_cl) {
|
||||
@ -444,7 +466,7 @@ module.exports = function() {
|
||||
} else {
|
||||
json['content'] = JSON.stringify(this.content);
|
||||
}
|
||||
return json;
|
||||
return Insert.__super__._encode.call(this, json);
|
||||
};
|
||||
|
||||
return Insert;
|
||||
@ -458,38 +480,6 @@ module.exports = function() {
|
||||
}
|
||||
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) {
|
||||
__extends(Delimiter, _super);
|
||||
|
||||
|
@ -105,20 +105,6 @@ module.exports = function() {
|
||||
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;
|
||||
|
||||
})(ops.Operation);
|
||||
@ -319,20 +305,6 @@ module.exports = function() {
|
||||
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;
|
||||
|
||||
})(ops.Operation);
|
||||
@ -407,15 +379,13 @@ module.exports = function() {
|
||||
return typeof o.val === "function" ? o.val() : void 0;
|
||||
};
|
||||
|
||||
ReplaceManager.prototype._encode = function() {
|
||||
var json;
|
||||
json = {
|
||||
'type': this.type,
|
||||
'uid': this.getUid(),
|
||||
'beginning': this.beginning.getUid(),
|
||||
'end': this.end.getUid()
|
||||
};
|
||||
return json;
|
||||
ReplaceManager.prototype._encode = function(json) {
|
||||
if (json == null) {
|
||||
json = {};
|
||||
}
|
||||
json.beginning = this.beginning.getUid();
|
||||
json.end = this.end.getUid();
|
||||
return ReplaceManager.__super__._encode.call(this, json);
|
||||
};
|
||||
|
||||
return ReplaceManager;
|
||||
@ -424,46 +394,13 @@ module.exports = function() {
|
||||
ops.Replaceable = (function(_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);
|
||||
Replaceable.__super__.constructor.call(this, custom_type, content, uid, prev, next, origin);
|
||||
this.is_deleted = is_deleted;
|
||||
}
|
||||
|
||||
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() {
|
||||
var old_value;
|
||||
if (this.next_cl.type === "Delimiter" && this.prev_cl.type !== "Delimiter") {
|
||||
@ -503,39 +440,23 @@ module.exports = function() {
|
||||
}
|
||||
};
|
||||
|
||||
Replaceable.prototype._encode = function() {
|
||||
var 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();
|
||||
Replaceable.prototype._encode = function(json) {
|
||||
if (json == null) {
|
||||
json = {};
|
||||
}
|
||||
if (this.content instanceof ops.Operation) {
|
||||
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.__super__._encode.call(this, json);
|
||||
};
|
||||
|
||||
return Replaceable;
|
||||
|
||||
})(ops.Insert);
|
||||
ops.Replaceable.parse = function(json) {
|
||||
var content, custom_type, is_deleted, 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'];
|
||||
return new this(custom_type, content, parent, uid, prev, next, origin, is_deleted);
|
||||
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'], custom_type = json['custom_type'];
|
||||
if (typeof content === "string") {
|
||||
content = JSON.parse(content);
|
||||
}
|
||||
return new this(custom_type, content, parent, uid, prev, next, origin);
|
||||
};
|
||||
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()
|
||||
@
|
||||
|
||||
#
|
||||
# @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
|
||||
# Operations may depend on other operations (linked lists, etc.).
|
||||
@ -313,7 +328,7 @@ module.exports = ()->
|
||||
applyDelete: (o)->
|
||||
@deleted_by ?= []
|
||||
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
|
||||
callLater = true
|
||||
if o?
|
||||
@ -328,12 +343,6 @@ module.exports = ()->
|
||||
# garbage collect prev_cl
|
||||
@prev_cl.applyDelete()
|
||||
|
||||
# delete content
|
||||
if @content instanceof ops.Operation
|
||||
@content.applyDelete()
|
||||
delete @content
|
||||
|
||||
|
||||
cleanup: ()->
|
||||
if @next_cl.isDeleted()
|
||||
# delete all ops that delete this insertion
|
||||
@ -350,6 +359,17 @@ module.exports = ()->
|
||||
# reconnect left/right
|
||||
@prev_cl.next_cl = @next_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
|
||||
# else
|
||||
# Someone inserted something in the meantime.
|
||||
@ -378,6 +398,8 @@ module.exports = ()->
|
||||
else
|
||||
if @content instanceof ops.Operation
|
||||
@content.insert_parent = @ # TODO: this is probably not necessary and only nice for debugging
|
||||
@content.referenced_by ?= 0
|
||||
@content.referenced_by++
|
||||
if @parent?
|
||||
if not @prev_cl?
|
||||
@prev_cl = @parent.beginning
|
||||
@ -481,15 +503,10 @@ module.exports = ()->
|
||||
# Convert all relevant information of this operation to the json-format.
|
||||
# This result can be send to other clients.
|
||||
#
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'prev': @prev_cl.getUid()
|
||||
'next': @next_cl.getUid()
|
||||
'parent': @parent.getUid()
|
||||
}
|
||||
_encode: (json = {})->
|
||||
json.prev = @prev_cl.getUid()
|
||||
json.next = @next_cl.getUid()
|
||||
json.parent = @parent.getUid()
|
||||
|
||||
if @origin.type is "Delimiter"
|
||||
json.origin = "Delimiter"
|
||||
@ -500,7 +517,7 @@ module.exports = ()->
|
||||
json['content'] = @content.getUid()
|
||||
else
|
||||
json['content'] = JSON.stringify @content
|
||||
json
|
||||
super json
|
||||
|
||||
ops.Insert.parse = (json)->
|
||||
{
|
||||
@ -515,47 +532,6 @@ module.exports = ()->
|
||||
content = JSON.parse(content)
|
||||
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
|
||||
# A delimiter is placed at the end and at the beginning of the associative lists.
|
||||
|
@ -79,20 +79,6 @@ module.exports = ()->
|
||||
rm.execute()
|
||||
@_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)->
|
||||
{
|
||||
'uid' : uid
|
||||
@ -279,21 +265,6 @@ module.exports = ()->
|
||||
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)->
|
||||
{
|
||||
'uid' : uid
|
||||
@ -381,15 +352,10 @@ module.exports = ()->
|
||||
#
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'beginning' : @beginning.getUid()
|
||||
'end' : @end.getUid()
|
||||
}
|
||||
json
|
||||
_encode: (json = {})->
|
||||
json.beginning = @beginning.getUid()
|
||||
json.end = @end.getUid()
|
||||
super json
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
@ -403,35 +369,12 @@ module.exports = ()->
|
||||
# @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.
|
||||
#
|
||||
constructor: (custom_type, content, parent, uid, prev, next, origin, is_deleted)->
|
||||
constructor: (custom_type, content, parent, uid, prev, next, origin)->
|
||||
@saveOperation 'parent', parent
|
||||
super custom_type, content, uid, prev, next, origin # Parent is already saved by Replaceable
|
||||
@is_deleted = is_deleted
|
||||
|
||||
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.
|
||||
# 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: ()->
|
||||
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
|
||||
_encode: (json = {})->
|
||||
super json
|
||||
|
||||
ops.Replaceable.parse = (json)->
|
||||
{
|
||||
@ -503,10 +424,11 @@ module.exports = ()->
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
'is_deleted': is_deleted
|
||||
'custom_type' : custom_type
|
||||
} = 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
|
||||
|
@ -12,17 +12,6 @@ Y = require "../lib/y.coffee"
|
||||
Y.Text = require "../lib/Types/Text"
|
||||
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"
|
||||
|
||||
class JsonTest extends Test
|
||||
@ -36,12 +25,13 @@ class JsonTest extends Test
|
||||
|
||||
type: "JsonTest"
|
||||
|
||||
getRandomRoot: (user_num, root)->
|
||||
getRandomRoot: (user_num, root, depth = @max_depth)->
|
||||
root ?= @users[user_num]
|
||||
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
|
||||
else # take child
|
||||
depth--
|
||||
elems = null
|
||||
if root._name is "Object"
|
||||
elems =
|
||||
@ -58,7 +48,7 @@ class JsonTest extends Test
|
||||
root
|
||||
else
|
||||
p = elems[_.random(0, elems.length-1)]
|
||||
@getRandomRoot user_num, p
|
||||
@getRandomRoot user_num, p, depth
|
||||
|
||||
getGeneratingFunctions: (user_num)->
|
||||
super(user_num).concat [
|
||||
@ -105,7 +95,19 @@ class JsonTest extends Test
|
||||
l = y.val().length
|
||||
y.val(_.random(0,l-1), _.random(0,42))
|
||||
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", ->
|
||||
@timeout 500000
|
||||
@ -135,7 +137,7 @@ describe "JsonFramework", ->
|
||||
u1._model.engine.applyOp ops2, 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)", ->
|
||||
@yTest.users[0].val('a', new Y.Text('q'))
|
||||
@ -291,5 +293,17 @@ describe "JsonFramework", ->
|
||||
expect(last_task).to.equal("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
|
||||
@ops = 0 # number of operations (used with @time)
|
||||
@time_now = 0 # current time
|
||||
@max_depth = 4
|
||||
|
||||
@debug = false
|
||||
|
||||
@ -97,21 +98,21 @@ module.exports = class Test
|
||||
getContent: (user_num)->
|
||||
throw new Error "implement me!"
|
||||
|
||||
compare: (o1, o2)->
|
||||
if o1 is o2
|
||||
compare: (o1, o2, depth = (@max_depth+1))->
|
||||
if o1 is o2 or depth <= 0
|
||||
true
|
||||
else 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))
|
||||
@compare(val, o2.val(name), depth-1)
|
||||
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
|
||||
if o1.length isnt o2.length
|
||||
throw new Error "The Arrays do not have the same size!"
|
||||
for o,i in o1
|
||||
@compare o, o2[i]
|
||||
@compare o, o2[i], (depth-1)
|
||||
else if o1 isnt o2
|
||||
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