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