Operations are now Garbage Collected!

This commit is contained in:
Kevin Jahns
2014-09-17 16:10:41 +02:00
parent b03f477a3f
commit 68c17f1876
63 changed files with 2420 additions and 934 deletions

View File

@@ -27,6 +27,7 @@ module.exports = (user_list)->
if not (user_list?.length is 0)
@engine.applyOps user_list[0].getHistoryBuffer()._encode()
@HB.setManualGarbageCollect()
@unexecuted = {}
#

View File

@@ -21,16 +21,25 @@ class JsonFramework
type_manager = json_types_uninitialized @HB
@types = type_manager.types
@engine = new Engine @HB, type_manager.parser
@HB.engine = @engine # TODO: !! only for debugging
@connector = new Connector @engine, @HB, type_manager.execution_listener, @
first_word = new @types.JsonType @HB.getReservedUniqueIdentifier()
first_word = new @types.JsonType(@HB.getReservedUniqueIdentifier())
@HB.addOperation(first_word).execute()
@root_element = first_word
uid_beg = @HB.getReservedUniqueIdentifier()
uid_end = @HB.getReservedUniqueIdentifier()
beg = @HB.addOperation(new @types.Delimiter uid_beg, undefined, uid_end).execute()
end = @HB.addOperation(new @types.Delimiter uid_end, beg, undefined).execute()
@root_element = new @types.ReplaceManager undefined, @HB.getReservedUniqueIdentifier(), beg, end
@HB.addOperation(@root_element).execute()
@root_element.replace first_word, @HB.getReservedUniqueIdentifier()
#
# @return JsonType
#
getSharedObject: ()->
@root_element
@root_element.val()
#
# Get the initialized connector.
@@ -48,7 +57,7 @@ class JsonFramework
# @see JsonType.setMutableDefault
#
setMutableDefault: (mutable)->
@root_element.setMutableDefault(mutable)
@getSharedObject().setMutableDefault(mutable)
#
# Get the UserId from the HistoryBuffer object.
@@ -62,31 +71,31 @@ class JsonFramework
# @see JsonType.toJson
#
toJson : ()->
@root_element.toJson()
@getSharedObject().toJson()
#
# @see JsonType.val
#
val : (name, content, mutable)->
@root_element.val(name, content, mutable)
@getSharedObject().val(name, content, mutable)
#
# @see Operation.on
#
on: ()->
@root_element.on arguments...
@getSharedObject().on arguments...
#
# @see Operation.deleteListener
#
deleteListener: ()->
@root_element.deleteListener arguments...
@getSharedObject().deleteListener arguments...
#
# @see JsonType.value
#
Object.defineProperty JsonFramework.prototype, 'value',
get : -> @root_element.value
get : -> @getSharedObject().value
set : (o)->
if o.constructor is {}.constructor
for o_name,o_obj of o

View File

@@ -7,6 +7,8 @@
#
class HistoryBuffer
#
# Creates an empty HB.
# @param {Object} user_id Creator of the HB.
@@ -15,6 +17,23 @@ class HistoryBuffer
@operation_counter = {}
@buffer = {}
@change_listeners = []
@garbage = [] # Will be cleaned on next call of garbageCollector
@trash = [] # Is deleted. Wait until it is not used anymore.
@performGarbageCollection = true
@garbageCollectTimeout = 1000
@reserved_identifier_counter = 0
setTimeout @emptyGarbage, @garbageCollectTimeout
emptyGarbage: ()=>
for o in @garbage
#if @getOperationCounter(o.creator) > o.op_number
o.cleanup?()
@garbage = @trash
@trash = []
if @garbageCollectTimeout isnt -1
@garbageCollectTimeoutId = setTimeout @emptyGarbage, @garbageCollectTimeout
undefined
#
# Get the user id with wich the History Buffer was initialized.
@@ -22,8 +41,26 @@ class HistoryBuffer
getUserId: ()->
@user_id
addToGarbageCollector: ()->
if @performGarbageCollection
for o in arguments
if o?
@garbage.push o
stopGarbageCollection: ()->
@performGarbageCollection = false
@setManualGarbageCollect()
@garbage = []
@trash = []
setManualGarbageCollect: ()->
@garbageCollectTimeout = -1
clearTimeout @garbageCollectTimeoutId
@garbageCollectTimeoutId = undefined
setGarbageCollectTimeout: (@garbageCollectTimeout)->
#
# There is only one reserved unique identifier (uid), so use it wisely.
# I propose to use it in your Framework, to create something like a root element.
# An operation with this identifier is not propagated to other clients.
# This is why everybode must create the same operation with this uid.
@@ -31,17 +68,21 @@ class HistoryBuffer
getReservedUniqueIdentifier: ()->
{
creator : '_'
op_number : '_'
op_number : "_#{@reserved_identifier_counter++}"
}
#
# Get the operation counter that describes the current state of the document.
#
getOperationCounter: ()->
res = {}
for user,ctn of @operation_counter
res[user] = ctn
res
getOperationCounter: (user_id)->
if not user_id?
res = {}
for user,ctn of @operation_counter
res[user] = ctn
res
else
@operation_counter[user_id]
#
# Encode this operation in such a way that it can be parsed by remote peers.
@@ -55,16 +96,16 @@ class HistoryBuffer
for u_name,user of @buffer
for o_number,o of user
if (not isNaN(parseInt(o_number))) and unknown(u_name, o_number)
if o.doSync and unknown(u_name, o_number)
# its necessary to send it, and not known in state_vector
o_json = o._encode()
if o.next_cl?
if o.next_cl? # applies for all ops but the most right delimiter!
# search for the next _known_ operation. (When state_vector is {} then this is the Delimiter)
o_next = o.next_cl
while o_next.next_cl? and unknown(o_next.creator, o_next.op_number)
o_next = o_next.next_cl
o_json.next = o_next.getUid()
else if o.prev_cl?
else if o.prev_cl? # most right delimiter only!
# same as the above with prev.
o_prev = o.prev_cl
while o_prev.prev_cl? and unknown(o_prev.creator, o_prev.op_number)
@@ -111,6 +152,9 @@ class HistoryBuffer
@buffer[o.creator][o.op_number] = o
o
removeOperation: (o)->
delete @buffer[o.creator]?[o.op_number]
#
# Increment the operation_counter that defines the current state of the Engine.
#

View File

@@ -23,13 +23,20 @@ module.exports = (HB)->
# @see HistoryBuffer.getNextOperationIdentifier
#
constructor: (uid)->
if not uid?
@is_deleted = false
@doSync = true
@garbage_collected = false
if uid?
@doSync = not isNaN(parseInt(uid.op_number))
else
uid = HB.getNextOperationIdentifier()
{
'creator': @creator
'op_number' : @op_number
} = uid
type: "Insert"
#
# Add an event listener. It depends on the operation which events are supported.
# @param {String} event Name of the event.
@@ -76,6 +83,21 @@ module.exports = (HB)->
for f in @event_listeners[event]
f.call op, event, 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 @
#
# Set the parent of this operation.
#
@@ -91,7 +113,10 @@ module.exports = (HB)->
# Computes a unique identifier (uid) that identifies this operation.
#
getUid: ()->
{ 'creator': @creator, 'op_number': @op_number }
{ 'creator': @creator, 'op_number': @op_number , 'sync': @doSync}
dontSync: ()->
@doSync = false
#
# @private
@@ -162,7 +187,7 @@ module.exports = (HB)->
#
# @nodoc
# A simple Delete-type operation that deletes an Insert-type operation.
# A simple Delete-type operation that deletes an operation.
#
class Delete extends Operation
@@ -174,6 +199,8 @@ module.exports = (HB)->
@saveOperation 'deletes', deletes
super uid
type: "Delete"
#
# @private
# Convert all relevant information of this operation to the json-format.
@@ -235,21 +262,46 @@ module.exports = (HB)->
@saveOperation 'origin', prev_cl
super uid
type: "Insert"
#
# set content to null and other stuff
# @private
#
applyDelete: (o)->
@deleted_by ?= []
@deleted_by.push o
if @parent? and @deleted_by.length is 1
if @parent? and not @isDeleted()
# call iff wasn't deleted earlyer
@parent.callEvent "delete", @
if o?
@deleted_by.push o
garbagecollect = false
if @prev_cl.isDeleted()
garbagecollect = true
super garbagecollect
if @next_cl.isDeleted()
# garbage collect next_cl
@next_cl.applyDelete()
cleanup: ()->
# TODO: Debugging
if @prev_cl.isDeleted()
# delete all ops that delete this insertion
for d in @deleted_by
d.cleanup()
# throw new Error "left is not deleted. inconsistency!, wrararar"
# delete 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
super
#
# If isDeleted() is true this operation won't be maintained in the sl
#
isDeleted: ()->
@deleted_by?.length > 0
#
# @private
@@ -262,45 +314,22 @@ module.exports = (HB)->
if @origin is o
break
d++
#TODO: delete this
if @ is @prev_cl
throw new Error "this should not happen ;) "
o = o.prev_cl
d
#
# @private
# Update the short list
# TODO (Unused)
update_sl: ()->
o = @prev_cl
update: (dest_cl,dest_sl)->
while true
if o.isDeleted()
o = o[dest_cl]
else
@[dest_sl] = o
break
update "prev_cl", "prev_sl"
update "next_cl", "prev_sl"
#
# @private
# Include this operation in the associative lists.
#
execute: ()->
if @is_executed?
return @
if not @validateSavedOperations()
return false
else
if @prev_cl?.validateSavedOperations() and @next_cl?.validateSavedOperations() and @prev_cl.next_cl isnt @
distance_to_origin = 0
if @prev_cl?
distance_to_origin = @getDistanceToOrigin() # most cases: 0
o = @prev_cl.next_cl
i = 0
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
@@ -314,10 +343,6 @@ module.exports = (HB)->
# case 3: $origin > $o.origin
# $this insert_position is to the left of $o (forever!)
while true
if not o?
# TODO: Debugging
console.log JSON.stringify @prev_cl.getUid()
console.log JSON.stringify @next_cl.getUid()
if o isnt @next_cl
# $o happened concurrently
if o.getDistanceToOrigin() is i
@@ -346,6 +371,7 @@ module.exports = (HB)->
@next_cl = @prev_cl.next_cl
@prev_cl.next_cl = @
@next_cl.prev_cl = @
parent = @prev_cl?.getParent()
if parent?
@setParent parent
@@ -361,7 +387,7 @@ module.exports = (HB)->
while true
if prev instanceof Delimiter
break
if prev.isDeleted? and not prev.isDeleted()
if not prev.isDeleted()
position++
prev = prev.prev_cl
position
@@ -370,7 +396,7 @@ module.exports = (HB)->
# @nodoc
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
#
class ImmutableObject extends Insert
class ImmutableObject extends Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
@@ -379,6 +405,8 @@ module.exports = (HB)->
constructor: (uid, @content, prev, next, origin)->
super uid, prev, next, origin
type: "ImmutableObject"
#
# @return [String] The content of this operation.
#
@@ -398,8 +426,8 @@ module.exports = (HB)->
json['prev'] = @prev_cl.getUid()
if @next_cl?
json['next'] = @next_cl.getUid()
if @origin? and @origin isnt @prev_cl
json["origin"] = @origin.getUid()
if @origin? # and @origin isnt @prev_cl
json["origin"] = @origin().getUid()
json
parser['ImmutableObject'] = (json)->
@@ -432,11 +460,18 @@ module.exports = (HB)->
@saveOperation 'origin', prev_cl
super uid
#
# If isDeleted() is true this operation won't be maintained in the sl
#
isDeleted: ()->
false
type: "Delimiter"
applyDelete: ()->
super()
o = @next_cl
while o?
o.applyDelete()
o = o.next_cl
undefined
cleanup: ()->
super()
#
# @private

View File

@@ -123,6 +123,11 @@ module.exports = (HB)->
#
type: "JsonType"
applyDelete: ()->
super()
cleanup: ()->
super()
#
# Transform this to a Json and loose all the sharing-abilities (the new object will be a deep clone)!
# @return {Json}
@@ -147,16 +152,18 @@ module.exports = (HB)->
# @see WordType.setReplaceManager
# Sets the parent of this JsonType object.
#
setReplaceManager: (rm)->
@parent = rm.parent
setReplaceManager: (replace_manager)->
@replace_manager = replace_manager
@on ['change','addProperty'], ()->
rm.parent.forwardEvent this, arguments...
if replace_manager.parent?
replace_manager.parent.forwardEvent this, arguments...
#
# Get the parent of this JsonType.
# @return {JsonType}
#
getParent: ()->
@parent
@replace_manager.parent
#
# Whether the default is 'mutable' (true) or 'immutable' (false)
@@ -196,8 +203,9 @@ module.exports = (HB)->
if typeof name is 'object'
# Special case. First argument is an object. Then the second arg is mutable.
# Keep that in mind when reading the following..
for o_name,o of name
@val(o_name,o,content)
json = new JsonType undefined, name, content
HB.addOperation(json).execute()
@replace_manager.replace json
@
else if name? and (content? or content is null)
if mutable?

View File

@@ -18,6 +18,16 @@ module.exports = (HB)->
@map = {}
super uid
type: "MapManager"
applyDelete: ()->
for name,p of @map
p.applyDelete()
super()
cleanup: ()->
super()
#
# @see JsonTypes.val
#
@@ -60,6 +70,14 @@ module.exports = (HB)->
@saveOperation 'map_manager', map_manager
super uid
type: "AddName"
applyDelete: ()->
super()
cleanup: ()->
super()
#
# If map_manager doesn't have the property name, then add it.
# The ReplaceManager that is being written on the property is unique
@@ -81,6 +99,7 @@ module.exports = (HB)->
end = HB.addOperation(new types.Delimiter uid_end, beg, undefined).execute()
@map_manager.map[@name] = HB.addOperation(new ReplaceManager undefined, uid_r, beg, end)
@map_manager.map[@name].setParent @map_manager, @name
(@map_manager.map[@name].add_name_ops ?= []).push @
@map_manager.map[@name].execute()
super
@@ -107,7 +126,7 @@ module.exports = (HB)->
# @nodoc
# Manages a list of Insert-type operations.
#
class ListManager extends types.Insert
class ListManager extends types.Operation
#
# A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!)
@@ -126,6 +145,8 @@ module.exports = (HB)->
@end.execute()
super uid, prev, next, origin
type: "ListManager"
#
# @private
# @see Operation.execute
@@ -195,6 +216,22 @@ module.exports = (HB)->
if initial_content?
@replace initial_content
type: "ReplaceManager"
applyDelete: ()->
o = @beginning
while o?
o.applyDelete()
o = o.next_cl
# if this was created by an AddName operation, delete it too
if @add_name_ops?
for o in @add_name_ops
o.applyDelete()
super()
cleanup: ()->
super()
#
# Replace the existing word with a new word.
#
@@ -205,6 +242,7 @@ module.exports = (HB)->
o = @getLastOperation()
op = new Replaceable content, @, replaceable_uid, o, o.next_cl
HB.addOperation(op).execute()
undefined
#
# Add change listeners for parent.
@@ -222,7 +260,7 @@ module.exports = (HB)->
@deleteListener 'addProperty', addPropertyListener
@on 'insert', addPropertyListener
super parent
#
# Get the value of this WordType
# @return {String}
@@ -247,8 +285,8 @@ module.exports = (HB)->
if @prev_cl? and @next_cl?
json['prev'] = @prev_cl.getUid()
json['next'] = @next_cl.getUid()
if @origin? and @origin isnt @prev_cl
json["origin"] = @origin.getUid()
if @origin? # and @origin isnt @prev_cl
json["origin"] = @origin().getUid()
json
parser["ReplaceManager"] = (json)->
@@ -279,10 +317,12 @@ module.exports = (HB)->
constructor: (content, parent, uid, prev, next, origin)->
@saveOperation 'content', content
@saveOperation 'parent', parent
if not (prev? and next? and content?)
throw new Error "You must define content, prev, and next for Replaceable-types!"
if not (prev? and next?)
throw new Error "You must define prev, and next for Replaceable-types!"
super uid, prev, next, origin
type: "Replaceable"
#
# Return the content that this operation holds.
#
@@ -295,6 +335,17 @@ module.exports = (HB)->
replace: (content)->
@parent.replace content
applyDelete: ()->
if @content?
@content.applyDelete()
@content.dontSync()
@beforeDelete = @content # TODO!!!!!!!!!!
@content = null
super
cleanup: ()->
super
#
# If possible set the replace manager in the content.
# @see WordType.setReplaceManager
@@ -303,8 +354,15 @@ module.exports = (HB)->
if not @validateSavedOperations()
return false
else
@content.setReplaceManager?(@parent)
super
@content?.setReplaceManager?(@parent)
ins_result = super()
if ins_result
if @next_cl.type is "Delimiter" and @prev_cl.type isnt "Delimiter"
@prev_cl.applyDelete()
else if @next_cl.type isnt "Delimiter"
@applyDelete()
return ins_result
#
# Encode this operation in such a way that it can be parsed by remote peers.
@@ -313,7 +371,7 @@ module.exports = (HB)->
json =
{
'type': "Replaceable"
'content': @content.getUid()
'content': @content?.getUid()
'ReplaceManager' : @parent.getUid()
'prev': @prev_cl.getUid()
'next': @next_cl.getUid()

View File

@@ -26,6 +26,9 @@ module.exports = (HB)->
if not (prev? and next?)
throw new Error "You must define prev, and next for TextInsert-types!"
super uid, prev, next, origin
type: "TextInsert"
#
# Retrieve the effective length of the $content of this operation.
#
@@ -35,13 +38,17 @@ module.exports = (HB)->
else
@content.length
applyDelete: ()->
@content = null
super
#
# The result will be concatenated with the results from the other insert operations
# in order to retrieve the content of the engine.
# @see HistoryBuffer.toExecutedArray
#
val: (current_position)->
if @isDeleted()
if @isDeleted() or not @content?
""
else
@content
@@ -59,7 +66,7 @@ module.exports = (HB)->
'prev': @prev_cl.getUid()
'next': @next_cl.getUid()
}
if @origin? and @origin isnt @prev_cl
if @origin isnt @prev_cl
json["origin"] = @origin.getUid()
json
@@ -98,16 +105,32 @@ module.exports = (HB)->
#
type: "WordType"
applyDelete: ()->
o = @beginning
while o?
o.applyDelete()
o = o.next_cl
super()
cleanup: ()->
super()
#
# Inserts a string into the word.
#
# @return {WordType} This WordType object.
#
insertText: (position, content)->
o = @getOperationByPosition position
# TODO: getOperationByPosition should return "(i-2)th" character
ith = @getOperationByPosition position # the (i-1)th character. e.g. "abc" a is the 0th character
left = ith.prev_cl # left is the non-deleted charather to the left of ith
while left.isDeleted()
left = left.prev_cl # find the first character to the left, that is not deleted. Case position is 0, its the Delimiter.
right = left.next_cl
for c in content
op = new TextInsert c, undefined, o.prev_cl, o
op = new TextInsert c, undefined, left, right
HB.addOperation(op).execute()
left = op
@
#
@@ -300,8 +323,8 @@ module.exports = (HB)->
json['prev'] = @prev_cl.getUid()
if @next_cl?
json['next'] = @next_cl.getUid()
if @origin? and @origin isnt @prev_cl
json["origin"] = @origin.getUid()
if @origin? # and @origin isnt @prev_cl
json["origin"] = @origin().getUid()
json
parser['WordType'] = (json)->