module.exports = ()-> # @see Engine.parse ops = {} execution_listener = [] # # @private # @abstract # @nodoc # A generic interface to ops. # # An operation has the following methods: # * _encode: encodes an operation (needed only if instance of this operation is sent). # * execute: execute the effects of this operations. Good examples are Insert-type and AddName-type # * val: in the case that the operation holds a value # # Furthermore an encodable operation has a parser. We extend the parser object in order to parse encoded operations. # class ops.Operation # # @param {Object} uid A unique identifier. # If uid is undefined, a new uid will be created before at the end of the execution sequence # constructor: (custom_type, uid, content, content_operations)-> if custom_type? @custom_type = custom_type @is_deleted = false @garbage_collected = false @event_listeners = [] # TODO: rename to observers or sth like that if uid? @uid = uid # see encode to see, why we are doing it this way if content is undefined # nop else if content? and content.creator? @saveOperation 'content', content else @content = content if content_operations? @content_operations = {} for name, op of content_operations @saveOperation name, op, 'content_operations' type: "Operation" getContent: (name)-> if @content? if @content.getCustomType? @content.getCustomType() else if @content.constructor is Object if name? if @content[name]? @content[name] else @content_operations[name].getCustomType() else content = {} for n,v of @content content[n] = v if @content_operations? for n,v of @content_operations v = v.getCustomType() content[n] = v content else @content else @content retrieveSub: ()-> throw new Error "sub properties are not enable on this operation type!" # # Add an event listener. It depends on the operation which events are supported. # @param {Function} f f is executed in case the event fires. # observe: (f)-> @event_listeners.push f # # Deletes function from the observer list # @see Operation.observe # # @overload unobserve(event, f) # @param f {Function} The function that you want to delete unobserve: (f)-> @event_listeners = @event_listeners.filter (g)-> f isnt g # # Deletes all subscribed event listeners. # This should be called, e.g. after this has been replaced. # (Then only one replace event should fire. ) # This is also called in the cleanup method. deleteAllObservers: ()-> @event_listeners = [] delete: ()-> (new ops.Delete undefined, @).execute() null # # Fire an event. # TODO: Do something with timeouts. You don't want this to fire for every operation (e.g. insert). # TODO: do you need callEvent+forwardEvent? Only one suffices probably callEvent: ()-> if @custom_type? callon = @getCustomType() else callon = @ @forwardEvent callon, arguments... # # Fire an event and specify in which context the listener is called (set 'this'). # TODO: do you need this ? forwardEvent: (op, args...)-> for f in @event_listeners f.call op, args... isDeleted: ()-> @is_deleted applyDelete: (garbagecollect = true)-> if not @garbage_collected #console.log "applyDelete: #{@type}" @is_deleted = true if garbagecollect @garbage_collected = true @HB.addToGarbageCollector @ cleanup: ()-> #console.log "cleanup: #{@type}" @HB.removeOperation @ @deleteAllObservers() # # Set the parent of this operation. # setParent: (@parent)-> # # Get the parent of this operation. # getParent: ()-> @parent # # Computes a unique identifier (uid) that identifies this operation. # getUid: ()-> if not @uid.noOperation? @uid else if @uid.alt? # could be (safely) undefined map_uid = @uid.alt.cloneUid() map_uid.sub = @uid.sub map_uid else undefined cloneUid: ()-> uid = {} for n,v of @getUid() uid[n] = v uid # # @private # If not already done, set the uid # Add this to the HB # Notify the all the listeners. # execute: ()-> if @validateSavedOperations() @is_executed = true if not @uid? # When this operation was created without a uid, then set it here. # There is only one other place, where this can be done - before an Insertion # is executed (because we need the creator_id) @uid = @HB.getNextOperationIdentifier() if not @uid.noOperation? @HB.addOperation @ for l in execution_listener l @_encode() @ else false # # @private # Operations may depend on other operations (linked lists, etc.). # The saveOperation and validateSavedOperations methods provide # an easy way to refer to these operations via an uid or object reference. # # For example: We can create a new Delete operation that deletes the operation $o like this # - var d = new Delete(uid, $o); or # - var d = new Delete(uid, $o.getUid()); # Either way we want to access $o via d.deletes. In the second case validateSavedOperations must be called first. # # @overload saveOperation(name, op_uid) # @param {String} name The name of the operation. After validating (with validateSavedOperations) the instantiated operation will be accessible via this[name]. # @param {Object} op_uid A uid that refers to an operation # @overload saveOperation(name, op) # @param {String} name The name of the operation. After calling this function op is accessible via this[name]. # @param {Operation} op An Operation object # saveOperation: (name, op, base = "this")-> if op? and op._getModel? op = op._getModel(@custom_types, @operations) # # Every instance of $Operation must have an $execute function. # We use duck-typing to check if op is instantiated since there # could exist multiple classes of $Operation # if not op? # nop else if op.execute? or not (op.op_number? and op.creator?) # is instantiated, or op is string. Currently "Delimiter" is saved as string # (in combination with @parent you can retrieve the delimiter..) if base is "this" @[name] = op else dest = @[base] paths = name.split("/") last_path = paths.pop() for path in paths dest = dest[path] dest[last_path] = op else # not initialized. Do it when calling $validateSavedOperations() @unchecked ?= {} @unchecked[base] ?= {} @unchecked[base][name] = op # # @private # After calling this function all not instantiated operations will be accessible. # @see Operation.saveOperation # # @return [Boolean] Whether it was possible to instantiate all operations. # validateSavedOperations: ()-> uninstantiated = {} success = true for base_name, base of @unchecked for name, op_uid of base op = @HB.getOperation op_uid if op if base_name is "this" @[name] = op else dest = @[base_name] paths = name.split("/") last_path = paths.pop() for path in paths dest = dest[path] dest[last_path] = op else uninstantiated[base_name] ?= {} uninstantiated[base_name][name] = op_uid success = false if not success @unchecked = uninstantiated return false else delete @unchecked return @ getCustomType: ()-> if not @custom_type? # throw new Error "This operation was not initialized with a custom type" @ else if @custom_type.constructor is String # has not been initialized yet (only the name is specified) Type = @custom_types for t in @custom_type.split(".") Type = Type[t] @custom_type = new Type() @custom_type._setModel @ @custom_type # # @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 if @content?.getUid? json.content = @content.getUid() else json.content = @content if @content_operations? operations = {} for n,o of @content_operations if o._getModel? o = o._getModel(@custom_types, @operations) operations[n] = o.getUid() json.content_operations = operations json # # @nodoc # A simple Delete-type operation that deletes an operation. # class ops.Delete extends ops.Operation # # @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created. # @param {Object} deletes UID or reference of the operation that this to be deleted. # constructor: (custom_type, uid, deletes)-> @saveOperation 'deletes', deletes super custom_type, uid type: "Delete" # # @private # Convert all relevant information of this operation to the json-format. # This result can be sent to other clients. # _encode: ()-> { 'type': "Delete" 'uid': @getUid() 'deletes': @deletes.getUid() } # # @private # Apply the deletion. # execute: ()-> if @validateSavedOperations() res = super if res @deletes.applyDelete @ res else false # # Define how to parse Delete operations. # ops.Delete.parse = (o)-> { 'uid' : uid 'deletes': deletes_uid } = o new this(null, uid, deletes_uid) # # @nodoc # A simple insert-type operation. # # An insert operation is always positioned between two other insert operations. # Internally this is realized as associative lists, whereby each insert operation has a predecessor and a successor. # For the sake of efficiency we maintain two lists: # - The short-list (abbrev. sl) maintains only the operations that are not deleted (unimplemented, good idea?) # - The complete-list (abbrev. cl) maintains all operations # class ops.Insert extends ops.Operation # # @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} next_cl The successor of this operation in the complete-list (cl) # constructor: (custom_type, content, content_operations, parent, uid, prev_cl, next_cl, origin)-> @saveOperation 'parent', parent @saveOperation 'prev_cl', prev_cl @saveOperation 'next_cl', next_cl if origin? @saveOperation 'origin', origin else @saveOperation 'origin', prev_cl super custom_type, uid, content, content_operations type: "Insert" val: ()-> @getContent() getNext: (i=1)-> n = @ while i > 0 and n.next_cl? n = n.next_cl if not n.is_deleted i-- if n.is_deleted null n getPrev: (i=1)-> n = @ while i > 0 and n.prev_cl? n = n.prev_cl if not n.is_deleted i-- if n.is_deleted null else n # # set content to null and other stuff # @private # applyDelete: (o)-> @deleted_by ?= [] callLater = false 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? @deleted_by.push o garbagecollect = false if @next_cl.isDeleted() garbagecollect = true super garbagecollect if callLater @parent.callOperationSpecificDeleteEvents(this, o) if @prev_cl? and @prev_cl.isDeleted() and @prev_cl.garbage_collected isnt true # garbage collect prev_cl @prev_cl.applyDelete() cleanup: ()-> if @next_cl.isDeleted() # delete all ops that delete this insertion for d in @deleted_by d.cleanup() # throw new Error "right is not deleted. inconsistency!, wrararar" # change origin references to the right o = @next_cl while o.type isnt "Delimiter" if o.origin is @ o.origin = @prev_cl o = o.next_cl # 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 # * NODE: We never delete Insertions! if @content instanceof ops.Operation and not (@content instanceof ops.Insert) @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. # Remember: this can only be garbage collected when next_cl is deleted # # @private # The amount of positions that $this operation was moved to the right. # getDistanceToOrigin: ()-> d = 0 o = @prev_cl while true if @origin is o break d++ o = o.prev_cl d # # @private # Include this operation in the associative lists. execute: ()-> if not @validateSavedOperations() return false 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 if not @origin? @origin = @prev_cl else if @origin is "Delimiter" @origin = @parent.beginning if not @next_cl? @next_cl = @parent.end if @prev_cl? distance_to_origin = @getDistanceToOrigin() # most cases: 0 o = @prev_cl.next_cl i = distance_to_origin # loop counter # $this has to find a unique position between origin and the next known character # case 1: $origin equals $o.origin: the $creator parameter decides if left or right # let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4 # o2,o3 and o4 origin is 1 (the position of o2) # there is the case that $this.creator < o2.creator, but o3.creator < $this.creator # then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex # therefore $this would be always to the right of o3 # case 2: $origin < $o.origin # if current $this insert_position > $o origin: $this ins # else $insert_position will not change # (maybe we encounter case 1 later, then this will be to the right of $o) # case 3: $origin > $o.origin # $this insert_position is to the left of $o (forever!) while true if o isnt @next_cl # $o happened concurrently if o.getDistanceToOrigin() is i # case 1 if o.uid.creator < @uid.creator @prev_cl = o distance_to_origin = i + 1 else # nop else if o.getDistanceToOrigin() < i # case 2 if i - distance_to_origin <= o.getDistanceToOrigin() @prev_cl = o distance_to_origin = i + 1 else #nop else # case 3 break i++ o = o.next_cl else # $this knows that $o exists, break # now reconnect everything @next_cl = @prev_cl.next_cl @prev_cl.next_cl = @ @next_cl.prev_cl = @ @setParent @prev_cl.getParent() # do Insertions always have a parent? super # notify the execution_listeners @parent.callOperationSpecificInsertEvents(this) @ # # Compute the position of this operation. # getPosition: ()-> position = 0 prev = @prev_cl while true if prev instanceof ops.Delimiter break if not prev.isDeleted() position++ prev = prev.prev_cl position # # Convert all relevant information of this operation to the json-format. # This result can be send to other clients. # _encode: (json = {})-> json.prev = @prev_cl.getUid() json.next = @next_cl.getUid() if @origin.type is "Delimiter" json.origin = "Delimiter" else if @origin isnt @prev_cl json.origin = @origin.getUid() # if not (json.prev? and json.next?) json.parent = @parent.getUid() super json ops.Insert.parse = (json)-> { 'content' : content 'content_operations' : content_operations 'uid' : uid 'prev': prev 'next': next 'origin' : origin 'parent' : parent } = json new this null, content, content_operations, parent, uid, prev, next, origin # # @nodoc # A delimiter is placed at the end and at the beginning of the associative lists. # This is necessary in order to have a beginning and an end even if the content # of the Engine is empty. # class ops.Delimiter extends ops.Operation # # @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} next_cl The successor of this operation in the complete-list (cl) # constructor: (prev_cl, next_cl, origin)-> @saveOperation 'prev_cl', prev_cl @saveOperation 'next_cl', next_cl @saveOperation 'origin', prev_cl super null, {noOperation: true} type: "Delimiter" applyDelete: ()-> super() o = @prev_cl while o? o.applyDelete() o = o.prev_cl undefined cleanup: ()-> super() # # @private # execute: ()-> if @unchecked?['next_cl']? super else if @unchecked?['prev_cl'] if @validateSavedOperations() if @prev_cl.next_cl? throw new Error "Probably duplicated operations" @prev_cl.next_cl = @ super else false else if @prev_cl? and not @prev_cl.next_cl? delete @prev_cl.unchecked.next_cl @prev_cl.next_cl = @ super else if @prev_cl? or @next_cl? or true # TODO: are you sure? This can happen right? super #else # throw new Error "Delimiter is unsufficient defined!" # # @private # _encode: ()-> { 'type' : @type 'uid' : @getUid() 'prev' : @prev_cl?.getUid() 'next' : @next_cl?.getUid() } ops.Delimiter.parse = (json)-> { 'uid' : uid 'prev' : prev 'next' : next } = json new this(uid, prev, next) # This is what this module exports after initializing it with the HistoryBuffer { 'operations' : ops 'execution_listener' : execution_listener }