there are some cases that may lead to inconsistencies. Currently, only the master-slave method is a reliable sync method
224 lines
7.2 KiB
CoffeeScript
224 lines
7.2 KiB
CoffeeScript
|
|
#
|
|
# @nodoc
|
|
# An object that holds all applied operations.
|
|
#
|
|
# @note The HistoryBuffer is commonly abbreviated to HB.
|
|
#
|
|
class HistoryBuffer
|
|
|
|
#
|
|
# Creates an empty HB.
|
|
# @param {Object} user_id Creator of the HB.
|
|
#
|
|
constructor: (@user_id)->
|
|
@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 = 30000
|
|
@reserved_identifier_counter = 0
|
|
setTimeout @emptyGarbage, @garbageCollectTimeout
|
|
|
|
resetUserId: (id)->
|
|
own = @buffer[@user_id]
|
|
if own?
|
|
for o_name,o of own
|
|
if o.uid.creator?
|
|
o.uid.creator = id
|
|
if o.uid.alt?
|
|
o.uid.alt.creator = id
|
|
if @buffer[id]?
|
|
throw new Error "You are re-assigning an old user id - this is not (yet) possible!"
|
|
@buffer[id] = own
|
|
delete @buffer[@user_id]
|
|
if @operation_counter[@user_id]?
|
|
@operation_counter[id] = @operation_counter[@user_id]
|
|
delete @operation_counter[@user_id]
|
|
@user_id = id
|
|
|
|
emptyGarbage: ()=>
|
|
for o in @garbage
|
|
#if @getOperationCounter(o.uid.creator) > o.uid.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.
|
|
#
|
|
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)->
|
|
|
|
#
|
|
# 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.
|
|
#
|
|
getReservedUniqueIdentifier: ()->
|
|
{
|
|
creator : '_'
|
|
op_number : "_#{@reserved_identifier_counter++}"
|
|
doSync: false
|
|
}
|
|
|
|
#
|
|
# Get the operation counter that describes the current state of the document.
|
|
#
|
|
getOperationCounter: (user_id)->
|
|
if not user_id?
|
|
res = {}
|
|
for user,ctn of @operation_counter
|
|
res[user] = ctn
|
|
res
|
|
else
|
|
@operation_counter[user_id]
|
|
|
|
isExpectedOperation: (o)->
|
|
@operation_counter[o.uid.creator] ?= 0
|
|
o.uid.op_number <= @operation_counter[o.uid.creator]
|
|
true #TODO: !! this could break stuff. But I dunno why
|
|
|
|
#
|
|
# Encode this operation in such a way that it can be parsed by remote peers.
|
|
# TODO: Make this more efficient!
|
|
_encode: (state_vector={})->
|
|
json = []
|
|
unknown = (user, o_number)->
|
|
if (not user?) or (not o_number?)
|
|
throw new Error "dah!"
|
|
not state_vector[user]? or state_vector[user] <= o_number
|
|
|
|
for u_name,user of @buffer
|
|
# TODO next, if @state_vector[user] <= state_vector[user]
|
|
for o_number,o of user
|
|
if (not o.uid.noOperation?) and o.uid.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? # 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.uid.creator, o_next.uid.op_number)
|
|
o_next = o_next.next_cl
|
|
o_json.next = o_next.getUid()
|
|
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.uid.creator, o_prev.uid.op_number)
|
|
o_prev = o_prev.prev_cl
|
|
o_json.prev = o_prev.getUid()
|
|
json.push o_json
|
|
|
|
json
|
|
|
|
#
|
|
# Get the number of operations that were created by a user.
|
|
# Accordingly you will get the next operation number that is expected from that user.
|
|
# This will increment the operation counter.
|
|
#
|
|
getNextOperationIdentifier: (user_id)->
|
|
if not user_id?
|
|
user_id = @user_id
|
|
if not @operation_counter[user_id]?
|
|
@operation_counter[user_id] = 0
|
|
uid =
|
|
'creator' : user_id
|
|
'op_number' : @operation_counter[user_id]
|
|
'doSync' : true
|
|
@operation_counter[user_id]++
|
|
uid
|
|
|
|
#
|
|
# Retrieve an operation from a unique id.
|
|
#
|
|
# when uid has a "sub" property, the value of it will be applied
|
|
# on the operations retrieveSub method (which must! be defined)
|
|
#
|
|
getOperation: (uid)->
|
|
if uid.uid?
|
|
uid = uid.uid
|
|
o = @buffer[uid.creator]?[uid.op_number]
|
|
if uid.sub? and o?
|
|
o.retrieveSub uid.sub
|
|
else
|
|
o
|
|
|
|
#
|
|
# Add an operation to the HB. Note that this will not link it against
|
|
# other operations (it wont executed)
|
|
#
|
|
addOperation: (o)->
|
|
if not @buffer[o.uid.creator]?
|
|
@buffer[o.uid.creator] = {}
|
|
if @buffer[o.uid.creator][o.uid.op_number]?
|
|
throw new Error "You must not overwrite operations!"
|
|
if (o.uid.op_number.constructor isnt String) and (not @isExpectedOperation(o)) and (not o.fromHB?) # you already do this in the engine, so delete it here!
|
|
throw new Error "this operation was not expected!"
|
|
@addToCounter(o)
|
|
@buffer[o.uid.creator][o.uid.op_number] = o
|
|
o
|
|
|
|
removeOperation: (o)->
|
|
delete @buffer[o.uid.creator]?[o.uid.op_number]
|
|
|
|
# When the HB determines inconsistencies, then the invokeSync
|
|
# handler wil be called, which should somehow invoke the sync with another collaborator.
|
|
# The parameter of the sync handler is the user_id with wich an inconsistency was determined
|
|
setInvokeSyncHandler: (f)->
|
|
@invokeSync = f
|
|
|
|
# empty per default # TODO: do i need this?
|
|
invokeSync: ()->
|
|
|
|
# after you received the HB of another user (in the sync process),
|
|
# you renew your own state_vector to the state_vector of the other user
|
|
renewStateVector: (state_vector)->
|
|
for user,state of state_vector
|
|
if ((not @operation_counter[user]?) or (@operation_counter[user] < state_vector[user])) and state_vector[user]?
|
|
@operation_counter[user] = state_vector[user]
|
|
|
|
#
|
|
# Increment the operation_counter that defines the current state of the Engine.
|
|
#
|
|
addToCounter: (o)->
|
|
if not @operation_counter[o.uid.creator]?
|
|
@operation_counter[o.uid.creator] = 0
|
|
if typeof o.uid.op_number is 'number' and o.uid.creator isnt @getUserId()
|
|
# TODO: check if operations are send in order
|
|
if o.uid.op_number is @operation_counter[o.uid.creator]
|
|
@operation_counter[o.uid.creator]++
|
|
else
|
|
@invokeSync o.uid.creator
|
|
|
|
#if @operation_counter[o.uid.creator] isnt (o.uid.op_number + 1)
|
|
#console.log (@operation_counter[o.uid.creator] - (o.uid.op_number + 1))
|
|
#console.log o
|
|
#throw new Error "You don't receive operations in the proper order. Try counting like this 0,1,2,3,4,.. ;)"
|
|
|
|
module.exports = HistoryBuffer
|