module.exports = # # @params new Connector(syncMode, role) # @param syncMode {String} is either "syncAll" or "master-slave". # @param role {String} The role of this client # (slave or master (only used when syncMode is master-slave)) # 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!" req "syncMode", ["syncAll", "master-slave"] req "role", ["master", "slave"] req "user_id" @on_user_id_set(@user_id) # A Master should sync with everyone! TODO: really? - for now its safer this way! if @role is "master" @syncMode = "syncAll" # 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 # @receive_handlers = [] # this is already set in the ConnectorAdapter! # whether this instance is bound to any y instance @is_bound_to_y = false @connections = {} @current_sync_target = null @sent_hb_to_all_users = false isRoleMaster: -> @role is "master" isRoleSlave: -> @role is "slave" findNewSyncTarget: ()-> @current_sync_target = null if @syncMode is "syncAll" for user, c of @connections if not c.is_synced @performSync user break if not @current_sync_target? @setStateSynced() null userLeft: (user)-> delete @connections[user] @findNewSyncTarget() userJoined: (user, role)-> if not role? throw new Error "Internal: You must specify the role of the joined user! E.g. userJoined('uid:3939','slave')" # a user joined the room @connections[user] = is_synced : false if (not @is_synced) or @syncMode is "syncAll" if @syncMode is "syncAll" @performSync user else if role is "master" # TODO: What if there are two masters? Prevent sending everything two times! @performSyncWithMaster user # # 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" 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 if _hb.length > 30 @broadcast sync_step: "applyHB_" data: _hb _hb = [] @broadcast sync_step: "applyHB" data: _hb # # 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)-> @current_sync_target = user @send user, sync_step: "getHB" send_again: "true" data: [] hb = @getHB([]).hb _hb = [] for o in hb _hb.push o if _hb.length > 30 @broadcast sync_step: "applyHB_" data: _hb _hb = [] @broadcast sync_step: "applyHB" data: _hb # # You are sure that all clients are synced, call this function. # setStateSynced: ()-> if not @is_synced @is_synced = true for f in @compute_when_synced f() delete @compute_when_synced 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 if sender is @user_id return if res.sync_step is "getHB" data = @getHB(res.data) hb = data.hb _hb = [] # 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 if @is_synced sendApplyHB = (m)=> @send sender, m else sendApplyHB = (m)=> @broadcast m for o in hb _hb.push o if _hb.length > 30 sendApplyHB sync_step: "applyHB_" data: _hb _hb = [] sendApplyHB sync_step : "applyHB" data: _hb if res.send_again? 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" @applyHB(res.data, sender is @current_sync_target) if (@syncMode is "syncAll" or res.sent_again?) and (not @is_synced) and (@current_sync_target is sender) @connections[sender].is_synced = true @findNewSyncTarget() else if res.sync_step is "applyHB_" @applyHB(res.data, sender is @current_sync_target) # 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 # # # # 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