added test connector, webrtc connector, ideas to apply operations with very low overhead
This commit is contained in:
		
							parent
							
								
									3142b0f161
								
							
						
					
					
						commit
						fec03dc6e1
					
				| @ -4,16 +4,21 @@ | |||||||
|   }, |   }, | ||||||
|   "rules": { |   "rules": { | ||||||
|     "strict": 0, |     "strict": 0, | ||||||
|     "camelcase": [1, {"properties": "never"}] |     "camelcase": [1, {"properties": "never"}], | ||||||
|  |     "no-underscore-dangle": 0 | ||||||
|   }, |   }, | ||||||
|   "parser": "babel-eslint", |   "parser": "babel-eslint", | ||||||
|   "globals": { |   "globals": { | ||||||
|         "OperationStore": true, |         "OperationStore": true, | ||||||
|         "AbstractOperationStore": true, |         "AbstractOperationStore": true, | ||||||
|         "AbstractTransaction": true, |         "AbstractTransaction": true, | ||||||
|  |         "AbstractConnector": true, | ||||||
|         "Transaction": true, |         "Transaction": true, | ||||||
|         "IndexedDB": true, |         "IndexedDB": true, | ||||||
|         "IDBRequest": true, |         "IDBRequest": true, | ||||||
|         "GeneratorFunction": true |         "GeneratorFunction": true, | ||||||
|  |         "Y": true, | ||||||
|  |         "setTimeout": true, | ||||||
|  |         "setInterval": true | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										223
									
								
								src/Connector.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/Connector.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,223 @@ | |||||||
|  | 
 | ||||||
|  | class AbstractConnector { | ||||||
|  |   /* | ||||||
|  |     opts | ||||||
|  |      .role : String Role of this client ("master" or "slave") | ||||||
|  |      .userId : String that uniquely defines the user. | ||||||
|  |   */ | ||||||
|  |   constructor (opts) { | ||||||
|  |     if (opts == null){ | ||||||
|  |       opts = {}; | ||||||
|  |     } | ||||||
|  |     if (opts.role == null || opts.role === "master") { | ||||||
|  |       this.role = "master"; | ||||||
|  |     } else if (opts.role === "slave") { | ||||||
|  |       this.role = "slave"; | ||||||
|  |     } else { | ||||||
|  |       throw new Error("Role must be either 'master' or 'slave'!"); | ||||||
|  |     } | ||||||
|  |     this.role = opts.role; | ||||||
|  |     this.connections = {}; | ||||||
|  |     this.userEventListeners = []; | ||||||
|  |     this.whenSyncedListeners = []; | ||||||
|  |     this.currentSyncTarget = null; | ||||||
|  |   } | ||||||
|  |   setUserId (userId) { | ||||||
|  |     this.os.setUserId(userId); | ||||||
|  |   } | ||||||
|  |   onUserEvent (f) { | ||||||
|  |     this.userEventListeners.push(f); | ||||||
|  |   } | ||||||
|  |   userLeft (user : string) { | ||||||
|  |     delete this.connections[user]; | ||||||
|  |     if (user === this.currentSyncTarget){ | ||||||
|  |       this.currentSyncTarget = null; | ||||||
|  |       this.findNextSyncTarget(); | ||||||
|  |     } | ||||||
|  |     for (var f of this.userEventListeners){ | ||||||
|  |       f({ | ||||||
|  |         action: "userLeft", | ||||||
|  |         user: user | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   userJoined (user, role) { | ||||||
|  |     if(role == null){ | ||||||
|  |       throw new Error("You must specify the role of the joined user!"); | ||||||
|  |     } | ||||||
|  |     if (this.connections[user] != null) { | ||||||
|  |       throw new Error("This user already joined!"); | ||||||
|  |     } | ||||||
|  |     this.connections[user] = { | ||||||
|  |       isSynced: false, | ||||||
|  |       role: role | ||||||
|  |     }; | ||||||
|  |     for (var f of this.userEventListeners) { | ||||||
|  |       f({ | ||||||
|  |         action: "userJoined", | ||||||
|  |         user: user, | ||||||
|  |         role: role | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // Execute a function _when_ we are connected.
 | ||||||
|  |   // If not connected, wait until connected
 | ||||||
|  |   whenSynced (f) { | ||||||
|  |     if (this.isSynced === true) { | ||||||
|  |       f(); | ||||||
|  |     } else { | ||||||
|  |       this.whenSyncedListeners.push(f); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // returns false, if there is no sync target
 | ||||||
|  |   // true otherwise
 | ||||||
|  |   findNextSyncTarget () { | ||||||
|  |     if (this.currentSyncTarget != null && this.connections[this.currentSyncTarget].isSynced === false) { | ||||||
|  |       throw new Error("The current sync has not finished!") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (var uid in this.connections) { | ||||||
|  |       var u = this.connections[uid]; | ||||||
|  |       if (!u.isSynced) { | ||||||
|  |         this.currentSyncTarget = uid; | ||||||
|  |         this.send(uid, { | ||||||
|  |             type: "sync step 1", | ||||||
|  |             stateVector: hb.getStateVector() | ||||||
|  |         }); | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     // set the state to synced!
 | ||||||
|  |     if (!this.isSynced) { | ||||||
|  |       this.isSynced = true; | ||||||
|  |       for (var f of this.whenSyncedListeners) { | ||||||
|  |         f() | ||||||
|  |       } | ||||||
|  |       this.whenSyncedListeners = null; | ||||||
|  |     }    return false; | ||||||
|  |   } | ||||||
|  |   // You received a raw message, and you know that it is intended for to Yjs. Then call this function.
 | ||||||
|  |   receiveMessage (sender, m) { | ||||||
|  |     if (m.type === "sync step 1") { | ||||||
|  |       // TODO: make transaction, stream the ops
 | ||||||
|  |       var ops = yield* this.os.getOperations(m.stateVector); | ||||||
|  |       // TODO: compare against m.sv!
 | ||||||
|  |       var sv = yield* this.getStateVector(); | ||||||
|  |       this.send (sender, { | ||||||
|  |         type: "sync step 2" | ||||||
|  |         os: ops, | ||||||
|  |         stateVector: sv | ||||||
|  |       }); | ||||||
|  |       this.syncingClients.push(sender); | ||||||
|  |       setTimeout(()=>{ | ||||||
|  |         this.syncingClients = this.syncingClients.filter(function(client){ | ||||||
|  |           return client !== sender; | ||||||
|  |         }); | ||||||
|  |         this.send(sender, { | ||||||
|  |           type: "sync done" | ||||||
|  |         }) | ||||||
|  |       }, this.syncingClientDuration); | ||||||
|  |     } else if (m.type === "sync step 2") { | ||||||
|  |       var ops = this.os.getOperations(m.stateVector); | ||||||
|  |       this.broadcast { | ||||||
|  |         type: "update", | ||||||
|  |         ops: ops | ||||||
|  |       } | ||||||
|  |     } else if (m.type === "sync done") { | ||||||
|  |       this.connections[sender].isSynced = true; | ||||||
|  |       this.findNextSyncTarget(); | ||||||
|  |     } | ||||||
|  |     } else if (m.type === "update") { | ||||||
|  |       for (var client of this.syncingClients) { | ||||||
|  |         this.send(client, m); | ||||||
|  |       } | ||||||
|  |       this.os.apply(m.ops); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // 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) { | ||||||
|  |     function parseArray (node) { | ||||||
|  |       for (var n of node.children){ | ||||||
|  |         if (n.getAttribute("isArray") === "true") { | ||||||
|  |           return parseArray(n); | ||||||
|  |         } else { | ||||||
|  |           return parseObject(n); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     function parseObject (node) { | ||||||
|  |       var json = {}; | ||||||
|  |       for (name in node.attrs) { | ||||||
|  |         var value = node.attrs[name]; | ||||||
|  |         var int = parseInt(value); | ||||||
|  |         if (isNaN(int) or (""+int) !== value){ | ||||||
|  |           json[name] = value; | ||||||
|  |         } else { | ||||||
|  |           json[name] = int; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       for (n in node.children){ | ||||||
|  |         var name = n.name; | ||||||
|  |         if (n.getAttribute("isArray") === "true") { | ||||||
|  |           json[name] = parseArray(n); | ||||||
|  |         } else { | ||||||
|  |           json[name] = parseObject(n); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return json; | ||||||
|  |     } | ||||||
|  |     parseObject(node); | ||||||
|  |   } | ||||||
|  |   // 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 - Object
 | ||||||
|  |   encodeMessageToXml (m, json) { | ||||||
|  |     // attributes is optional
 | ||||||
|  |     function encodeObject (m, json) { | ||||||
|  |       for (name in json) { | ||||||
|  |         var value = json[name]; | ||||||
|  |         if (name == null) { | ||||||
|  |           // nop
 | ||||||
|  |         } else if (value.constructor === Object) { | ||||||
|  |           encodeObject(m.c(name), value); | ||||||
|  |         } else if (value.constructor === Array) { | ||||||
|  |           encodeArray(m.c(name), value); | ||||||
|  |         } else { | ||||||
|  |           m.setAttribute(name, value); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     function encodeArray (m, array) { | ||||||
|  |       m.setAttribute("isArray", "true"); | ||||||
|  |       for (var e of array) { | ||||||
|  |         if (e.constructor === Object) { | ||||||
|  |           encodeObject(m.c("array-element"), e); | ||||||
|  |         } else { | ||||||
|  |           encodeArray(m.c("array-element"), e); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (json.constructor === Object) { | ||||||
|  |       encodeObject(m.c("y", {xmlns:"http://y.ninja/connector-stanza"}), json); | ||||||
|  |     } else if (json.constructor === Array) { | ||||||
|  |       encodeArray(m.c("y", {xmlns:"http://y.ninja/connector-stanza"}), json); | ||||||
|  |     } else { | ||||||
|  |       throw new Error("I can't encode this json!"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,446 +0,0 @@ | |||||||
| 
 |  | ||||||
| 
 |  | ||||||
| (function(){ |  | ||||||
|   function WebRTC(webrtc_options){ |  | ||||||
|     if(webrtc_options === undefined){ |  | ||||||
|       throw new Error("webrtc_options must not be undefined!") |  | ||||||
|     } |  | ||||||
|     var room = webrtc_options.room; |  | ||||||
| 
 |  | ||||||
|     // connect per default to our server
 |  | ||||||
|     if(webrtc_options.url === undefined){ |  | ||||||
|       webrtc_options.url = "https://yatta.ninja:8888"; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     var swr = new SimpleWebRTC(webrtc_options); |  | ||||||
|     this.swr = swr; |  | ||||||
|     var self = this; |  | ||||||
| 
 |  | ||||||
|     var channel; |  | ||||||
| 
 |  | ||||||
|     swr.once('connectionReady',function(user_id){ |  | ||||||
|       // SimpleWebRTC (swr) is initialized
 |  | ||||||
|       swr.joinRoom(room); |  | ||||||
| 
 |  | ||||||
|       swr.once('joinedRoom', function(){ |  | ||||||
|         // the client joined the specified room
 |  | ||||||
| 
 |  | ||||||
|         // initialize the connector with the required parameters.
 |  | ||||||
|         // You always should specify `role`, `syncMethod`, and `user_id`
 |  | ||||||
|         self.init({ |  | ||||||
|           role : "slave", |  | ||||||
|           syncMethod : "syncAll", |  | ||||||
|           user_id : user_id |  | ||||||
|         }); |  | ||||||
|         var i; |  | ||||||
|         // notify the connector class about all the users that already
 |  | ||||||
|         // joined the session
 |  | ||||||
|         for(i in self.swr.webrtc.peers){ |  | ||||||
|           self.userJoined(self.swr.webrtc.peers[i].id, "slave"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         swr.on("channelMessage", function(peer, room, message){ |  | ||||||
|           // The client received a message
 |  | ||||||
|           // Check if the connector is already initialized,
 |  | ||||||
|           // only then forward the message to the connector class
 |  | ||||||
|           if(self.is_initialized && message.type === "yjs"){ |  | ||||||
|             self.receiveMessage(peer.id, message.payload); |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       swr.on("createdPeer", function(peer){ |  | ||||||
|         // a new peer/client joined the session.
 |  | ||||||
|         // Notify the connector class, if the connector
 |  | ||||||
|         // is already initialized
 |  | ||||||
|         if(self.is_initialized){ |  | ||||||
|           // note: Since the WebRTC Connector only supports the SyncAll
 |  | ||||||
|           // syncmethod, every client is a slave.
 |  | ||||||
|           self.userJoined(peer.id, "slave"); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       swr.on("peerStreamRemoved",function(peer){ |  | ||||||
|         // a client left the session.
 |  | ||||||
|         // Notify the connector class, if the connector
 |  | ||||||
|         // is already initialized
 |  | ||||||
|         if(self.is_initialized){ |  | ||||||
|           self.userLeft(peer.id); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Specify how to send a message to a specific user (by uid)
 |  | ||||||
|   WebRTC.prototype.send = function(uid, message){ |  | ||||||
|     var self = this; |  | ||||||
|     // we have to make sure that the message is sent under all circumstances
 |  | ||||||
|     var send = function(){ |  | ||||||
|       // check if the clients still exists
 |  | ||||||
|       var peer = self.swr.webrtc.getPeers(uid)[0]; |  | ||||||
|       var success; |  | ||||||
|       if(peer){ |  | ||||||
|         // success is true, if the message is successfully sent
 |  | ||||||
|         success = peer.sendDirectly("simplewebrtc", "yjs", message); |  | ||||||
|       } |  | ||||||
|       if(!success){ |  | ||||||
|         // resend the message if it didn't work
 |  | ||||||
|         window.setTimeout(send,500); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|     // try to send the message
 |  | ||||||
|     send(); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   // specify how to broadcast a message to all users
 |  | ||||||
|   // (it may send the message back to itself).
 |  | ||||||
|   // The webrtc connecor tries to send it to every single clients directly
 |  | ||||||
|   WebRTC.prototype.broadcast = function(message){ |  | ||||||
|     this.swr.sendDirectlyToAll("simplewebrtc","yjs",message); |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   Y.Connectors.WebRTC = WebRTC; |  | ||||||
| })() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| var connectorAdapter = (){ |  | ||||||
|   # |  | ||||||
|   # @params new Connector(options) |  | ||||||
|   #   @param options.syncMethod {String}  is either "syncAll" or "master-slave". |  | ||||||
|   #   @param options.role {String} The role of this client |  | ||||||
|   #            (slave or master (only used when syncMethod is master-slave)) |  | ||||||
|   #   @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) |  | ||||||
|   # |  | ||||||
|   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 "syncMethod", ["syncAll", "master-slave"] |  | ||||||
|     req "role", ["master", "slave"] |  | ||||||
|     req "user_id" |  | ||||||
|     @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 |  | ||||||
| 
 |  | ||||||
|     # A Master should sync with everyone! TODO: really? - for now its safer this way! |  | ||||||
|     if @role is "master" |  | ||||||
|       @syncMethod = "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 ?= [] |  | ||||||
| 
 |  | ||||||
|     # whether this instance is bound to any y instance |  | ||||||
|     @connections = {} |  | ||||||
|     @current_sync_target = null |  | ||||||
|     @sent_hb_to_all_users = false |  | ||||||
|     @is_initialized = true |  | ||||||
| 
 |  | ||||||
|   onUserEvent: (f)-> |  | ||||||
|     @connections_listeners ?= [] |  | ||||||
|     @connections_listeners.push f |  | ||||||
| 
 |  | ||||||
|   isRoleMaster: -> |  | ||||||
|     @role is "master" |  | ||||||
| 
 |  | ||||||
|   isRoleSlave: -> |  | ||||||
|     @role is "slave" |  | ||||||
| 
 |  | ||||||
|   findNewSyncTarget: ()-> |  | ||||||
|     @current_sync_target = null |  | ||||||
|     if @syncMethod 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() |  | ||||||
|     if @connections_listeners? |  | ||||||
|       for f in @connections_listeners |  | ||||||
|         f { |  | ||||||
|           action: "userLeft" |  | ||||||
|           user: user |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   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] ?= {} |  | ||||||
|     @connections[user].is_synced = false |  | ||||||
| 
 |  | ||||||
|     if (not @is_synced) or @syncMethod is "syncAll" |  | ||||||
|       if @syncMethod is "syncAll" |  | ||||||
|         @performSync user |  | ||||||
|       else if role is "master" |  | ||||||
|         # TODO: What if there are two masters? Prevent sending everything two times! |  | ||||||
|         @performSyncWithMaster user |  | ||||||
| 
 |  | ||||||
|     if @connections_listeners? |  | ||||||
|       for f in @connections_listeners |  | ||||||
|         f { |  | ||||||
|           action: "userJoined" |  | ||||||
|           user: user |  | ||||||
|           role: role |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|   # |  | ||||||
|   # Execute a function _when_ we are connected. If not connected, wait until connected. |  | ||||||
|   # @param f {Function} Will be executed on the Connector context. |  | ||||||
|   # |  | ||||||
|   whenSynced: (args)-> |  | ||||||
|     if args.constructor 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 |  | ||||||
| 
 |  | ||||||
|   # |  | ||||||
|   # 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 > 10 |  | ||||||
|             @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: @getStateVector() |  | ||||||
|     hb = @getHB([]).hb |  | ||||||
|     _hb = [] |  | ||||||
|     for o in hb |  | ||||||
|       _hb.push o |  | ||||||
|       if _hb.length > 10 |  | ||||||
|         @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 |  | ||||||
|       if @compute_when_synced? |  | ||||||
|         for el in @compute_when_synced |  | ||||||
|           f = el[0] |  | ||||||
|           args = el[1..] |  | ||||||
|           f.apply(args) |  | ||||||
|         delete @compute_when_synced |  | ||||||
|       null |  | ||||||
| 
 |  | ||||||
|   # executed when the a state_vector is received. listener will be called only once! |  | ||||||
|   whenReceivedStateVector: (f)-> |  | ||||||
|     @when_received_state_vector_listeners ?= [] |  | ||||||
|     @when_received_state_vector_listeners.push f |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   # |  | ||||||
|   # 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" |  | ||||||
|         # call listeners |  | ||||||
|         if @when_received_state_vector_listeners? |  | ||||||
|           for f in @when_received_state_vector_listeners |  | ||||||
|             f.call this, res.data |  | ||||||
|         delete @when_received_state_vector_listeners |  | ||||||
| 
 |  | ||||||
|         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 > 10 |  | ||||||
|             sendApplyHB |  | ||||||
|               sync_step: "applyHB_" |  | ||||||
|               data: _hb |  | ||||||
|             _hb = [] |  | ||||||
| 
 |  | ||||||
|         sendApplyHB |  | ||||||
|           sync_step : "applyHB" |  | ||||||
|           data: _hb |  | ||||||
| 
 |  | ||||||
|         if res.send_again? and @perform_send_again |  | ||||||
|           send_again = do (sv = data.state_vector)=> |  | ||||||
|             ()=> |  | ||||||
|               hb = @getHB(sv).hb |  | ||||||
|               for o in hb |  | ||||||
|                 _hb.push o |  | ||||||
|                 if _hb.length > 10 |  | ||||||
|                   @send sender, |  | ||||||
|                     sync_step: "applyHB_" |  | ||||||
|                     data: _hb |  | ||||||
|                   _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 (@syncMethod is "syncAll" or res.sent_again?) and (not @is_synced) and ((@current_sync_target is sender) or (not @current_sync_target?)) |  | ||||||
|           @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 |  | ||||||
|   # <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 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
							
								
								
									
										75
									
								
								src/Connectors/Test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/Connectors/Test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | |||||||
|  | // returns a rendom element of o
 | ||||||
|  | // works on Object, and Array
 | ||||||
|  | function getRandom (o) { | ||||||
|  |   if (o instanceof Array) { | ||||||
|  |     return o[Math.floor(Math.random() * o.length)]; | ||||||
|  |   } else if (o.constructor === Object) { | ||||||
|  |     var keys = []; | ||||||
|  |     for (var key in o) { | ||||||
|  |       keys.push(key); | ||||||
|  |     } | ||||||
|  |     return o[getRandom(keys)]; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var globalRoom = { | ||||||
|  |   users: {}, | ||||||
|  |   buffers: {}, | ||||||
|  |   removeUser: function(user){ | ||||||
|  |     for (var u of this.users) { | ||||||
|  |       u.userLeft(user); | ||||||
|  |     } | ||||||
|  |     delete this.users[user]; | ||||||
|  |     delete this.buffers[user]; | ||||||
|  |   }, | ||||||
|  |   addUser: function(connector){ | ||||||
|  |     for (var u of this.users) { | ||||||
|  |       u.userJoined(connector.userId); | ||||||
|  |     } | ||||||
|  |     this.users[connector.userId] = connector; | ||||||
|  |     this.buffers[connector.userId] = []; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | setInterval(function(){ | ||||||
|  |   var bufs = []; | ||||||
|  |   for (var i in globalRoom.buffers) { | ||||||
|  |     if (globalRoom.buffers[i].length > 0) { | ||||||
|  |       bufs.push(i); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (bufs.length > 0) { | ||||||
|  |     var userId = getRandom(bufs); | ||||||
|  |     var m = globalRoom.buffers[userId]; | ||||||
|  |     var user = globalRoom.users[userId]; | ||||||
|  |     user.receiveMessage(m); | ||||||
|  |   } | ||||||
|  | }, 10); | ||||||
|  | 
 | ||||||
|  | var userIdCounter = 0; | ||||||
|  | 
 | ||||||
|  | class Test extends AbstractConnector { | ||||||
|  |   constructor (options) { | ||||||
|  |     if(options === undefined){ | ||||||
|  |       throw new Error("Options must not be undefined!"); | ||||||
|  |     } | ||||||
|  |     super({ | ||||||
|  |       role: "master" | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.setUserId((userIdCounter++) + ""); | ||||||
|  |   } | ||||||
|  |   send (uid, message) { | ||||||
|  |     globalRoom.buffers[uid].push(message); | ||||||
|  |   } | ||||||
|  |   broadcast (message) { | ||||||
|  |     for (var buf of globalRoom.buffers) { | ||||||
|  |       buf.push(message); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   disconnect () { | ||||||
|  |     globalRoom.removeUser(this.userId); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Y.Test = Test; | ||||||
							
								
								
									
										83
									
								
								src/Connectors/WebRTC.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/Connectors/WebRTC.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | |||||||
|  | 
 | ||||||
|  | class WebRTC extends AbstractConnector { | ||||||
|  |   constructor (options) { | ||||||
|  |     if(options === undefined){ | ||||||
|  |       throw new Error("Options must not be undefined!"); | ||||||
|  |     } | ||||||
|  |     super({ | ||||||
|  |       role: "slave" | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     var room = options.room; | ||||||
|  | 
 | ||||||
|  |     // connect per default to our server
 | ||||||
|  |     if(options.url == null){ | ||||||
|  |       options.url = "https://yatta.ninja:8888"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var swr = new SimpleWebRTC(options); //eslint-disable-line no-undef
 | ||||||
|  |     this.swr = swr; | ||||||
|  |     var self = this; | ||||||
|  | 
 | ||||||
|  |     swr.once("connectionReady", function(userId){ | ||||||
|  |       // SimpleWebRTC (swr) is initialized
 | ||||||
|  |       swr.joinRoom(room); | ||||||
|  | 
 | ||||||
|  |       swr.once("joinedRoom", function(){ | ||||||
|  |         self.setUserId(userId); | ||||||
|  |         var i; | ||||||
|  |         // notify the connector class about all the users that already
 | ||||||
|  |         // joined the session
 | ||||||
|  |         for(i in self.swr.webrtc.peers){ | ||||||
|  |           self.userJoined(self.swr.webrtc.peers[i].id, "master"); | ||||||
|  |         } | ||||||
|  |         swr.on("channelMessage", function(peer, room_, message){ | ||||||
|  |           // The client received a message
 | ||||||
|  |           // Check if the connector is already initialized,
 | ||||||
|  |           // only then forward the message to the connector class
 | ||||||
|  |           if(message.type != null ){ | ||||||
|  |             self.receiveMessage(peer.id, message.payload); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       swr.on("createdPeer", function(peer){ | ||||||
|  |         // a new peer/client joined the session.
 | ||||||
|  |         // Notify the connector class, if the connector
 | ||||||
|  |         // is already initialized
 | ||||||
|  |         self.userJoined(peer.id, "master"); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       swr.on("peerStreamRemoved", function(peer){ | ||||||
|  |         // a client left the session.
 | ||||||
|  |         // Notify the connector class, if the connector
 | ||||||
|  |         // is already initialized
 | ||||||
|  |         self.userLeft(peer.id); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   send (uid, message) { | ||||||
|  |     var self = this; | ||||||
|  |     // we have to make sure that the message is sent under all circumstances
 | ||||||
|  |     var send = function(){ | ||||||
|  |       // check if the clients still exists
 | ||||||
|  |       var peer = self.swr.webrtc.getPeers(uid)[0]; | ||||||
|  |       var success; | ||||||
|  |       if(peer){ | ||||||
|  |         // success is true, if the message is successfully sent
 | ||||||
|  |         success = peer.sendDirectly("simplewebrtc", "yjs", message); | ||||||
|  |       } | ||||||
|  |       if(!success){ | ||||||
|  |         // resend the message if it didn't work
 | ||||||
|  |         setTimeout(send, 500); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |     // try to send the message
 | ||||||
|  |     send(); | ||||||
|  |   } | ||||||
|  |   broadcast (message) { | ||||||
|  |     this.swr.sendDirectlyToAll("simplewebrtc", "yjs", message); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Y.WebRTC = WebRTC; | ||||||
| @ -1,7 +1,7 @@ | |||||||
| /* @flow */ | /* @flow */ | ||||||
| 
 | 
 | ||||||
| // Op is anything that we could get from the OperationStore.
 | // Op is anything that we could get from the OperationStore.
 | ||||||
| struct Op = Object; | type Op = Object; | ||||||
| 
 | 
 | ||||||
| var Struct = { | var Struct = { | ||||||
|   Operation: {  //eslint-disable-line no-unused-vars
 |   Operation: {  //eslint-disable-line no-unused-vars
 | ||||||
| @ -17,10 +17,11 @@ var Struct = { | |||||||
|                       content : any, |                       content : any, | ||||||
|                       left : Struct.Insert, |                       left : Struct.Insert, | ||||||
|                       right : Struct.Insert, |                       right : Struct.Insert, | ||||||
|                       parent : Struct.List) : Struct.Insert { |                       parent : Struct.List) : Insert { | ||||||
|       op.left = left ? left.id : null; |       op.left = left ? left.id : null; | ||||||
|       op.origin = op.left; |       op.origin = op.left; | ||||||
|       op.right = right ? right.id : null; |       op.right = right ? right.id : null; | ||||||
|  |       op.parent = parent.id; | ||||||
|       op.struct = "Insert"; |       op.struct = "Insert"; | ||||||
|       yield* Struct.Operation.create.call(this, op, user); |       yield* Struct.Operation.create.call(this, op, user); | ||||||
| 
 | 
 | ||||||
| @ -127,14 +128,26 @@ var Struct = { | |||||||
|     }, |     }, | ||||||
|     execute: function* (op) { |     execute: function* (op) { | ||||||
|       // nop
 |       // nop
 | ||||||
|     } |     }, | ||||||
|     ref: function* (op, pos) : Struct.Insert | undefined{ |     ref: function* (op : Op, pos : number) : Insert { | ||||||
|       var o = op.start; |       var o = op.start; | ||||||
|       while ( pos !== 0 || o == null) { |       while ( pos !== 0 || o == null) { | ||||||
|         o = (yield* this.getOperation(op.start)).right; |         o = (yield* this.getOperation(o)).right; | ||||||
|  |         pos--; | ||||||
|       } |       } | ||||||
|       return (o == null) ? null : yield* this.getOperation(o); |       return (o == null) ? null : yield* this.getOperation(o); | ||||||
|  |     }, | ||||||
|  |     map: function* (o : Op, f : Function) : Array<any> { | ||||||
|  |       o = o.start; | ||||||
|  |       var res = []; | ||||||
|  |       while ( pos !== 0 || o == null) { | ||||||
|  |         var operation = yield* this.getOperation(o); | ||||||
|  |         res.push(f(operation.content)); | ||||||
|  |         o = operation.right; | ||||||
|  |         pos--; | ||||||
|       } |       } | ||||||
|  |       return res; | ||||||
|  |     }, | ||||||
|     insert: function* (op, pos : number, contents : Array<any>) { |     insert: function* (op, pos : number, contents : Array<any>) { | ||||||
|       var o = yield* Struct.List.ref.call(this, op, pos); |       var o = yield* Struct.List.ref.call(this, op, pos); | ||||||
|       var o_end = yield* this.getOperation(o.right); |       var o_end = yield* this.getOperation(o.right); | ||||||
|  | |||||||
| @ -7,8 +7,12 @@ | |||||||
|       this._model = _model; |       this._model = _model; | ||||||
|     } |     } | ||||||
|     *val (pos) { |     *val (pos) { | ||||||
|       var o = yield* this.Struct.List.ref(pos); |       if (pos != null) { | ||||||
|  |         var o = yield* this.Struct.List.ref(this._model, pos); | ||||||
|         return o ? o.content : null; |         return o ? o.content : null; | ||||||
|  |       } else { | ||||||
|  |         return yield* this.Struct.List.map(this._model, function(c){return c; }); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     *insert (pos, contents) { |     *insert (pos, contents) { | ||||||
|       yield* this.Struct.List.insert(pos, contents); |       yield* this.Struct.List.insert(pos, contents); | ||||||
| @ -17,9 +21,7 @@ | |||||||
| 
 | 
 | ||||||
|   Y.List = function* YList(){ |   Y.List = function* YList(){ | ||||||
|     var model = yield* this.Struct.List.create(); |     var model = yield* this.Struct.List.create(); | ||||||
|     return new Y.List.Create(model); |     return new List(model); | ||||||
|   } |   }; | ||||||
| 
 |  | ||||||
|   Y.List.Create = List; |   Y.List.Create = List; | ||||||
|   Y.List = List; |  | ||||||
| })(); | })(); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user