included connector type

This commit is contained in:
DadaMonad 2015-02-03 18:55:02 +00:00
parent f835a72151
commit 58a479be9b
22 changed files with 2015 additions and 736 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,13 @@
var adaptConnector;
var ConnectorClass, adaptConnector;
ConnectorClass = require("./ConnectorClass");
adaptConnector = function(connector, engine, HB, execution_listener) {
var applyHB, encode_state_vector, getHB, getStateVector, parse_state_vector, send_;
var applyHB, encode_state_vector, f, getHB, getStateVector, name, parse_state_vector, send_;
for (name in ConnectorClass) {
f = ConnectorClass[name];
connector[name] = f;
}
send_ = function(o) {
if (o.uid.creator === HB.getUserId() && (typeof o.uid.op_number !== "string")) {
return connector.broadcast(o);
@ -12,7 +18,7 @@ adaptConnector = function(connector, engine, HB, execution_listener) {
}
execution_listener.push(send_);
encode_state_vector = function(v) {
var name, value, _results;
var value, _results;
_results = [];
for (name in v) {
value = v[name];
@ -55,6 +61,7 @@ adaptConnector = function(connector, engine, HB, execution_listener) {
connector.getStateVector = getStateVector;
connector.getHB = getHB;
connector.applyHB = applyHB;
connector.receive_handlers = [];
connector.receive_handlers.push(function(sender, op) {
if (op.uid.creator !== HB.getUserId()) {
return engine.applyOp(op);

View File

@ -0,0 +1,304 @@
module.exports = {
init: function(options) {
var req;
req = (function(_this) {
return function(name, choices) {
if (options[name] != null) {
if ((choices == null) || choices.some(function(c) {
return c === options[name];
})) {
return _this[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!");
}
};
})(this);
req("syncMode", ["syncAll", "master-slave"]);
req("role", ["master", "slave"]);
req("user_id");
this.on_user_id_set(this.user_id);
if (this.role === "master") {
this.syncMode = "syncAll";
}
this.is_synced = false;
this.is_syncing = false;
this.connections = {};
this.is_bound_to_y = false;
this.connections = {};
return this.current_sync_target = null;
},
isRoleMaster: function() {
return this.role === "master";
},
isRoleSlave: function() {
return this.role === "slave";
},
findNewSyncTarget: function() {
var c, user, _ref;
this.current_sync_target = null;
if (this.syncMode === "syncAll") {
_ref = this.connections;
for (user in _ref) {
c = _ref[user];
if (!c.is_synced) {
this.performSync(user);
break;
}
}
}
return null;
},
userLeft: function(user) {
delete this.connections[user];
return this.findNewSyncTarget();
},
userJoined: function(user, role) {
if (role == null) {
throw new Error("Internal: You must specify the role of the joined user!");
}
this.connections[user] = {
is_synced: false
};
if ((!this.is_synced) || this.syncMode === "syncAll") {
if (this.syncMode === "syncAll") {
return this.performSync(user);
} else if (role === "master") {
return this.performSyncWithMaster(user);
}
}
},
whenSynced: function(args) {
if (args.constructore === Function) {
args = [args];
}
if (this.is_synced) {
return args[0].apply(this, args.slice(1));
} else {
if (this.compute_when_synced == null) {
this.compute_when_synced = [];
}
return this.compute_when_synced.push(args);
}
},
onReceive: function(f) {
return this.receive_handlers.push(f);
},
/*
* Broadcast a message to all connected peers.
* @param message {Object} The message to broadcast.
*
broadcast: (message)->
throw new Error "You must implement broadcast!"
*
* Send a message to a peer, or set of peers
*
send: (peer_s, message)->
throw new Error "You must implement send!"
*/
performSync: function(user) {
if (this.current_sync_target == null) {
this.current_sync_target = user;
return this.send(user, {
sync_step: "getHB",
data: this.getStateVector()
});
}
},
performSyncWithMaster: function(user) {
var hb, o, _hb, _i, _len;
if (!this.is_syncing) {
this.current_sync_target = user;
this.is_syncing = true;
this.send(user, {
sync_step: "getHB",
send_again: "true",
data: []
});
hb = this.getHB([]).hb;
_hb = [];
for (_i = 0, _len = hb.length; _i < _len; _i++) {
o = hb[_i];
_hb.push(o);
if (_hb.length > 30) {
this.broadcast({
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
return this.broadcast({
sync_step: "applyHB",
data: _hb
});
}
},
setStateSynced: function() {
var f, _i, _len, _ref;
if (!this.is_synced) {
this.is_synced = true;
_ref = this.compute_when_synced;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
f = _ref[_i];
f();
}
delete this.compute_when_synced;
return null;
}
},
receiveMessage: function(sender, res) {
var data, f, hb, o, send_again, _hb, _i, _j, _len, _len1, _ref, _results;
if (res.sync_step == null) {
_ref = this.receive_handlers;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
f = _ref[_i];
_results.push(f(sender, res));
}
return _results;
} else {
if (res.sync_step === "getHB") {
data = this.getHB(res.data);
hb = data.hb;
_hb = [];
for (_j = 0, _len1 = hb.length; _j < _len1; _j++) {
o = hb[_j];
_hb.push(o);
if (_hb.length > 30) {
this.send(sender, {
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
this.send(sender, {
sync_step: "applyHB",
data: _hb
});
if (res.send_again != null) {
send_again = (function(_this) {
return function(sv) {
return function() {
hb = _this.getHB(sv).hb;
return _this.send(sender, {
sync_step: "applyHB",
data: hb,
sent_again: "true"
});
};
};
})(this)(data.state_vector);
return setTimeout(send_again, 3000);
}
} else if (res.sync_step === "applyHB") {
this.applyHB(res.data);
if ((this.syncMode === "syncAll" || (res.sent_again != null)) && !this.is_synced) {
this.setStateSynced();
this.connections[sender].is_synced = true;
return this.findNewSyncTarget();
}
} else if (res.sync_step === "applyHB_") {
return this.applyHB(res.data);
}
}
},
parseMessageFromXml: function(m) {
var parse_array, parse_object;
parse_array = function(node) {
var n, _i, _len, _ref, _results;
_ref = node.children;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
n = _ref[_i];
if (n.getAttribute("isArray") === "true") {
_results.push(parse_array(n));
} else {
_results.push(parse_object(n));
}
}
return _results;
};
parse_object = function(node) {
var int, json, n, name, value, _i, _len, _ref, _ref1;
json = {};
_ref = node.attrs;
for (name in _ref) {
value = _ref[name];
int = parseInt(value);
if (isNaN(int) || ("" + int) !== value) {
json[name] = value;
} else {
json[name] = int;
}
}
_ref1 = node.children;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
n = _ref1[_i];
name = n.name;
if (n.getAttribute("isArray") === "true") {
json[name] = parse_array(n);
} else {
json[name] = parse_object(n);
}
}
return json;
};
return parse_object(m);
},
encodeMessageToXml: function(m, json) {
var encode_array, encode_object;
encode_object = function(m, json) {
var name, value;
for (name in json) {
value = json[name];
if (value == null) {
} else if (value.constructor === Object) {
encode_object(m.c(name), value);
} else if (value.constructor === Array) {
encode_array(m.c(name), value);
} else {
m.setAttribute(name, value);
}
}
return m;
};
encode_array = function(m, array) {
var e, _i, _len;
m.setAttribute("isArray", "true");
for (_i = 0, _len = array.length; _i < _len; _i++) {
e = array[_i];
if (e.constructor === Object) {
encode_object(m.c("array-element"), e);
} else {
encode_array(m.c("array-element"), e);
}
}
return m;
};
if (json.constructor === Object) {
return encode_object(m.c("y", {
xmlns: "http://y.ninja/connector-stanza"
}), json);
} else if (json.constructor === Array) {
return encode_array(m.c("y", {
xmlns: "http://y.ninja/connector-stanza"
}), json);
} else {
throw new Error("I can't encode this json!");
}
},
setIsBoundToY: function() {
if (typeof this.on_bound_to_y === "function") {
this.on_bound_to_y();
}
delete this.when_bound_to_y;
return this.is_bound_to_y = true;
}
};

View File

@ -30,8 +30,10 @@ HistoryBuffer = (function() {
this.buffer[id] = own;
delete this.buffer[this.user_id];
}
this.operation_counter[id] = this.operation_counter[this.user_id];
delete this.operation_counter[this.user_id];
if (this.operation_counter[this.user_id] != null) {
this.operation_counter[id] = this.operation_counter[this.user_id];
delete this.operation_counter[this.user_id];
}
return this.user_id = id;
};
@ -221,7 +223,7 @@ HistoryBuffer = (function() {
_results = [];
for (user in state_vector) {
state = state_vector[user];
if ((this.operation_counter[user] == null) || (this.operation_counter[user] < state_vector[user])) {
if (((this.operation_counter[user] == null) || (this.operation_counter[user] < state_vector[user])) && (state_vector[user] != null)) {
_results.push(this.operation_counter[user] = state_vector[user]);
} else {
_results.push(void 0);

View File

@ -187,7 +187,7 @@ module.exports = function(HB) {
ReplaceManager.prototype.callEventDecorator = function(events) {
var event, name, prop, _i, _len, _ref;
if (!this.isDeleted()) {
if (!(this.isDeleted() || this.getLastOperation().isDeleted())) {
for (_i = 0, _len = events.length; _i < _len; _i++) {
event = events[_i];
_ref = this.event_properties;

View File

@ -13,14 +13,14 @@ adaptConnector = require("./ConnectorAdapter");
createY = function(connector) {
var HB, Y, type_manager, types, user_id;
user_id = null;
if (connector.id != null) {
user_id = connector.id;
if (connector.user_id != null) {
user_id = connector.user_id;
} else {
user_id = "_temp";
connector.onUserIdSet(function(id) {
connector.on_user_id_set = function(id) {
user_id = id;
return HB.resetUserId(id);
});
};
}
HB = new HistoryBuffer(user_id);
type_manager = json_types_uninitialized(HB);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,5 @@
</head>
<body>
<y-test></y-test>
<script src="./index.js"></script>
</body>
</html>

View File

@ -1,19 +0,0 @@
setTimeout(function(){
window.y_test = document.querySelector("y-test");
window.y_test.y.val("stuff",{otherstuff:{nostuff:"this is no stuff"}})
setTimeout(function(){
var res = y_test.y.val("stuff");
if(!(y_test.nostuff === "this is no stuff")){
console.log("Deep inherit doesn't work!")
}
window.y_stuff_property.val = {nostuff: "this is also no stuff"};
setTimeout(function(){
if(!(y_test.nostuff === "this is also no stuff")){
console.log("Element val overwrite doesn't work")
}
console.log("Everything is fine :)");
},500)
},500);
},3000)

View File

@ -1,11 +1,11 @@
<link rel="import" href="../../y-object.html">
<link rel="import" href="../../../y-connectors/y-xmpp/y-xmpp.html">
<link rel="import" href="../../../y-xmpp/build/browser/y-xmpp.html">
<link rel="import" href="../../../paper-slider/paper-slider.html">
<polymer-element name="y-test" attributes="y connector stuff">
<template>
<h1 id="text" contentEditable> Check this out !</h1>
<y-xmpp id="connector" connector={{connector}} room="testy-xmpp-polymer"></y-xmpp>
<y-xmpp id="connector" connector={{connector}} room="testy-xmpp-polymer" syncMode="syncAll"></y-xmpp>
<y-object connector={{connector}} val={{y}}>
<y-property name="slider" val={{slider}}>
</y-property>
@ -32,6 +32,23 @@
}
that.y.val("text").bind(that.$.text,that.shadowRoot)
})
// Everything is initialized. Lets test stuff!
window.y_test = this;
window.y_test.y.val("stuff",{otherstuff:{nostuff:"this is no stuff"}})
setTimeout(function(){
var res = y_test.y.val("stuff");
if(!(y_test.nostuff === "this is no stuff")){
console.log("Deep inherit doesn't work!")
}
window.y_stuff_property.val = {nostuff: "this is also no stuff"};
setTimeout(function(){
if(!(y_test.nostuff === "this is also no stuff")){
console.log("Element val overwrite doesn't work")
}
console.log("Everything is fine :)");
},500)
},500);
}
})
</script>

View File

@ -4,7 +4,7 @@
<meta charset=utf-8 />
<title>Y Example</title>
<script src="../../build/browser/y.js"></script>
<script src="../../../y-connectors/y-xmpp/y-xmpp.js"></script>
<script src="../../../y-xmpp/y-xmpp.js"></script>
<script src="./index.js"></script>
</head>
<body>

View File

@ -1,6 +1,6 @@
connector = new Y.XMPP("testy-xmpp-json2");
connector = new Y.XMPP().join("testy-xmpp-json3", {syncMode: "syncAll"});
connector.debug = true
y = new Y(connector);

View File

@ -1,11 +1,15 @@
ConnectorClass = require "./ConnectorClass"
#
# @param {Engine} engine The transformation engine
# @param {HistoryBuffer} HB
# @param {Array<Function>} execution_listener You must ensure that whenever an operation is executed, every function in this Array is called.
#
adaptConnector = (connector, engine, HB, execution_listener)->
for name, f of ConnectorClass
connector[name] = f
send_ = (o)->
if o.uid.creator is HB.getUserId() and (typeof o.uid.op_number isnt "string")
connector.broadcast o
@ -46,6 +50,7 @@ adaptConnector = (connector, engine, HB, execution_listener)->
connector.getHB = getHB
connector.applyHB = applyHB
connector.receive_handlers = []
connector.receive_handlers.push (sender, op)->
if op.uid.creator isnt HB.getUserId()
engine.applyOp op

279
lib/ConnectorClass.coffee Normal file
View File

@ -0,0 +1,279 @@
module.exports =
#
# @params new Connector(syncMode, role)
# @param syncMode {String} is either "syncAll" or "master-slave".
# @param role {String} The role of this client
# (slave or master (only used when syncMode is master-slave))
#
init: (options)->
req = (name, choices)=>
if options[name]?
if (not choices?) or choices.some((c)->c is options[name])
@[name] = options[name]
else
throw new Error "You can set the '"+name+"' option to one of the following choices: "+JSON.encode(choices)
else
throw new Error "You must specify "+name+", when initializing the Connector!"
req "syncMode", ["syncAll", "master-slave"]
req "role", ["master", "slave"]
req "user_id"
@on_user_id_set(@user_id)
# A Master should sync with everyone! TODO: really? - for now its safer this way!
if @role is "master"
@syncMode = "syncAll"
# is set to true when this is synced with all other connections
@is_synced = false
# true, iff the client is currently syncing
@is_syncing = false
# Peerjs Connections: key: conn-id, value: object
@connections = {}
# List of functions that shall process incoming data
# @receive_handlers = [] # this is already set in the ConnectorAdapter!
# whether this instance is bound to any y instance
@is_bound_to_y = false
@connections = {}
@current_sync_target = null
isRoleMaster: ->
@role is "master"
isRoleSlave: ->
@role is "slave"
findNewSyncTarget: ()->
@current_sync_target = null
if @syncMode is "syncAll"
for user, c of @connections
if not c.is_synced
@performSync user
break
null
userLeft: (user)->
delete @connections[user]
@findNewSyncTarget()
userJoined: (user, role)->
if not role?
throw new Error "Internal: You must specify the role of the joined user!"
# a user joined the room
@connections[user] =
is_synced : false
if (not @is_synced) or @syncMode is "syncAll"
if @syncMode is "syncAll"
@performSync user
else if role is "master"
# TODO: What if there are two masters? Prevent sending everything two times!
@performSyncWithMaster user
#
# Execute a function _when_ we are connected. If not connected, wait until connected.
# @param f {Function} Will be executed on the PeerJs-Connector context.
#
whenSynced: (args)->
if args.constructore is Function
args = [args]
if @is_synced
args[0].apply this, args[1..]
else
@compute_when_synced ?= []
@compute_when_synced.push args
#
# Execute an function when a message is received.
# @param f {Function} Will be executed on the PeerJs-Connector context. f will be called with (sender_id, broadcast {true|false}, message).
#
onReceive: (f)->
@receive_handlers.push f
###
# Broadcast a message to all connected peers.
# @param message {Object} The message to broadcast.
#
broadcast: (message)->
throw new Error "You must implement broadcast!"
#
# Send a message to a peer, or set of peers
#
send: (peer_s, message)->
throw new Error "You must implement send!"
###
#
# perform a sync with a specific user.
#
performSync: (user)->
if not @current_sync_target?
@current_sync_target = user
@send user,
sync_step: "getHB"
data: @getStateVector()
#
# 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)->
if not @is_syncing
@current_sync_target = user
@is_syncing = true
@send user,
sync_step: "getHB"
send_again: "true"
data: []
hb = @getHB([]).hb
_hb = []
for o in hb
_hb.push o
if _hb.length > 30
@broadcast
sync_step: "applyHB_"
data: _hb
_hb = []
@broadcast
sync_step: "applyHB"
data: _hb
#
# You are sure that all clients are synced, call this function.
#
setStateSynced: ()->
if not @is_synced
@is_synced = true
for f in @compute_when_synced
f()
delete @compute_when_synced
null
#
# You received a raw message, and you know that it is intended for to Yjs. Then call this function.
#
receiveMessage: (sender, res)->
if not res.sync_step?
for f in @receive_handlers
f sender, res
else
if res.sync_step is "getHB"
data = @getHB(res.data)
hb = data.hb
_hb = []
if @is_synced
sendApplyHB = ()->
for o in hb
_hb.push o
if _hb.length > 30
@send sender,
sync_step: "applyHB_"
data: _hb
_hb = []
if @is_synced
@send sender,
sync_s tep: "applyHB"
data: _hb
if res.send_again?
send_again = do (sv = data.state_vector)=>
()=>
hb = @getHB(sv).hb
@send sender,
sync_step: "applyHB",
data: hb
sent_again: "true"
setTimeout send_again, 3000
else if res.sync_step is "applyHB"
@applyHB(res.data)
if (@syncMode is "syncAll" or res.sent_again?) and not @is_synced
@setStateSynced()
@connections[sender].is_synced = true
@findNewSyncTarget()
else if res.sync_step is "applyHB_"
@applyHB(res.data)
# 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

View File

@ -31,9 +31,9 @@ class HistoryBuffer
throw new Error "You are re-assigning an old user id - this is not (yet) possible!"
@buffer[id] = own
delete @buffer[@user_id]
@operation_counter[id] = @operation_counter[@user_id]
delete @operation_counter[@user_id]
if @operation_counter[@user_id]?
@operation_counter[id] = @operation_counter[@user_id]
delete @operation_counter[@user_id]
@user_id = id
emptyGarbage: ()=>
@ -196,7 +196,7 @@ class HistoryBuffer
# you renew your own state_vector to the state_vector of the other user
renewStateVector: (state_vector)->
for user,state of state_vector
if (not @operation_counter[user]?) or (@operation_counter[user] < state_vector[user])
if ((not @operation_counter[user]?) or (@operation_counter[user] < state_vector[user])) and state_vector[user]?
@operation_counter[user] = state_vector[user]
#

View File

@ -184,7 +184,7 @@ module.exports = (HB)->
#
#
callEventDecorator: (events)->
if not @isDeleted()
if not (@isDeleted() or @getLastOperation().isDeleted())
for event in events
for name,prop of @event_properties
event[name] = prop

View File

@ -6,11 +6,11 @@ adaptConnector = require "./ConnectorAdapter"
createY = (connector)->
user_id = null
if connector.id?
user_id = connector.id # TODO: change to getUniqueId()
if connector.user_id?
user_id = connector.user_id # TODO: change to getUniqueId()
else
user_id = "_temp"
connector.onUserIdSet (id)->
connector.on_user_id_set = (id)->
user_id = id
HB.resetUserId id
HB = new HistoryBuffer user_id

View File

@ -1,8 +1,8 @@
<link rel="import" href="../polymer/polymer.html">
<polymer-element name="y-object" hidden attributes="val connector y">
</polymer-element>
<polymer-element name="y-property" hidden attributes="val name y">
</polymer-element>
<script src="./build/browser/y-object.js"></script>
<script src="./y-object.js"></script>

File diff suppressed because one or more lines are too long

3
y.js

File diff suppressed because one or more lines are too long