2015-02-03 18:55:02 +00:00
module.exports =
#
2015-02-05 14:15:20 +00:00
# @params new Connector(options)
2015-02-05 16:19:55 +00:00
# @param options.syncMethod {String} is either "syncAll" or "master-slave".
2015-02-05 14:15:20 +00:00
# @param options.role {String} The role of this client
2015-02-05 16:19:55 +00:00
# (slave or master (only used when syncMethod is master-slave))
2015-02-05 14:15:20 +00:00
# @param options.perform_send_again {Boolean} Whetehr to whether to resend the HB after some time period. This reduces sync errors, but has some overhead (optional)
2015-02-03 18:55:02 +00:00
#
init: (options)->
req = (name, choices)=>
if options [ name ] ?
if ( not choices ? ) or choices . some ( (c)-> c is options [ name ] )
@ [ name ] = options [ name ]
else
throw new Error " You can set the ' " + name + " ' option to one of the following choices: " + JSON . encode ( choices )
else
throw new Error " You must specify " + name + " , when initializing the Connector! "
2015-02-05 16:19:55 +00:00
req " syncMethod " , [ " syncAll " , " master-slave " ]
2015-02-03 18:55:02 +00:00
req " role " , [ " master " , " slave " ]
req " user_id "
2015-02-05 14:15:20 +00:00
@ on_user_id_set ? ( @ user_id )
# whether to resend the HB after some time period. This reduces sync errors.
# But this is not necessary in the test-connector
if options . perform_send_again ?
@perform_send_again = options . perform_send_again
else
@perform_send_again = true
2015-02-03 18:55:02 +00:00
# A Master should sync with everyone! TODO: really? - for now its safer this way!
if @ role is " master "
2015-02-05 16:19:55 +00:00
@syncMethod = " syncAll "
2015-02-03 18:55:02 +00:00
# is set to true when this is synced with all other connections
@is_synced = false
# Peerjs Connections: key: conn-id, value: object
@connections = { }
# List of functions that shall process incoming data
2015-02-05 16:19:55 +00:00
@ receive_handlers ? = [ ]
2015-02-03 18:55:02 +00:00
# whether this instance is bound to any y instance
@connections = { }
@current_sync_target = null
2015-02-05 10:46:40 +00:00
@sent_hb_to_all_users = false
2015-02-15 15:33:35 +00:00
@is_initialized = true
2015-04-28 11:31:43 +02:00
onUserEvent: (f)->
2015-04-30 09:48:39 +02:00
@ connections_listeners ? = [ ]
2015-04-28 11:31:43 +02:00
@ connections_listeners . push f
2015-02-03 18:55:02 +00:00
isRoleMaster: ->
@ role is " master "
isRoleSlave: ->
@ role is " slave "
findNewSyncTarget: ()->
@current_sync_target = null
2015-02-05 16:19:55 +00:00
if @ syncMethod is " syncAll "
2015-02-03 18:55:02 +00:00
for user , c of @ connections
if not c . is_synced
@ performSync user
break
2015-02-05 10:46:40 +00:00
if not @ current_sync_target ?
@ setStateSynced ( )
2015-02-03 18:55:02 +00:00
null
userLeft: (user)->
delete @ connections [ user ]
@ findNewSyncTarget ( )
2015-04-28 11:31:43 +02:00
for f in @ connections_listeners
f {
action: " userLeft "
user: user
}
2015-02-03 18:55:02 +00:00
userJoined: (user, role)->
if not role ?
2015-02-05 10:46:40 +00:00
throw new Error " Internal: You must specify the role of the joined user! E.g. userJoined( ' uid:3939 ' , ' slave ' ) "
2015-02-03 18:55:02 +00:00
# a user joined the room
2015-02-05 14:15:20 +00:00
@ connections [ user ] ? = { }
@ connections [ user ] . is_synced = false
2015-02-03 18:55:02 +00:00
2015-02-05 16:19:55 +00:00
if ( not @ is_synced ) or @ syncMethod is " syncAll "
if @ syncMethod is " syncAll "
2015-02-03 18:55:02 +00:00
@ performSync user
else if role is " master "
# TODO: What if there are two masters? Prevent sending everything two times!
@ performSyncWithMaster user
2015-04-28 11:31:43 +02:00
for f in @ connections_listeners
f {
action: " userJoined "
user: user
role: role
}
2015-02-03 18:55:02 +00:00
#
# Execute a function _when_ we are connected. If not connected, wait until connected.
# @param f {Function} Will be executed on the PeerJs-Connector context.
#
whenSynced: (args)->
if args . constructore is Function
args = [ args ]
if @ is_synced
args [ 0 ] . apply this , args [ 1 . . ]
else
@ compute_when_synced ? = [ ]
@ compute_when_synced . push args
#
# Execute an function when a message is received.
# @param f {Function} Will be executed on the PeerJs-Connector context. f will be called with (sender_id, broadcast {true|false}, message).
#
onReceive: (f)->
@ receive_handlers . push f
# ##
# Broadcast a message to all connected peers.
# @param message {Object} The message to broadcast.
#
broadcast: (message)->
throw new Error " You must implement broadcast! "
#
# Send a message to a peer, or set of peers
#
send: (peer_s, message)->
throw new Error " You must implement send! "
# ##
#
# perform a sync with a specific user.
#
performSync: (user)->
if not @ current_sync_target ?
@current_sync_target = user
@ send user ,
sync_step: " getHB "
2015-02-05 10:46:40 +00:00
send_again: " true "
data: [ ] # @getStateVector()
if not @ sent_hb_to_all_users
@sent_hb_to_all_users = true
hb = @ getHB ( [ ] ) . hb
_hb = [ ]
for o in hb
_hb . push o
2015-03-09 17:38:26 +00:00
if _hb . length > 10
2015-02-05 10:46:40 +00:00
@ broadcast
sync_step: " applyHB_ "
data: _hb
_hb = [ ]
@ broadcast
sync_step: " applyHB "
data: _hb
2015-02-03 18:55:02 +00:00
#
# When a master node joined the room, perform this sync with him. It will ask the master for the HB,
# and will broadcast his own HB
#
performSyncWithMaster: (user)->
2015-02-05 10:46:40 +00:00
@current_sync_target = user
@ send user ,
sync_step: " getHB "
send_again: " true "
data: [ ]
hb = @ getHB ( [ ] ) . hb
_hb = [ ]
for o in hb
_hb . push o
2015-03-09 17:38:26 +00:00
if _hb . length > 10
2015-02-05 10:46:40 +00:00
@ broadcast
sync_step: " applyHB_ "
data: _hb
_hb = [ ]
@ broadcast
sync_step: " applyHB "
data: _hb
2015-02-03 18:55:02 +00:00
#
# You are sure that all clients are synced, call this function.
#
setStateSynced: ()->
if not @ is_synced
@is_synced = true
2015-02-05 14:15:20 +00:00
if @ compute_when_synced ?
for f in @ compute_when_synced
f ( )
delete @ compute_when_synced
2015-02-03 18:55:02 +00:00
null
#
# You received a raw message, and you know that it is intended for to Yjs. Then call this function.
#
receiveMessage: (sender, res)->
if not res . sync_step ?
for f in @ receive_handlers
f sender , res
else
2015-02-05 10:46:40 +00:00
if sender is @ user_id
return
2015-02-03 18:55:02 +00:00
if res . sync_step is " getHB "
data = @ getHB ( res . data )
hb = data . hb
_hb = [ ]
2015-02-05 10:46:40 +00:00
# always broadcast, when not synced.
# This reduces errors, when the clients goes offline prematurely.
# When this client only syncs to one other clients, but looses connectors,
# before syncing to the other clients, the online clients have different states.
# Since we do not want to perform regular syncs, this is a good alternative
2015-02-03 18:55:02 +00:00
if @ is_synced
2015-02-05 10:46:40 +00:00
sendApplyHB = (m)=>
@ send sender , m
else
sendApplyHB = (m)=>
@ broadcast m
for o in hb
2015-02-03 18:55:02 +00:00
_hb . push o
2015-03-09 17:38:26 +00:00
if _hb . length > 10
2015-02-05 10:46:40 +00:00
sendApplyHB
2015-02-03 18:55:02 +00:00
sync_step: " applyHB_ "
data: _hb
_hb = [ ]
2015-02-05 10:46:40 +00:00
sendApplyHB
sync_step : " applyHB "
data: _hb
2015-02-05 14:15:20 +00:00
if res . send_again ? and @ perform_send_again
2015-02-03 18:55:02 +00:00
send_again = do (sv = data.state_vector)=>
()=>
hb = @ getHB ( sv ) . hb
@ send sender ,
sync_step: " applyHB " ,
data: hb
sent_again: " true "
setTimeout send_again , 3000
else if res . sync_step is " applyHB "
2015-02-05 10:46:40 +00:00
@ applyHB ( res . data , sender is @ current_sync_target )
2015-02-03 18:55:02 +00:00
2015-02-15 15:33:35 +00:00
if ( @ syncMethod is " syncAll " or res . sent_again ? ) and ( not @ is_synced ) and ( ( @ current_sync_target is sender ) or ( not @ current_sync_target ? ) )
2015-02-03 18:55:02 +00:00
@ connections [ sender ] . is_synced = true
@ findNewSyncTarget ( )
else if res . sync_step is " applyHB_ "
2015-02-05 10:46:40 +00:00
@ applyHB ( res . data , sender is @ current_sync_target )
2015-02-03 18:55:02 +00:00
# Currently, the HB encodes operations as JSON. For the moment I want to keep it
# that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
# too much overhead. Y is very likely to get changed a lot in the future
#
# Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
# we encode the JSON as XML.
#
# When the HB support encoding as XML, the format should look pretty much like this.
# does not support primitive values as array elements
# expects an ltx (less than xml) object
parseMessageFromXml: (m)->
parse_array = (node)->
for n in node . children
if n . getAttribute ( " isArray " ) is " true "
parse_array n
else
parse_object n
parse_object = (node)->
json = { }
for name , value of node . attrs
int = parseInt ( value )
if isNaN ( int ) or ( " " + int ) isnt value
json [ name ] = value
else
json [ name ] = int
for n in node . children
name = n . name
if n . getAttribute ( " isArray " ) is " true "
json [ name ] = parse_array n
else
json [ name ] = parse_object n
json
parse_object m
# encode message in xml
# we use string because Strophe only accepts an "xml-string"..
# So {a:4,b:{c:5}} will look like
# <y a="4">
# <b c="5"></b>
# </y>
# m - ltx element
# json - guess it ;)
#
encodeMessageToXml: (m, json)->
# attributes is optional
encode_object = (m, json)->
for name , value of json
if not value ?
# nop
else if value . constructor is Object
encode_object m . c ( name ) , value
else if value . constructor is Array
encode_array m . c ( name ) , value
else
m . setAttribute ( name , value )
m
encode_array = (m, array)->
m . setAttribute ( " isArray " , " true " )
for e in array
if e . constructor is Object
encode_object m . c ( " array-element " ) , e
else
encode_array m . c ( " array-element " ) , e
m
if json . constructor is Object
encode_object m . c ( " y " , { xmlns : " http://y.ninja/connector-stanza " } ) , json
else if json . constructor is Array
encode_array m . c ( " y " , { xmlns : " http://y.ninja/connector-stanza " } ) , json
else
throw new Error " I can ' t encode this json! "
setIsBoundToY: ()->
@ on_bound_to_y ? ( )
delete @ when_bound_to_y
@is_bound_to_y = true