added test connector, webrtc connector, ideas to apply operations with very low overhead

This commit is contained in:
Kevin Jahns
2015-06-25 18:41:00 +02:00
parent 3142b0f161
commit fec03dc6e1
9 changed files with 419 additions and 464 deletions

223
src/Connector.js Normal file
View 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!");
}
}
}