support for circular structures (e.g. with JSON)

This commit is contained in:
DadaMonad 2015-02-24 16:09:42 +00:00
parent fea6de3bf9
commit 3ba89edf7d
13 changed files with 434 additions and 939 deletions

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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

2
y.js

File diff suppressed because one or more lines are too long