Introduced a new model for custom collaborative types.

This commit is contained in:
DadaMonad 2015-01-16 19:13:01 +00:00
parent b647b2af58
commit f1f710b269
15 changed files with 1019 additions and 1311 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,18 +11,18 @@ class Engine
# #
# @param {HistoryBuffer} HB # @param {HistoryBuffer} HB
# @param {Array} parser Defines how to parse encoded messages. # @param {Object} types list of available types
# #
constructor: (@HB, @parser)-> constructor: (@HB, @types)->
@unprocessed_ops = [] @unprocessed_ops = []
# #
# Parses an operatio from the json format. It uses the specified parser in your OperationType module. # Parses an operatio from the json format. It uses the specified parser in your OperationType module.
# #
parseOperation: (json)-> parseOperation: (json)->
typeParser = @parser[json.type] type = @types[json.type]
if typeParser? if type?.parse?
typeParser json type.parse json
else else
throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}." throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}."

View File

@ -1,6 +1,6 @@
module.exports = (HB)-> module.exports = (HB)->
# @see Engine.parse # @see Engine.parse
parser = {} types = {}
execution_listener = [] execution_listener = []
# #
@ -16,7 +16,7 @@ module.exports = (HB)->
# #
# Furthermore an encodable operation has a parser. We extend the parser object in order to parse encoded operations. # Furthermore an encodable operation has a parser. We extend the parser object in order to parse encoded operations.
# #
class Operation class types.Operation
# #
# @param {Object} uid A unique identifier. # @param {Object} uid A unique identifier.
@ -192,12 +192,11 @@ module.exports = (HB)->
@unchecked = uninstantiated @unchecked = uninstantiated
success success
# #
# @nodoc # @nodoc
# A simple Delete-type operation that deletes an operation. # A simple Delete-type operation that deletes an operation.
# #
class Delete extends Operation class types.Delete extends types.Operation
# #
# @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.
@ -237,12 +236,12 @@ module.exports = (HB)->
# #
# Define how to parse Delete operations. # Define how to parse Delete operations.
# #
parser['Delete'] = (o)-> types.Delete.parse = (o)->
{ {
'uid' : uid 'uid' : uid
'deletes': deletes_uid 'deletes': deletes_uid
} = o } = o
new Delete uid, deletes_uid new this(uid, deletes_uid)
# #
# @nodoc # @nodoc
@ -254,7 +253,7 @@ module.exports = (HB)->
# - The short-list (abbrev. sl) maintains only the operations that are not deleted # - The short-list (abbrev. sl) maintains only the operations that are not deleted
# - The complete-list (abbrev. cl) maintains all operations # - The complete-list (abbrev. cl) maintains all operations
# #
class Insert extends Operation class types.Insert extends types.Operation
# #
# @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.
@ -422,7 +421,7 @@ module.exports = (HB)->
position = 0 position = 0
prev = @prev_cl prev = @prev_cl
while true while true
if prev instanceof Delimiter if prev instanceof types.Delimiter
break break
if not prev.isDeleted() if not prev.isDeleted()
position++ position++
@ -433,7 +432,7 @@ module.exports = (HB)->
# @nodoc # @nodoc
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number. # Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
# #
class ImmutableObject extends Operation class types.ImmutableObject extends types.Operation
# #
# @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.
@ -455,18 +454,18 @@ module.exports = (HB)->
# #
_encode: ()-> _encode: ()->
json = { json = {
'type': "ImmutableObject" 'type': @type
'uid' : @getUid() 'uid' : @getUid()
'content' : @content 'content' : @content
} }
json json
parser['ImmutableObject'] = (json)-> types.ImmutableObject.parse = (json)->
{ {
'uid' : uid 'uid' : uid
'content' : content 'content' : content
} = json } = json
new ImmutableObject uid, content new this(uid, content)
# #
# @nodoc # @nodoc
@ -474,7 +473,7 @@ module.exports = (HB)->
# This is necessary in order to have a beginning and an end even if the content # This is necessary in order to have a beginning and an end even if the content
# of the Engine is empty. # of the Engine is empty.
# #
class Delimiter extends Operation class types.Delimiter extends types.Operation
# #
# @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.
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl) # @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
@ -527,29 +526,23 @@ module.exports = (HB)->
# #
_encode: ()-> _encode: ()->
{ {
'type' : "Delimiter" 'type' : @type
'uid' : @getUid() 'uid' : @getUid()
'prev' : @prev_cl?.getUid() 'prev' : @prev_cl?.getUid()
'next' : @next_cl?.getUid() 'next' : @next_cl?.getUid()
} }
parser['Delimiter'] = (json)-> types.Delimiter.parse = (json)->
{ {
'uid' : uid 'uid' : uid
'prev' : prev 'prev' : prev
'next' : next 'next' : next
} = json } = json
new Delimiter uid, prev, next new this(uid, prev, next)
# This is what this module exports after initializing it with the HistoryBuffer # This is what this module exports after initializing it with the HistoryBuffer
{ {
'types' : 'types' : types
'Delete' : Delete
'Insert' : Insert
'Delimiter': Delimiter
'Operation': Operation
'ImmutableObject' : ImmutableObject
'parser' : parser
'execution_listener' : execution_listener 'execution_listener' : execution_listener
} }

View File

@ -3,100 +3,11 @@ text_types_uninitialized = require "./TextTypes"
module.exports = (HB)-> module.exports = (HB)->
text_types = text_types_uninitialized HB text_types = text_types_uninitialized HB
types = text_types.types types = text_types.types
parser = text_types.parser
createJsonTypeWrapper = (_jsonType)->
#
# @note EXPERIMENTAL
#
# A JsonTypeWrapper was intended to be a convenient wrapper for the JsonType.
# But it can make things more difficult than they are.
# @see JsonType
#
# @example create a JsonTypeWrapper
# # You get a JsonTypeWrapper from a JsonType by calling
# w = yatta.value
#
# It creates Javascripts -getter and -setter methods for each property that JsonType maintains.
# @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
#
# @example Getter Example
# # you can access the x property of yatta by calling
# w.x
# # instead of
# yatta.val('x')
#
# @note You can only overwrite existing values! Setting a new property won't have any effect!
#
# @example Setter Example
# # you can set an existing x property of yatta by calling
# w.x = "text"
# # instead of
# yatta.val('x', "text")
#
# In order to set a new property you have to overwrite an existing property.
# Therefore the JsonTypeWrapper supports a special feature that should make things more convenient
# (we can argue about that, use the JsonType if you don't like it ;).
# If you overwrite an object property of the JsonTypeWrapper with a new object, it will result in a merged version of the objects.
# Let `yatta.value.p` the property that is to be overwritten and o the new value. E.g. `yatta.value.p = o`
# * The result has all properties of o
# * The result has all properties of w.p if they don't occur under the same property-name in o.
#
# @example Conflict Example
# yatta.value = {a : "string"}
# w = yatta.value
# console.log(w) # {a : "string"}
# w.a = {a : {b : "string"}}
# console.log(w) # {a : {b : "String"}}
# w.a = {a : {c : 4}}
# console.log(w) # {a : {b : "String", c : 4}}
#
# @example Common Pitfalls
# w = yatta.value
# # Setting a new property
# w.newProperty = "Awesome"
# console.log(w.newProperty == "Awesome") # false, w.newProperty is undefined
# # overwrite the w object
# w = {newProperty : "Awesome"}
# console.log(w.newProperty == "Awesome") # true!, but ..
# console.log(yatta.value.newProperty == "Awesome") # false, you are only allowed to set properties!
# # The solution
# yatta.value = {newProperty : "Awesome"}
# console.log(w.newProperty == "Awesome") # true!
#
class JsonTypeWrapper
#
# @param {JsonType} jsonType Instance of the JsonType that this class wrappes.
#
constructor: (jsonType)->
for name, obj of jsonType.map
do (name, obj)->
Object.defineProperty JsonTypeWrapper.prototype, name,
get : ->
x = obj.val()
if x instanceof JsonType
createJsonTypeWrapper x
else if x instanceof types.ImmutableObject
x.val()
else
x
set : (o)->
overwrite = jsonType.val(name)
if o.constructor is {}.constructor and overwrite instanceof types.Operation
for o_name,o_obj of o
overwrite.val(o_name, o_obj, 'immutable')
else
jsonType.val(name, o, 'immutable')
enumerable: true
configurable: false
new JsonTypeWrapper _jsonType
# #
# Manages Object-like values. # Manages Object-like values.
# #
class JsonType extends types.MapManager class types.Object extends types.MapManager
# #
# Identifies this class. # Identifies this class.
@ -104,11 +15,11 @@ module.exports = (HB)->
# #
# @example # @example
# var x = yatta.val('unknown') # var x = yatta.val('unknown')
# if (x.type === "JsonType") { # if (x.type === "Object") {
# console.log JSON.stringify(x.toJson()) # console.log JSON.stringify(x.toJson())
# } # }
# #
type: "JsonType" type: "Object"
applyDelete: ()-> applyDelete: ()->
super() super()
@ -130,7 +41,7 @@ module.exports = (HB)->
val = @val() val = @val()
json = {} json = {}
for name, o of val for name, o of val
if o instanceof JsonType if o instanceof types.Object
json[name] = o.toJson(transform_to_value) json[name] = o.toJson(transform_to_value)
else if transform_to_value and o instanceof types.Operation else if transform_to_value and o instanceof types.Operation
json[name] = o.val() json[name] = o.val()
@ -171,24 +82,6 @@ module.exports = (HB)->
changedBy:event.changedBy changedBy:event.changedBy
@bound_json @bound_json
#
# Whether the default is 'mutable' (true) or 'immutable' (false)
#
mutable_default:
false
#
# Set if the default is 'mutable' or 'immutable'
# @param {String|Boolean} mutable Set either 'mutable' / true or 'immutable' / false
setMutableDefault: (mutable)->
if mutable is true or mutable is 'mutable'
JsonType.prototype.mutable_default = true
else if mutable is false or mutable is 'immutable'
JsonType.prototype.mutable_default = false
else
throw new Error 'Set mutable either "mutable" or "immutable"!'
'OK'
# #
# @overload val() # @overload val()
# Get this as a Json object. # Get this as a Json object.
@ -197,70 +90,56 @@ module.exports = (HB)->
# @overload val(name) # @overload val(name)
# Get value of a property. # Get value of a property.
# @param {String} name Name of the object property. # @param {String} name Name of the object property.
# @return [JsonType|WordType|String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object. # @return [Object Type||String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object.
# #
# @overload val(name, content) # @overload val(name, content)
# Set a new property. # Set a new property.
# @param {String} name Name of the object property. # @param {String} name Name of the object property.
# @param {Object|String} content Content of the object property. # @param {Object|String} content Content of the object property.
# @return [JsonType] This object. (supports chaining) # @return [Object Type] This object. (supports chaining)
# #
val: (name, content, mutable)-> val: (name, content)->
if name? and arguments.length > 1 if name? and arguments.length > 1
if mutable? if content? and content.constructor?
if mutable is true or mutable is 'mutable' type = types[content.constructor.name]
mutable = true if type? and type.create?
args = []
for i in [1...arguments.length]
args.push arguments[i]
o = type.create.apply null, args
super name, o
else else
mutable = false throw new Error "The #{content.constructor.name}-type is not (yet) supported in Yatta."
else else
mutable = @mutable_default
if typeof content is 'function'
@ # Just do nothing
else if (not content?) or (((not mutable) or typeof content is 'number') and content.constructor isnt Object)
super name, (new types.ImmutableObject undefined, content).execute() super name, (new types.ImmutableObject undefined, content).execute()
else else # is this even necessary ? I have to define every type anyway.. (see Number type below)
if typeof content is 'string' super name
word = (new types.WordType undefined).execute()
word.insertText 0, content
super name, word
else if content.constructor is Object
json = new JsonType().execute()
for n,o of content
json.val n, o, mutable
super name, json
else
throw new Error "You must not set #{typeof content}-types in collaborative Json-objects!"
else
super name, content
Object.defineProperty JsonType.prototype, 'value',
get : -> createJsonTypeWrapper @
set : (o)->
if o.constructor is {}.constructor
for o_name,o_obj of o
@val(o_name, o_obj, 'immutable')
else
throw new Error "You must only set Object values!"
# #
# @private # @private
# #
_encode: ()-> _encode: ()->
{ {
'type' : "JsonType" 'type' : @type
'uid' : @getUid() 'uid' : @getUid()
} }
parser['JsonType'] = (json)-> types.Object.parse = (json)->
{ {
'uid' : uid 'uid' : uid
} = json } = json
new JsonType uid new this(uid)
types.Object.create = (content, mutable)->
json = new types.Object().execute()
for n,o of content
json.val n, o, mutable
json
types.Number = {}
types.Number.create = (content)->
types['JsonType'] = JsonType (new types.ImmutableObject undefined, content).execute()
text_types text_types

View File

@ -3,13 +3,12 @@ basic_types_uninitialized = require "./BasicTypes"
module.exports = (HB)-> module.exports = (HB)->
basic_types = basic_types_uninitialized HB basic_types = basic_types_uninitialized HB
types = basic_types.types types = basic_types.types
parser = basic_types.parser
# #
# @nodoc # @nodoc
# Manages map like objects. E.g. Json-Type and XML attributes. # Manages map like objects. E.g. Json-Type and XML attributes.
# #
class MapManager extends types.Operation class types.MapManager extends types.Operation
# #
# @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.
@ -69,7 +68,7 @@ module.exports = (HB)->
rm_uid = rm_uid =
noOperation: true noOperation: true
alt: map_uid alt: map_uid
rm = new ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB rm = new types.ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB
@map[property_name] = rm @map[property_name] = rm
rm.setParent @, property_name rm.setParent @, property_name
rm.execute() rm.execute()
@ -79,7 +78,7 @@ module.exports = (HB)->
# @nodoc # @nodoc
# Manages a list of Insert-type operations. # Manages a list of Insert-type operations.
# #
class ListManager extends types.Operation class types.ListManager extends types.Operation
# #
# A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!) # A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!)
@ -156,10 +155,10 @@ module.exports = (HB)->
# Adds support for replace. The ReplaceManager manages Replaceable operations. # Adds support for replace. The ReplaceManager manages Replaceable operations.
# Each Replaceable holds a value that is now replaceable. # Each Replaceable holds a value that is now replaceable.
# #
# The WordType-type has implemented support for replace # The TextType-type has implemented support for replace
# @see WordType # @see TextType
# #
class ReplaceManager extends ListManager class types.ReplaceManager extends types.ListManager
# #
# @param {Object} event_properties Decorates the event that is thrown by the RM # @param {Object} event_properties Decorates the event that is thrown by the RM
# @param {Object} event_this The object on which the event shall be executed # @param {Object} event_this The object on which the event shall be executed
@ -207,7 +206,7 @@ module.exports = (HB)->
# #
replace: (content, replaceable_uid)-> replace: (content, replaceable_uid)->
o = @getLastOperation() o = @getLastOperation()
relp = (new Replaceable content, @, replaceable_uid, o, o.next_cl).execute() relp = (new types.Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
# TODO: delete repl (for debugging) # TODO: delete repl (for debugging)
undefined undefined
@ -219,7 +218,7 @@ module.exports = (HB)->
undefined undefined
# #
# Get the value of this WordType # Get the value of this
# @return {String} # @return {String}
# #
val: ()-> val: ()->
@ -234,7 +233,7 @@ module.exports = (HB)->
_encode: ()-> _encode: ()->
json = json =
{ {
'type': "ReplaceManager" 'type': @type
'uid' : @getUid() 'uid' : @getUid()
'beginning' : @beginning.getUid() 'beginning' : @beginning.getUid()
'end' : @end.getUid() 'end' : @end.getUid()
@ -246,7 +245,7 @@ module.exports = (HB)->
# The ReplaceManager manages Replaceables. # The ReplaceManager manages Replaceables.
# @see ReplaceManager # @see ReplaceManager
# #
class Replaceable extends types.Insert class types.Replaceable extends types.Insert
# #
# @param {Operation} content The value that this Replaceable holds. # @param {Operation} content The value that this Replaceable holds.
@ -321,7 +320,7 @@ module.exports = (HB)->
_encode: ()-> _encode: ()->
json = json =
{ {
'type': "Replaceable" 'type': @type
'content': @content?.getUid() 'content': @content?.getUid()
'parent' : @parent.getUid() 'parent' : @parent.getUid()
'prev': @prev_cl.getUid() 'prev': @prev_cl.getUid()
@ -332,7 +331,7 @@ module.exports = (HB)->
} }
json json
parser["Replaceable"] = (json)-> types.Replaceable.parse = (json)->
{ {
'content' : content 'content' : content
'parent' : parent 'parent' : parent
@ -342,12 +341,8 @@ module.exports = (HB)->
'origin' : origin 'origin' : origin
'is_deleted': is_deleted 'is_deleted': is_deleted
} = json } = json
new Replaceable content, parent, uid, prev, next, origin, is_deleted new this(content, parent, uid, prev, next, origin, is_deleted)
types['ListManager'] = ListManager
types['MapManager'] = MapManager
types['ReplaceManager'] = ReplaceManager
types['Replaceable'] = Replaceable
basic_types basic_types

View File

@ -5,19 +5,11 @@ module.exports = (HB)->
types = structured_types.types types = structured_types.types
parser = structured_types.parser parser = structured_types.parser
#
# @nodoc
# At the moment TextDelete type equals the Delete type in BasicTypes.
# @see BasicTypes.Delete
#
class TextDelete extends types.Delete
parser["TextDelete"] = parser["Delete"]
# #
# @nodoc # @nodoc
# Extends the basic Insert type to an operation that holds a text value # Extends the basic Insert type to an operation that holds a text value
# #
class TextInsert extends types.Insert class types.TextInsert extends types.Insert
# #
# @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1 # @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
# @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.
@ -72,7 +64,7 @@ module.exports = (HB)->
_encode: ()-> _encode: ()->
json = json =
{ {
'type': "TextInsert" 'type': @type
'uid' : @getUid() 'uid' : @getUid()
'prev': @prev_cl.getUid() 'prev': @prev_cl.getUid()
'next': @next_cl.getUid() 'next': @next_cl.getUid()
@ -86,7 +78,7 @@ module.exports = (HB)->
json['content'] = @content json['content'] = @content
json json
parser["TextInsert"] = (json)-> types.TextInsert.parse = (json)->
{ {
'content' : content 'content' : content
'uid' : uid 'uid' : uid
@ -95,13 +87,109 @@ module.exports = (HB)->
'origin' : origin 'origin' : origin
'parent' : parent 'parent' : parent
} = json } = json
new TextInsert content, uid, prev, next, origin, parent new types.TextInsert content, uid, prev, next, origin, parent
class types.Array extends types.ListManager
type: "Array"
applyDelete: ()->
o = @end
while o?
o.applyDelete()
o = o.prev_cl
super()
cleanup: ()->
super()
val: ()->
o = @beginning.next_cl
result = []
while o isnt @end
result.push o.val()
o = o.next_cl
result
push: (content)->
@insertAfter @end.prev_cl, content
insertAfter: (left, content)->
right = left.next_cl
while right.isDeleted()
right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
left = right.prev_cl
if content.type?
(new types.TextInsert content, undefined, left, right).execute()
else
for c in content
tmp = (new types.TextInsert c, undefined, left, right).execute()
left = tmp
@
# #
# Handles a WordType-like data structures with support for insertText/deleteText at a word-position. # Inserts a string into the word.
#
# @return {Array Type} This String object.
#
insert: (position, content)->
ith = @getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@insertAfter ith, content
#
# Deletes a part of the word.
#
# @return {Array Type} This String object
#
delete: (position, length)->
o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character
delete_ops = []
for i in [0...length]
if o instanceof types.Delimiter
break
d = (new types.Delete undefined, o).execute()
o = o.next_cl
while not (o instanceof types.Delimiter) and o.isDeleted()
o = o.next_cl
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()
}
json
types.Array.parse = (json)->
{
'uid' : uid
} = json
new this(uid)
types.Array.create = (content, mutable)->
if (mutable is "mutable")
list = new types.Array().execute()
list.insert 0, content
list
else if (not mutable?) or (mutable is "immutable")
(new types.ImmutableObject undefined, content).execute()
else
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
#
# Handles a String-like data structures with support for insert/delete at a word-position.
# @note Currently, only Text is supported! # @note Currently, only Text is supported!
# #
class WordType extends types.ListManager class types.String extends types.Array
# #
# @private # @private
@ -117,66 +205,11 @@ module.exports = (HB)->
# #
# @example # @example
# var x = yatta.val('unknown') # var x = yatta.val('unknown')
# if (x.type === "WordType") { # if (x.type === "String") {
# console.log JSON.stringify(x.toJson()) # console.log JSON.stringify(x.toJson())
# } # }
# #
type: "WordType" type: "String"
applyDelete: ()->
o = @end
while o?
o.applyDelete()
o = o.prev_cl
super()
cleanup: ()->
super()
push: (content)->
@insertAfter @end.prev_cl, content
insertAfter: (left, content)->
right = left.next_cl
while right.isDeleted()
right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
left = right.prev_cl
if content.type?
(new TextInsert content, undefined, left, right).execute()
else
for c in content
tmp = (new TextInsert c, undefined, left, right).execute()
left = tmp
@
#
# Inserts a string into the word.
#
# @return {WordType} This WordType object.
#
insertText: (position, content)->
ith = @getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@insertAfter ith, content
#
# Deletes a part of the word.
#
# @return {WordType} This WordType object
#
deleteText: (position, length)->
o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character
delete_ops = []
for i in [0...length]
if o instanceof types.Delimiter
break
d = (new TextDelete undefined, o).execute()
o = o.next_cl
while not (o instanceof types.Delimiter) and o.isDeleted()
o = o.next_cl
delete_ops.push d._encode()
@
# #
# Get the String-representation of this word. # Get the String-representation of this word.
@ -191,14 +224,14 @@ module.exports = (HB)->
c.join('') c.join('')
# #
# Same as WordType.val # Same as String.val
# @see WordType.val # @see String.val
# #
toString: ()-> toString: ()->
@val() @val()
# #
# Bind this WordType to a textfield or input field. # Bind this String to a textfield or input field.
# #
# @example # @example
# var textbox = document.getElementById("textfield"); # var textbox = document.getElementById("textfield");
@ -257,8 +290,8 @@ module.exports = (HB)->
if char.length > 0 if char.length > 0
pos = Math.min textfield.selectionStart, textfield.selectionEnd pos = Math.min textfield.selectionStart, textfield.selectionEnd
diff = Math.abs(textfield.selectionEnd - textfield.selectionStart) diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
word.deleteText (pos), diff word.delete (pos), diff
word.insertText pos, char word.insert pos, char
new_pos = pos + char.length new_pos = pos + char.length
textfield.setSelectionRange new_pos, new_pos textfield.setSelectionRange new_pos, new_pos
event.preventDefault() event.preventDefault()
@ -294,7 +327,7 @@ module.exports = (HB)->
diff = Math.abs(textfield.selectionEnd - textfield.selectionStart) diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
if event.keyCode? and event.keyCode is 8 # Backspace if event.keyCode? and event.keyCode is 8 # Backspace
if diff > 0 if diff > 0
word.deleteText pos, diff word.delete pos, diff
textfield.setSelectionRange pos, pos textfield.setSelectionRange pos, pos
else else
if event.ctrlKey? and event.ctrlKey if event.ctrlKey? and event.ctrlKey
@ -307,42 +340,48 @@ module.exports = (HB)->
while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n' while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n'
new_pos-- new_pos--
del_length++ del_length++
word.deleteText new_pos, (pos-new_pos) word.delete new_pos, (pos-new_pos)
textfield.setSelectionRange new_pos, new_pos textfield.setSelectionRange new_pos, new_pos
else else
word.deleteText (pos-1), 1 word.delete (pos-1), 1
event.preventDefault() event.preventDefault()
else if event.keyCode? and event.keyCode is 46 # Delete else if event.keyCode? and event.keyCode is 46 # Delete
if diff > 0 if diff > 0
word.deleteText pos, diff word.delete pos, diff
textfield.setSelectionRange pos, pos textfield.setSelectionRange pos, pos
else else
word.deleteText pos, 1 word.delete pos, 1
textfield.setSelectionRange pos, pos textfield.setSelectionRange pos, pos
event.preventDefault() event.preventDefault()
# #
# @private # @private
# 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 = {
'type': "WordType" 'type': @type
'uid' : @getUid() 'uid' : @getUid()
} }
json json
parser['WordType'] = (json)-> types.String.parse = (json)->
{ {
'uid' : uid 'uid' : uid
} = json } = json
new WordType uid new this(uid)
types.String.create = (content, mutable)->
if (mutable is "mutable")
word = new types.String().execute()
word.insert 0, content
word
else if (not mutable?) or (mutable is "immutable")
(new types.ImmutableObject undefined, content).execute()
else
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
types['TextInsert'] = TextInsert
types['TextDelete'] = TextDelete
types['WordType'] = WordType
structured_types structured_types

View File

@ -24,7 +24,7 @@ createYatta = (connector)->
# * Integer # * Integer
# * Array # * Array
# #
class Yatta extends types.JsonType class Yatta extends types.Object
# #
# @param {String} user_id Unique id of the peer. # @param {String} user_id Unique id of the peer.
@ -34,7 +34,7 @@ createYatta = (connector)->
@connector = connector @connector = connector
@HB = HB @HB = HB
@types = types @types = types
@engine = new Engine @HB, type_manager.parser @engine = new Engine @HB, type_manager.types
adaptConnector @connector, @engine, @HB, type_manager.execution_listener adaptConnector @connector, @engine, @HB, type_manager.execution_listener
super super

View File

@ -45,19 +45,19 @@ class JsonTest extends Test
f : (y)=> # SET PROPERTY f : (y)=> # SET PROPERTY
y.val(@getRandomKey(), @getRandomText(), 'immutable') y.val(@getRandomKey(), @getRandomText(), 'immutable')
null null
types : [types.JsonType] types : [types.Object]
, ,
f : (y)=> # SET Object Property 1) f : (y)=> # SET Object Property 1)
y.val(@getRandomObject()) y.val(@getRandomObject())
types: [types.JsonType] types: [types.Object]
, ,
f : (y)=> # SET Object Property 2) f : (y)=> # SET Object Property 2)
y.val(@getRandomKey(), @getRandomObject()) y.val(@getRandomKey(), @getRandomObject())
types: [types.JsonType] types: [types.Object]
, ,
f : (y)=> # SET PROPERTY TEXT f : (y)=> # SET PROPERTY TEXT
y.val(@getRandomKey(), @getRandomText(), 'mutable') y.val(@getRandomKey(), @getRandomText(), 'mutable')
types: [types.JsonType] types: [types.Object]
] ]
describe "JsonFramework", -> describe "JsonFramework", ->
@ -73,30 +73,6 @@ describe "JsonFramework", ->
console.log "" # TODO console.log "" # TODO
@yTest.run() @yTest.run()
### TODO
it "has a update listener", ()->
addName = false
change = false
change2 = 0
@test_user.on 'add', (eventname, property_name)->
if property_name is 'x'
addName = true
@test_user.val('x',5)
@test_user.on 'change', (eventname, property_name)->
if property_name is 'x'
change = true
@test_user.val('x', 6)
@test_user.val('ins', "text", 'mutable')
@test_user.on 'update', (eventname, property_name)->
if property_name is 'ins'
change2++
@test_user.val('ins').insertText 4, " yay"
@test_user.val('ins').deleteText 0, 4
expect(addName).to.be.ok
expect(change).to.be.ok
expect(change2).to.equal 8
###
it "has a working test suite", -> it "has a working test suite", ->
@yTest.compareAll() @yTest.compareAll()
@ -119,9 +95,9 @@ describe "JsonFramework", ->
@yTest.users[1].val('a', 't', "mutable") @yTest.users[1].val('a', 't', "mutable")
@yTest.compareAll() @yTest.compareAll()
q = @yTest.users[2].val('a') q = @yTest.users[2].val('a')
q.insertText(0,'A') q.insert(0,'A')
@yTest.compareAll() @yTest.compareAll()
expect(@yTest.getSomeUser().value.a.val()).to.equal("At") expect(@yTest.getSomeUser().val("a").val()).to.equal("At")
it "can handle creaton of complex json (2)", -> it "can handle creaton of complex json (2)", ->
@yTest.getSomeUser().val('x', {'a':'b'}) @yTest.getSomeUser().val('x', {'a':'b'})
@ -130,10 +106,18 @@ describe "JsonFramework", ->
@yTest.getSomeUser().val('c', {'a':'c'}) @yTest.getSomeUser().val('c', {'a':'c'})
@yTest.getSomeUser().val('c', {'a':'b'}) @yTest.getSomeUser().val('c', {'a':'b'})
@yTest.compareAll() @yTest.compareAll()
q = @yTest.getSomeUser().value.a.a.q q = @yTest.getSomeUser().val("a").val("a").val("q")
q.insertText(0,'A') q.insert(0,'A')
@yTest.compareAll()
expect(@yTest.getSomeUser().val("a").val("a").val("q").val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
it "can handle creaton of complex json (3)", ->
@yTest.users[0].val('l', [1,2,3], "mutable")
@yTest.users[1].val('l', [4,5,6], "mutable")
@yTest.compareAll()
@yTest.users[2].val('l').insert(0,'A')
@yTest.users[1].val('l').insert(0,'B')
@yTest.compareAll() @yTest.compareAll()
expect(@yTest.getSomeUser().value.a.a.q.val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
it "handles immutables and primitive data types", -> it "handles immutables and primitive data types", ->
@yTest.getSomeUser().val('string', "text", "immutable") @yTest.getSomeUser().val('string', "text", "immutable")

View File

@ -12,9 +12,9 @@ Connector = require "../bower_components/connector/lib/test-connector/test-conne
module.exports = class Test module.exports = class Test
constructor: (@name_suffix = "")-> constructor: (@name_suffix = "")->
@number_of_test_cases_multiplier = 1 @number_of_test_cases_multiplier = 1
@repeat_this = 503 * @number_of_test_cases_multiplier @repeat_this = 3 * @number_of_test_cases_multiplier
@doSomething_amount = 5 * @number_of_test_cases_multiplier @doSomething_amount = 123 * @number_of_test_cases_multiplier
@number_of_engines = 3 + @number_of_test_cases_multiplier - 1 @number_of_engines = 5 + @number_of_test_cases_multiplier - 1
@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)
@ -31,6 +31,7 @@ module.exports = class Test
for user in @users for user in @users
u.getConnector().join(user.getConnector()) # TODO: change the test-connector to make this more convenient u.getConnector().join(user.getConnector()) # TODO: change the test-connector to make this more convenient
@users.push u @users.push u
@initUsers?(@users[0])
@flushAll() @flushAll()
# is called by implementing class # is called by implementing class
@ -74,17 +75,17 @@ module.exports = class Test
f : (y)=> # INSERT TEXT f : (y)=> # INSERT TEXT
y y
pos = _.random 0, (y.val().length-1) pos = _.random 0, (y.val().length-1)
y.insertText pos, @getRandomText() y.insert pos, @getRandomText()
null null
types: [types.WordType] types: [types.String]
, ,
f : (y)-> # DELETE TEXT f : (y)-> # DELETE TEXT
if y.val().length > 0 if y.val().length > 0
pos = _.random 0, (y.val().length-1) pos = _.random 0, (y.val().length-1) # TODO: put here also arbitrary number (test behaviour in error cases)
length = _.random 0, (y.val().length - pos) length = _.random 0, (y.val().length - pos)
ops1 = y.deleteText pos, length ops1 = y.delete pos, length
undefined undefined
types : [types.WordType] types : [types.String]
] ]
getRandomRoot: (user_num)-> getRandomRoot: (user_num)->
throw new Error "overwrite me!" throw new Error "overwrite me!"
@ -99,7 +100,7 @@ module.exports = class Test
y instanceof type y instanceof type
if choices.length is 0 if choices.length is 0
throw new Error "You forgot to specify a test generation methot for this Operation!" throw new Error "You forgot to specify a test generation methot for this Operation! (#{y.type})"
i = _.random 0, (choices.length-1) i = _.random 0, (choices.length-1)
choices[i].f y choices[i].f y

View File

@ -19,6 +19,9 @@ class TextTest extends Test
conn = new Connector userId conn = new Connector userId
new Yatta conn new Yatta conn
initUsers: (u)->
u.val("TextTest","","mutable")
getRandomRoot: (user_num)-> getRandomRoot: (user_num)->
@users[user_num].val("TextTest") @users[user_num].val("TextTest")
@ -29,21 +32,17 @@ describe "TextFramework", ->
beforeEach (done)-> beforeEach (done)->
@timeout 50000 @timeout 50000
@yTest = new TextTest() @yTest = new TextTest()
@users = @yTest.users
test_user_connector = new Connector 'test_user'
@test_user = @yTest.makeNewUser 'test_user', test_user_connector
test_user_connector.join @users[0].connector
@users[0].val("TextTest","","mutable")
@yTest.flushAll()
done() done()
it "simple multi-char insert", -> it "simple multi-char insert", ->
u = @yTest.users[0].val("TextTest") u = @yTest.users[0].val("TextTest")
u.insertText 0, "abc" u.insert 0, "abc"
u = @yTest.users[1].val("TextTest") u = @yTest.users[1].val("TextTest")
u.insertText 0, "xyz" u.insert 0, "xyz"
@yTest.compareAll() @yTest.compareAll()
expect(u.val()).to.equal("abcxyz") u.delete 0, 1
@yTest.compareAll()
expect(u.val()).to.equal("bcxyz")
it "Observers work on shared Text (insert type observers, local and foreign)", -> it "Observers work on shared Text (insert type observers, local and foreign)", ->
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest") u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
@ -59,7 +58,7 @@ describe "TextFramework", ->
expect(change.changedBy).to.equal('0') expect(change.changedBy).to.equal('0')
last_task = "observer1" last_task = "observer1"
u.observe observer1 u.observe observer1
u.insertText 1, "a" u.insert 1, "a"
expect(last_task).to.equal("observer1") expect(last_task).to.equal("observer1")
u.unobserve observer1 u.unobserve observer1
@ -74,7 +73,7 @@ describe "TextFramework", ->
last_task = "observer2" last_task = "observer2"
u.observe observer2 u.observe observer2
v = @yTest.users[1].val("TextTest") v = @yTest.users[1].val("TextTest")
v.insertText 0, "x" v.insert 0, "x"
@yTest.flushAll() @yTest.flushAll()
expect(last_task).to.equal("observer2") expect(last_task).to.equal("observer2")
u.unobserve observer2 u.unobserve observer2
@ -93,7 +92,7 @@ describe "TextFramework", ->
expect(change.changedBy).to.equal('0') expect(change.changedBy).to.equal('0')
last_task = "observer1" last_task = "observer1"
u.observe observer1 u.observe observer1
u.deleteText 1, 1 u.delete 1, 1
expect(last_task).to.equal("observer1") expect(last_task).to.equal("observer1")
u.unobserve observer1 u.unobserve observer1
@ -108,7 +107,7 @@ describe "TextFramework", ->
last_task = "observer2" last_task = "observer2"
u.observe observer2 u.observe observer2
v = @yTest.users[1].val("TextTest") v = @yTest.users[1].val("TextTest")
v.deleteText 0, 1 v.delete 0, 1
@yTest.flushAll() @yTest.flushAll()
expect(last_task).to.equal("observer2") expect(last_task).to.equal("observer2")
u.unobserve observer2 u.unobserve observer2

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long