Finished support for new connector type

This commit is contained in:
DadaMonad
2014-11-25 15:51:30 +00:00
parent 03925ab32f
commit e1900e8561
170 changed files with 73970 additions and 8705 deletions

View File

@@ -0,0 +1,26 @@
#
# @param {Engine} engine The transformation engine
# @param {HistoryBuffer} HB
# @param {Array<Function>} execution_listener You must ensure that whenever an operation is executed, every function in this Array is called.
#
adaptConnector = (connector, engine, HB, execution_listener)->
send_ = (o)->
if o.uid.creator is HB.getUserId() and (typeof o.uid.op_number isnt "string")
connector.broadcast o
execution_listener.push send_
sendStateVector = ()->
HB.getOperationCounter()
sendHb = (state_vector)->
HB._encode(state_vector)
applyHb = (hb)->
engine.applyOpsCheckDouble hb
connector.whenSyncing sendStateVector, sendHb, applyHb
connector.whenReceiving (sender, op)->
if op.uid.creator isnt HB.getUserId()
engine.applyOp op
module.exports = adaptConnector

View File

@@ -1,145 +0,0 @@
#
# @param {Function} callback The callback is called when the connector is initialized.
# @param {String} initial_user_id Optional. You can set you own user_id (since the ids of duiclient are not always unique)
#
createIwcConnector = (callback, options)->
userIwcHandler = null
if options?
{iwcHandler: userIwcHandler} = options
iwcHandler = {}
duiClient = new DUIClient()
#@duiClient = new iwc.Client()
duiClient.connect (intent)->
iwcHandler[intent.action]?.map (f)->
setTimeout ()->
f intent
, 0
if userIwcHandler?
userIwcHandler intent
duiClient.initOK()
received_HB = null
#
# The Iwc Connector adds support for the Inter-Widget-Communication protocol that is used in the Role-SDK.
#
# You must not use your own IWC client when using this connector!!
#
# @see http://dbis.rwth-aachen.de/cms/projects/the-xmpp-experience#interwidget-communication
# @see http://dbis.rwth-aachen.de/cms/projects/ROLE
#
class IwcConnector
#
# @param {Engine} engine The transformation engine
# @param {HistoryBuffer} HB
# @param {Array<Function>} execution_listener You must ensure that whenever an operation is executed, every function in this Array is called.
# @param {YattaFramework} yatta The Yatta framework.
#
constructor: (@engine, @HB, @execution_listener, @yatta)->
@duiClient = duiClient
@iwcHandler = iwcHandler
send_ = (o)=>
if Object.getOwnPropertyNames(@initialized).length isnt 0
@send o
@execution_listener.push send_
@initialized = {}
receiveHB = (json)=>
HB = json.extras.HB
him = json.extras.user
this.engine.applyOpsCheckDouble HB
@initialized[him] = true
iwcHandler["Yatta_push_HB_element"] = [receiveHB]
@sendIwcIntent "Yatta_get_HB_element", @HB.getOperationCounter()
receive_ = (intent)=>
o = intent.extras
if @initialized[o.uid.creator]? # initialize first
@receive o
@iwcHandler["Yatta_new_operation"] = [receive_]
if received_HB?
@engine.applyOpsCheckDouble received_HB
sendHistoryBuffer = (intent)=>
state_vector = intent.extras
console.log state_vector
json =
HB : @yatta.getHistoryBuffer()._encode(state_vector)
user : @yatta.getUserId()
@sendIwcIntent "Yatta_push_HB_element", json
@iwcHandler["Yatta_get_HB_element"] = [sendHistoryBuffer]
#
# Set your own IWC handler. It will be called after Yatta consumed the
# data from the received intent.
#
setIwcHandler: (f)->
userIwcHandler = f
#
# Helper for sending iwc intents.
# @overload sendIwcIntent intent
# @param {Object} intent The intent object.
# @overload sendIwcIntent action_name, content
# @param {String} action_name The name of the action that is going to be send.
# @param {String} content The content that is atteched to the intent.
#
sendIwcIntent: (action_name, content)->
intent = null
if arguments.length >= 2
[action_name, content] = arguments
intent =
action: action_name
component: ""
data: ""
dataType: ""
flags: ["PUBLISH_GLOBAL"]
extras: content
else
intent = arguments[0]
@duiClient.sendIntent(intent)
#
# @private
# This function is called whenever an operation was executed.
# @param {Operation} o The operation that was executed.
#
send: (o)->
if o.uid.creator is @HB.getUserId() and (typeof o.uid.op_number isnt "string")
@sendIwcIntent "Yatta_new_operation", o
#
# @private
# This function is called whenever an operation was received from another peer.
# @param {Operation} o The operation that was received.
#
receive: (o)->
if o.uid.creator isnt @HB.getUserId()
@engine.applyOp o
init = ()->
# proposed_user_id = duiClient.getIwcClient()._componentName #TODO: This is stupid! why can't i use this?
proposed_user_id = Math.floor(Math.random()*1000000)
callback IwcConnector, proposed_user_id
setTimeout init, 5000
undefined
module.exports = createIwcConnector
if window?
if not window.Y?
window.Y = {}
window.Y.createIwcConnector = createIwcConnector

View File

@@ -1,151 +0,0 @@
#
# @overload createPeerJsConnector peerjs_options, callback
# @param {Object} peerjs_options Is the options object that is passed to PeerJs.
# @param {Function} callback The callback is called when the connector is initialized.
# @overload createPeerJsConnector peerjs_user_id, peerjs_options, callback
# @param {String} peerjs_user_id The user_id that is passed to PeerJs as the user_id and should be unique between all (also the unconnected) Peers.
# @param {Object} peerjs_options Is the options object that is passed to PeerJs.
# @param {Function} callback The callback is called when the connector is initialized.
#
createPeerJsConnector = ()->
peer = null
if arguments.length is 2
peer = new Peer arguments[0]
callback = arguments[1]
else
peer = new Peer arguments[0], arguments[1]
peer.on 'error', (err)->
throw new Error "Peerjs connector: #{err}"
peer.on 'disconnected', ()->
throw new Error "Peerjs connector disconnected from signalling server. Cannot accept new connections. Not fatal, but not so good either.."
callback = arguments[2]
#
# PeerJs is a Framework that enables you to connect to other peers. You just need the
# user-id of the peer (browser/client). And then you can connect to it.
# @see http://peerjs.com
#
class PeerJsConnector
#
# @param {Engine} engine The transformation engine
# @param {HistoryBuffer} HB
# @param {Array<Function>} execution_listener You must ensure that whenever an operation is executed, every function in this Array is called.
# @param {Yatta} yatta The Yatta framework.
#
constructor: (@engine, @HB, @execution_listener, @yatta)->
@peer = peer
@connections = {}
@new_connection_listeners = []
@peer.on 'connection', (conn)=>
@addConnection conn
sync_every_collaborator = ()=>
for conn_id, conn of @connections
conn.send
sync_state_vector: @HB.getOperationCounter()
setInterval sync_every_collaborator, 4000
send_ = (o)=>
if o.uid.creator is @HB.getUserId() and (typeof o.uid.op_number isnt "string")
for conn_id,conn of @connections
conn.send
op: o
@execution_listener.push send_
#
# Connect the Framework to another peer. Therefore you have to receive his
# user_id. If the other peer is connected to other peers, the PeerJsConnector
# will automatically connect to them too.
#
# Transmitting the user_id is your job.
# See [TextEditing](../../examples/TextEditing/) for a nice example
# on how to do that with urls.
#
# @param id {String} Connection id
#
connectToPeer: (id)->
if not @connections[id]? and id isnt @yatta.getUserId()
@addConnection peer.connect id
#
# Receive the id of every connected peer.
# @return {Array<String>} A list of Peer-Ids
#
getAllConnectionIds: ()->
for conn_id of @connections
conn_id
onNewConnection: (f)->
@new_connection_listeners.push f
#
# Adds an existing connection to this connector.
# @param conn {PeerJsConnection}
#
addConnection: (conn)->
#
# What this method does:
# * Send state vector
# * Receive HB -> apply them
# * Send connections
# * Receive Connections -> Connect to unknow connections
@connections[conn.peer] = conn
initialized_me = false
initialized_him = false
conn.on 'data', (data)=>
if data is "empty_message"
# nop
else if data.HB?
initialized_me = true
@engine.applyOpsCheckDouble data.HB
if not data.initialized
conn.send
conns: @getAllConnectionIds()
@new_connection_listeners.map (f)->
f(conn)
else if data.op?
@engine.applyOp data.op
else if data.conns?
for conn_id in data.conns
@connectToPeer conn_id
else if data.sync_state_vector?
conn.send
HB: @yatta.getHistoryBuffer()._encode(data.sync_state_vector)
initialized: true
else if data.state_vector?
if not initialized_him
# make sure, that it is sent only once
conn.send
HB: @yatta.getHistoryBuffer()._encode(data.state_vector)
initialized: false
initialized_him = true
else
throw new Error "Can't parse this operation: #{data}"
sendStateVector = ()=>
conn.send
state_vector: @HB.getOperationCounter()
if not initialized_me
# Because of a bug in PeerJs,
# we never know if state vector was actually sent
setTimeout sendStateVector, 100
sendStateVector()
peer.on 'open', (id)->
callback PeerJsConnector, id
module.exports = createPeerJsConnector
if window?
if not window.Y?
window.Y = {}
window.Y.createPeerJsConnector = createPeerJsConnector

View File

@@ -1,78 +0,0 @@
_ = require "underscore"
module.exports = (user_list)->
#
# @nodoc
# A trivial Connector that simulates network delay.
#
class TestConnector
#
# @param {Engine} engine The transformation engine
# @param {HistoryBuffer} HB
# @param {Array<Function>} execution_listener You must ensure that whenever an operation is executed, every function in this Array is called.
# @param {Yatta} yatta The Yatta framework.
#
constructor: (@engine, @HB, @execution_listener)->
send_ = (o)=>
@send o
@execution_listener.push send_
@applied_operations = []
appliedOperationsListener = (o)=>
@applied_operations.push o
@execution_listener.push appliedOperationsListener
if not (user_list?.length is 0)
@engine.applyOps user_list[0].getHistoryBuffer()._encode()
@HB.setManualGarbageCollect()
@unexecuted = {}
#
# This engine applied operations in a specific order.
# Get the ops in the right order.
#
getOpsInExecutionOrder: ()->
@applied_operations
#
# This function is called whenever an operation was executed.
# @param {Operation} o The operation that was executed.
#
send: (o)->
if (o.uid.creator is @HB.getUserId()) and (typeof o.uid.op_number isnt "string")
for user in user_list
if user.getUserId() isnt @HB.getUserId()
user.getConnector().receive(o)
#
# This function is called whenever an operation was received from another peer.
# @param {Operation} o The operation that was received.
#
receive: (o)->
@unexecuted[o.uid.creator] ?= []
@unexecuted[o.uid.creator].push o
#
# Flush one operation from the line of a specific user.
#
flushOne: (user)->
if @unexecuted[user]?.length > 0
@engine.applyOp @unexecuted[user].shift()
#
# Flush one operation on a random line.
#
flushOneRandom: ()->
@flushOne (_.random 0, (user_list.length-1))
#
# Flush all operations on every line.
#
flushAll: ()->
for n,ops of @unexecuted
@engine.applyOps ops
@unexecuted = {}

View File

@@ -1,104 +1,104 @@
#
# @nodoc
# The Engine handles how and in which order to execute operations and add operations to the HistoryBuffer.
#
class Engine
#
# @param {HistoryBuffer} HB
# @param {Array} parser Defines how to parse encoded messages.
#
constructor: (@HB, @parser)->
@unprocessed_ops = []
#
# Parses an operatio from the json format. It uses the specified parser in your OperationType module.
#
parseOperation: (json)->
typeParser = @parser[json.type]
if typeParser?
typeParser json
else
throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}."
#
# Apply a set of operations. E.g. the operations you received from another users HB._encode().
# @note You must not use this method when you already have ops in your HB!
#
applyOpsBundle: (ops_json)->
ops = []
for o in ops_json
ops.push @parseOperation o
for o in ops
@HB.addOperation o
for o in ops
if not o.execute()
@unprocessed_ops.push o
@tryUnprocessed()
#
# Same as applyOps but operations that are already in the HB are not applied.
# @see Engine.applyOps
#
applyOpsCheckDouble: (ops_json)->
for o in ops_json
if not @HB.getOperation(o.uid)?
@applyOp o
#
# Apply a set of operations. (Helper for using applyOp on Arrays)
# @see Engine.applyOp
applyOps: (ops_json)->
for o in ops_json
@applyOp o
#
# Apply an operation that you received from another peer.
#
applyOp: (op_json)->
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
o = @parseOperation op_json
@HB.addToCounter o
# @HB.addOperation o
if @HB.getOperation(o)?
else if not o.execute()
@unprocessed_ops.push o
else
@HB.addOperation o
@tryUnprocessed()
#
# Call this method when you applied a new operation.
# It checks if operations that were previously not executable are now executable.
#
tryUnprocessed: ()->
while true
old_length = @unprocessed_ops.length
unprocessed = []
for op in @unprocessed_ops
if @HB.getOperation(op)?
else if not op.execute()
unprocessed.push op
else
@HB.addOperation op
@unprocessed_ops = unprocessed
if @unprocessed_ops.length is old_length
break
module.exports = Engine
#
# @nodoc
# The Engine handles how and in which order to execute operations and add operations to the HistoryBuffer.
#
class Engine
#
# @param {HistoryBuffer} HB
# @param {Array} parser Defines how to parse encoded messages.
#
constructor: (@HB, @parser)->
@unprocessed_ops = []
#
# Parses an operatio from the json format. It uses the specified parser in your OperationType module.
#
parseOperation: (json)->
typeParser = @parser[json.type]
if typeParser?
typeParser json
else
throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}."
#
# Apply a set of operations. E.g. the operations you received from another users HB._encode().
# @note You must not use this method when you already have ops in your HB!
#
applyOpsBundle: (ops_json)->
ops = []
for o in ops_json
ops.push @parseOperation o
for o in ops
@HB.addOperation o
for o in ops
if not o.execute()
@unprocessed_ops.push o
@tryUnprocessed()
#
# Same as applyOps but operations that are already in the HB are not applied.
# @see Engine.applyOps
#
applyOpsCheckDouble: (ops_json)->
for o in ops_json
if not @HB.getOperation(o.uid)?
@applyOp o
#
# Apply a set of operations. (Helper for using applyOp on Arrays)
# @see Engine.applyOp
applyOps: (ops_json)->
for o in ops_json
@applyOp o
#
# Apply an operation that you received from another peer.
#
applyOp: (op_json)->
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
o = @parseOperation op_json
@HB.addToCounter o
# @HB.addOperation o
if @HB.getOperation(o)?
else if not o.execute()
@unprocessed_ops.push o
else
@HB.addOperation o
@tryUnprocessed()
#
# Call this method when you applied a new operation.
# It checks if operations that were previously not executable are now executable.
#
tryUnprocessed: ()->
while true
old_length = @unprocessed_ops.length
unprocessed = []
for op in @unprocessed_ops
if @HB.getOperation(op)?
else if not op.execute()
unprocessed.push op
else
@HB.addOperation op
@unprocessed_ops = unprocessed
if @unprocessed_ops.length is old_length
break
module.exports = Engine

View File

@@ -2,6 +2,8 @@
json_types_uninitialized = require "../Types/JsonTypes"
HistoryBuffer = require "../HistoryBuffer"
Engine = require "../Engine"
adaptConnector = require "../ConnectorAdapter"
#
# Framework for Json data-structures.
@@ -16,13 +18,13 @@ class JsonFramework
# @param {String} user_id Unique id of the peer.
# @param {Connector} Connector the connector class.
#
constructor: (user_id, Connector)->
constructor: (user_id, @connector)->
@HB = new HistoryBuffer user_id
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, @
adaptConnector @connector, @engine, @HB, type_manager.execution_listener
first_word = new @types.JsonType(@HB.getReservedUniqueIdentifier())
@HB.addOperation(first_word).execute()

View File

@@ -2,6 +2,7 @@
text_types_uninitialized = require "../Types/TextTypes"
HistoryBuffer = require "../HistoryBuffer"
Engine = require "../Engine"
adaptConnector = require "../ConnectorAdapter"
#
# Framework for Text Datastructures.
@@ -12,12 +13,12 @@ class TextFramework
# @param {String} user_id Uniqe user id that defines this peer.
# @param {Connector} Connector The connector defines how you connect to the other peers.
#
constructor: (user_id, Connector)->
constructor: (user_id, @connector)->
@HB = new HistoryBuffer user_id
text_types = text_types_uninitialized @HB
@types = text_types.types
@engine = new Engine @HB, text_types.parser
@connector = new Connector @engine, @HB, text_types.execution_listener, @
adaptConnector @connector, @engine, @HB, text_types.execution_listener
beginning = @HB.addOperation new @types.Delimiter {creator: '_', op_number: '_beginning'} , undefined, undefined
end = @HB.addOperation new @types.Delimiter {creator: '_', op_number: '_end'} , beginning, undefined

View File

@@ -2,6 +2,7 @@
json_types_uninitialized = require "../Types/XmlTypes"
HistoryBuffer = require "../HistoryBuffer"
Engine = require "../Engine"
adaptConnector = require "../ConnectorAdapter"
#
# Framework for Xml-like data-structures.
@@ -13,13 +14,13 @@ class XmlFramework
# @param {String} user_id Unique id of the peer.
# @param {Connector} Connector the connector class.
#
constructor: (user_id, Connector)->
constructor: (user_id, @connector)->
@HB = new HistoryBuffer user_id
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, @
adaptConnector @connector, @engine, @HB, type_manager.execution_listener
#first_word = new @types.XmlType(undefined, undefined, undefined, undefined, document.createElement("shared"))
#@HB.addOperation(first_word).execute()

View File

@@ -1,9 +1,4 @@
exports['IwcConnector'] =
require './Connectors/IwcConnector'
exports['TestConnector'] =
require './Connectors/TestConnector'
exports['JsonFramework'] =
require './Frameworks/JsonFramework'
exports['TextFramework'] =