Compare commits

...

17 Commits

Author SHA1 Message Date
nicolaescu
672696ef86 Update README.md 2015-01-19 18:19:15 +01:00
DadaMonad
7c81dceb23 meme issues 2015-01-19 13:45:53 +00:00
DadaMonad
592a0969d3 changing the connector adapter to serve a maximum of 30 operations 2015-01-19 10:19:00 +00:00
DadaMonad
00458bab58 more text bind issues 2015-01-19 06:46:08 +00:00
DadaMonad
bc1c1f7bcf even more range bugs vanished 2015-01-19 05:47:59 +00:00
DadaMonad
ed392e72ae white space bug 2015-01-19 05:26:59 +00:00
DadaMonad
fb68550e2c really fixed the shadow dom bug :) 2015-01-19 05:14:29 +00:00
DadaMonad
08fe014f9b fixing bind in shadow root 2015-01-19 04:41:40 +00:00
DadaMonad
9f9ba33428 selection problem 2015-01-19 03:49:23 +00:00
DadaMonad
0421b1ab6a doubble textfield issue 2015-01-19 03:38:19 +00:00
DadaMonad
bec7d107bd fixes in array type 2015-01-19 02:27:37 +00:00
DadaMonad
54844f4535 added support for html content editable. Now you can bind event html tags to text types 2015-01-19 01:56:22 +00:00
DadaMonad
02d0ace241 fixing Polymer elements 2015-01-16 23:17:19 +00:00
DadaMonad
bab4bcc94b added support for Lists (like Arrays, but mutable). It has the same properties as the mutable string type, formerly known as Word Type 2015-01-16 21:20:47 +00:00
DadaMonad
e54402e842 no more Immutable types :) 2015-01-16 20:02:40 +00:00
DadaMonad
f1f710b269 Introduced a new model for custom collaborative types. 2015-01-16 19:13:01 +00:00
DadaMonad
b647b2af58 There are now "Pseudo operations" that are not sent, and get be queried by a special parameter with the HB.getOperation. This will reduce the number of operations that are sent and is necessary for the Array implementation, that I plan to implement in the near future 2015-01-16 13:36:15 +00:00
30 changed files with 2926 additions and 2982 deletions

View File

@@ -1,5 +1,6 @@
# ![Yatta!](https://dadamonad.github.io/files/layout/Yatta_logo.png)
[![Build Status](http://layers.dbis.rwth-aachen.de/jenkins/job/Yatta/badge/icon)](http://layers.dbis.rwth-aachen.de/jenkins/job/Yatta/)
A Real-Time web framework that manages concurrency control for arbitrary data types.
Yatta! provides similar functionality as [ShareJs](https://github.com/share/ShareJS) and [OpenCoweb](https://github.com/opencoweb/coweb),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,12 +2,6 @@
Here you find some (hopefully) usefull examples on how to use Yatta!
## Tutorials
* [PeerJs-Json Tutorial](./PeerJs-Json/) Tutorial on how to use Yatta! with Json and PeerJs Connector.
* [IWC Tutorial](./Iwc/) Tutorial on how to use IWC Connector.
## Demos
* [Text Editing](http://dadamonad.github.io/Yatta/examples/TextEditing/) Simple collaborative text editing demo with PeerJs and Text Framework.
* [XML Example](http://dadamonad.github.io/Yatta/examples/XmlExample) Collaboratively manipulate the dom with native dom-features and jQuery.
* [IWC Demo](./IwcDemo/) More IWC example widgets.
Please note, that the XMPP Connector is the best supported Connector at the moment.
Note: currently only the XMPP stuff is supported.

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>PeerJs Json Example</title>
<script src="../../../Connector/xmpp-connector/strophe.js"></script>
<script src="../../../Connector/bower_components/strophejs-plugins/muc/strophe.muc.js"></script>
<script src="../../../Connector/xmpp-connector/xmpp-connector.js"></script>
<script src="../../build/browser/yatta.js"></script>
<script src="./index.js"></script>
</head>
<body>
<h1> PeerJs + Json Tutorial</h1>
<p> Collaborative Json editing with <a href="https://github.com/DadaMonad/Yatta/">Yatta</a>
and <a href="http://peerjs.com/">PeerJs</a> (WebRTC). </p>
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
<p> <a href="https://github.com/DadaMonad/Yatta/">Yatta</a> is a Framework for Real-Time collaboration on arbitrary data structures.
You can find the code for this example <a href="https://github.com/DadaMonad/Yatta/tree/master/examples/PeerJs-Json">here</a>.
</p>
</body>
</html>

View File

@@ -1,53 +0,0 @@
/**
## PeerJs + JSON Example
Here, I will give a short overview on how to enable collaborative json with the
[PeerJs](http://peerjs.com/) Connector and the Json Framework. Open
[index.html](http://dadamonad.github.io/Yatta/examples/PeerJs-Json/index.html) in your Browser and
use the console to explore Yatta!
[PeerJs](http://peerjs.com) is a Framework that enables you to connect to other peers. You just need the
user-id of the peer (browser/client). And then you can connect to it.
First you have to include the following libraries in your html file:
```
<script src="http://cdn.peerjs.com/0.3/peer.js"></script>
<script src="../../build/browser/Frameworks/JsonFramework.js"></script>
<script src="../../build/browser/Connectors/PeerJsConnector.js"></script>
<script src="./index.js"></script>
```
### Create Connector
The PeerJs Framework requires an API key, or you need to set up your own PeerJs server.
Get an API key from the [Website](http://peerjs.com/peerserver).
The first parameter of `createPeerJsConnector` is forwarded as the options object in PeerJs.
Therefore, you may also specify the server/port here, if you have set up your own server.
*/
var yatta, yattaHandler;
/**
This will connect to the server owned by the peerjs team.
For now, you can use my API key.
*/
connector = new XMPPConnector();
/**
### Yatta
yatta is the shared json object. If you change something on this object,
it will be instantly shared with all the other collaborators.
*/
yatta = new Yatta(connector);
window.onload = function(){
var textbox = document.getElementById("textfield");
yatta.observe(function(events){
for(var i=0; i<events.length; i++){
var event = events[i];
if(event.name === "textfield" && event.type !== "delete"){
yatta.val("textfield").bind(textbox);
}
}
});
yatta.val("textfield","");
};

View File

@@ -1,106 +0,0 @@
## Text Editing Example
Here, I will give a short overview on how to enable collaborative text editing with the
[PeerJs](http://peerjs.com/) Connector and the TextFramework Framework.
PeerJs is a Framework that enables you to connect to other peers. You just need the
user-id of the peer (browser/client). And then you can connect to it. In this example we will encode
the client-id to which this client shall connect, in the url.
It should look like this: http://../index.html?user_id
First you have to include the following libraries in your html file:
```
<script src="http://cdn.peerjs.com/0.3/peer.js"></script>
<script src="../../build/browser/Frameworks/TextFramework.js"></script>
<script src="../../build/browser/Connectors/PeerJsConnector.js"></script>
<script src="./index.js"></script>
```
Open [index.html](./index.html) in order to start collaboration.
```js
var yatta;
function init(){
```
First create the connector - the underlaying communication protocol.
Here, we use the PeerJs connector. Its first parameter is the API key that you need to specify (see [website](http://peerjs.com/))
This will connect to the server owned by the peerjs team.
For now, you can use my API key.
```js
// var conn = {key: 'h7nlefbgavh1tt9'};
```
This will connect to one of my peerjs instances.
I can't guaranty that this will be always up. This is why you should use the previous method with the api key,
or set up your own server.
```js
var conn = {
host: "terrific-peerjs.herokuapp.com",
port: "", // this works because heroku can forward to the right port.
// debug: true,
};
Y.createPeerJsConnector(conn, function(Connector, user_id){
```
TextFramework is a shared text object. If you change something on this object,
it will be instantaneously shared with all the other collaborators.
```js
yatta = new Y.TextFramework(user_id, Connector);
```
Get the url of this frame. If it has a url-encoded parameter
we will connect to the foreign peer.
```js
var url = window.location.href;
var peer_id = location.search
var url = url.substring(0,-peer_id.length);
peer_id = peer_id.substring(1);
```
Set the shareable link.
```js
document.getElementById("peer_link").setAttribute("href",url+"?"+user_id);
```
Connect to other peer.
```js
if (peer_id.length > 0){
yatta.connector.connectToPeer(peer_id);
}
```
Bind yatta to the textfield.
The .bind property is a method of the Word class. You can also use it with all the other Frameworks in Yatta (e.g. Json).
```js
var textbox = document.getElementById("textfield");
yatta.bind(textbox);
});
}
window.onload = init
```

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>PeerJs Json Example</title>
<script src="../../bower_components/peerjs/peer.js"></script>
<script src="../../bower_components/connector/peerjs-connector/peerjs-connector.js"></script>
<script src="../../build/browser/yatta.js"></script>
<script src="./index.js"></script>
</head>
<body>
<h1> Text Editing Demo</h1>
<p> Collaborative text editing with <a href="https://github.com/DadaMonad/Yatta/">Yatta</a>
and <a href="http://peerjs.com/">PeerJs</a> (WebRTC). Open this link in other browsers: <a id="peer_link" target="_blank">Drop me </a> </p>
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
<p> <a href="https://github.com/DadaMonad/Yatta/">Yatta</a> is a Framework for Real-Time collaboration on arbitrary data structures.
You can find the code for this example <a href="https://github.com/DadaMonad/Yatta/tree/master/examples/TextEditing">here</a>.
</p>
</body>
</html>

View File

@@ -1,92 +0,0 @@
/**
## Text Editing Example
Here, I will give a short overview on how to enable collaborative text editing with the
[PeerJs](http://peerjs.com/) Connector and the TextFramework Framework.
PeerJs is a Framework that enables you to connect to other peers. You just need the
user-id of the peer (browser/client). And then you can connect to it. In this example we will encode
the client-id to which this client shall connect, in the url.
It should look like this: http://../index.html?user_id
First you have to include the following libraries in your html file:
```
<script src="../../bower_components/peerjs/peer.js"></script>
<script src="../../bower_components/connector/peerjs-connector/peerjs-connector.js"></script>
<script src="../../yatta.js"></script>
<script src="./index.js"></script>
```
Open [index.html](./index.html) in order to start collaboration.
*/
var yatta;
var connector;
function init(){
/**
First create the connector - the underlaying communication protocol.
Here, we use the PeerJs connector. Its first parameter is the API key that you need to specify (see [website](http://peerjs.com/))
*/
/**
This will connect to the server owned by the peerjs team.
For now, you can use my API key.
*/
var options = {key: 'h7nlefbgavh1tt9'};
/**
This will connect to one of my peerjs instances.
I can't guaranty that this will be always up. This is why you should use the previous method with the api key,
or set up your own server.
*/
/*var options = {
host: "terrific-peerjs.herokuapp.com",
port: "", // this works because heroku can forward to the right port.
// debug: true,
};*/
var user_id = Math.ceil(Math.random()*1000);
connector = new PeerJsConnector(user_id, options);
/**
TextFramework is a shared text object. If you change something on this object,
it will be instantaneously shared with all the other collaborators.
*/
yatta = new Yatta(connector);
yatta.val()
/**
Get the url of this frame. If it has a url-encoded parameter
we will connect to the foreign peer.
*/
var url = window.location.href;
var peer_id = location.search
var url = url.substring(0,-peer_id.length);
peer_id = peer_id.substring(1);
/**
Set the shareable link.
*/
document.getElementById("peer_link").setAttribute("href",url+"?"+user_id);
/**
Connect to other peer.
*/
if (peer_id.length > 0){
yatta.connector.join(peer_id);
}
/**
Bind yatta to the textfield.
The .bind property is a method of the Word class. You can also use it with all the other Frameworks in Yatta (e.g. Json).
*/
var textbox = document.getElementById("textfield");
function textbind(){
yatta.val("textbox").bind(textbox);
}
if(peer_id.length > 0){
connector.whenSynced([textbind]);
} else {
yatta.val("textbox",textbox.value)
textbind()
}
}
window.onload = init

View File

@@ -8,7 +8,7 @@
<script src="./index.js"></script>
</head>
<body>
<h1> PeerJs + Json Tutorial</h1>
<h1 contentEditable> PeerJs + Json Tutorial</h1>
<p> Collaborative Json editing with <a href="https://github.com/DadaMonad/Yatta/">Yatta</a>
and <a href="http://peerjs.com/">PeerJs</a> (WebRTC). </p>

View File

@@ -30,8 +30,8 @@ var yatta, yattaHandler;
For now, you can use my API key.
*/
connector = new XMPPConnector();
connector = new XMPPConnector("testy-xmpp-json2");
connector.debug = true
/**
### Yatta
yatta is the shared json object. If you change something on this object,
@@ -45,9 +45,15 @@ window.onload = function(){
for(var i=0; i<events.length; i++){
var event = events[i];
if(event.name === "textfield" && event.type !== "delete"){
yatta.val("textfield").bind(textbox);
//yatta.val("textfield").bind(textbox);
yatta.val("textfield").bind(document.querySelector("h1"))
}
}
});
yatta.val("textfield","");
connector.whenSynced(function(){
if(yatta.val("textfield") == null){
yatta.val("textfield","stuff", "mutable");
}
})
};

View File

@@ -5,15 +5,15 @@ setTimeout(function(){
x.yatta.val("stuff",{otherstuff:{nostuff:"this is no stuff"}})
setTimeout(function(){
var res = x.yatta.val("stuff");
if(!(x.nostuff.val() === "this is no stuff")){
console.log("Deep inherit doesn't work")
if(!(x.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(!(x.nostuff.val() === "this is also no stuff")){
if(!(x.nostuff === "this is also no stuff")){
console.log("Element val overwrite doesn't work")
}
console.log("res");
console.log("Everything is fine :)");
},500)
},500);
},3000)

View File

@@ -4,7 +4,8 @@
<polymer-element name="yatta-test" attributes="yatta connector stuff">
<template>
<xmpp-connector id="connector" connector={{connector}}></xmpp-connector>
<h1 id="text" contentEditable> Check this out !</h1>
<xmpp-connector id="connector" connector={{connector}} room="testy-xmpp-polymer"></xmpp-connector>
<yatta-element connector={{connector}} val={{yatta}}>
<yatta-property name="slider" val={{slider}}>
</yatta-property>
@@ -23,8 +24,14 @@
Polymer({
ready: function(){
window.y_stuff_property = this.$.otherstuff;
this.yatta.val("slider",50)
var that = this;
this.connector.whenSynced(function(){
if(that.yatta.val("text") == null){
that.yatta.val("text","stuff","mutable");
}
that.yatta.val("text").bind(that.$.text,that.shadowRoot)
})
}
})
</script>

View File

@@ -70,8 +70,8 @@ gulp.task 'build_browser', ->
extname: ".js"
.pipe gulp.dest './build/test/'
gulp.task 'watch', ['build_browser','mocha'], ->
gulp.watch files.all, ['build_browser', 'mocha']
gulp.task 'watch', ['build_browser'], ->
gulp.watch files.all, ['build_browser']
gulp.task 'mocha', ->
gulp.src files.test, { read: false }

View File

@@ -26,21 +26,25 @@ adaptConnector = (connector, engine, HB, execution_listener)->
state_vector[s.user] = s.state
state_vector
sendStateVector = ()->
getStateVector = ()->
encode_state_vector HB.getOperationCounter()
sendHb = (v)->
getHB = (v)->
state_vector = parse_state_vector v
hb = HB._encode state_vector
for o in hb
o.fromHB = "true" # execute immediately
json =
hb: HB._encode(state_vector)
hb: hb
state_vector: encode_state_vector HB.getOperationCounter()
json
applyHb = (res)->
HB.renewStateVector parse_state_vector res.state_vector
engine.applyOpsCheckDouble res.hb
applyHB = (hb)->
engine.applyOp hb
connector.whenSyncing sendStateVector, sendHb, applyHb
connector.getStateVector = getStateVector
connector.getHB = getHB
connector.applyHB = applyHB
connector.whenReceiving (sender, op)->
if op.uid.creator isnt HB.getUserId()

View File

@@ -11,18 +11,18 @@ class Engine
#
# @param {HistoryBuffer} HB
# @param {Array} parser Defines how to parse encoded messages.
# @param {Object} types list of available types
#
constructor: (@HB, @parser)->
constructor: (@HB, @types)->
@unprocessed_ops = []
#
# Parses an operatio from the json format. It uses the specified parser in your OperationType module.
#
parseOperation: (json)->
typeParser = @parser[json.type]
if typeParser?
typeParser json
type = @types[json.type]
if type?.parse?
type.parse json
else
throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}."
@@ -67,10 +67,12 @@ class Engine
for op_json in op_json_array
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
o = @parseOperation op_json
if op_json.fromHB?
o.fromHB = op_json.fromHB
# @HB.addOperation o
if @HB.getOperation(o)?
# nop
else if (not @HB.isExpectedOperation(o)) or (not o.execute())
else if ((not @HB.isExpectedOperation(o)) and (not o.fromHB?)) or (not o.execute())
@unprocessed_ops.push o
window?.unprocessed_types.push o.type # TODO: delete this
@tryUnprocessed()
@@ -86,7 +88,7 @@ class Engine
for op in @unprocessed_ops
if @HB.getOperation(op)?
# nop
else if (not @HB.isExpectedOperation(op)) or (not op.execute())
else if (not @HB.isExpectedOperation(op) and (not op.fromHB?)) or (not op.execute())
unprocessed.push op
@unprocessed_ops = unprocessed
if @unprocessed_ops.length is old_length

View File

@@ -18,7 +18,7 @@ class HistoryBuffer
@garbage = [] # Will be cleaned on next call of garbageCollector
@trash = [] # Is deleted. Wait until it is not used anymore.
@performGarbageCollection = true
@garbageCollectTimeout = 20000
@garbageCollectTimeout = 30000
@reserved_identifier_counter = 0
setTimeout @emptyGarbage, @garbageCollectTimeout
@@ -99,6 +99,7 @@ class HistoryBuffer
isExpectedOperation: (o)->
@operation_counter[o.uid.creator] ?= 0
o.uid.op_number <= @operation_counter[o.uid.creator]
true #TODO: !! this could break stuff. But I dunno why
#
# Encode this operation in such a way that it can be parsed by remote peers.
@@ -152,10 +153,17 @@ class HistoryBuffer
#
# Retrieve an operation from a unique id.
#
# when uid has a "sub" property, the value of it will be applied
# on the operations retrieveSub method (which must! be defined)
#
getOperation: (uid)->
if uid.uid?
uid = uid.uid
@buffer[uid.creator]?[uid.op_number]
o = @buffer[uid.creator]?[uid.op_number]
if uid.sub? and o?
o.retrieveSub uid.sub
else
o
#
# Add an operation to the HB. Note that this will not link it against
@@ -166,7 +174,7 @@ class HistoryBuffer
@buffer[o.uid.creator] = {}
if @buffer[o.uid.creator][o.uid.op_number]?
throw new Error "You must not overwrite operations!"
if (o.uid.op_number.constructor isnt String) and (not @isExpectedOperation(o)) # you already do this in the engine, so delete it here!
if (o.uid.op_number.constructor isnt String) and (not @isExpectedOperation(o)) and (not o.fromHB?) # you already do this in the engine, so delete it here!
throw new Error "this operation was not expected!"
@addToCounter(o)
@buffer[o.uid.creator][o.uid.op_number] = o

View File

@@ -1,6 +1,6 @@
module.exports = (HB)->
# @see Engine.parse
parser = {}
types = {}
execution_listener = []
#
@@ -16,7 +16,7 @@ module.exports = (HB)->
#
# Furthermore an encodable operation has a parser. We extend the parser object in order to parse encoded operations.
#
class Operation
class types.Operation
#
# @param {Object} uid A unique identifier.
@@ -31,6 +31,9 @@ module.exports = (HB)->
type: "Operation"
retrieveSub: ()->
throw new Error "sub properties are not enable on this operation type!"
#
# Add an event listener. It depends on the operation which events are supported.
# @param {Function} f f is executed in case the event fires.
@@ -101,7 +104,16 @@ module.exports = (HB)->
# Computes a unique identifier (uid) that identifies this operation.
#
getUid: ()->
@uid
if not @uid.noOperation?
@uid
else
@uid.alt # could be (safely) undefined
cloneUid: ()->
uid = {}
for n,v of @getUid()
uid[n] = v
uid
dontSync: ()->
@uid.doSync = false
@@ -119,9 +131,10 @@ module.exports = (HB)->
# There is only one other place, where this can be done - before an Insertion
# is executed (because we need the creator_id)
@uid = HB.getNextOperationIdentifier()
HB.addOperation @
for l in execution_listener
l @_encode()
if not @uid.noOperation?
HB.addOperation @
for l in execution_listener
l @_encode()
@
#
@@ -179,13 +192,11 @@ module.exports = (HB)->
@unchecked = uninstantiated
success
#
# @nodoc
# A simple Delete-type operation that deletes an operation.
#
class Delete extends Operation
class types.Delete extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
@@ -225,12 +236,12 @@ module.exports = (HB)->
#
# Define how to parse Delete operations.
#
parser['Delete'] = (o)->
types.Delete.parse = (o)->
{
'uid' : uid
'deletes': deletes_uid
} = o
new Delete uid, deletes_uid
new this(uid, deletes_uid)
#
# @nodoc
@@ -242,14 +253,15 @@ module.exports = (HB)->
# - The short-list (abbrev. sl) maintains only the operations that are not deleted
# - The complete-list (abbrev. cl) maintains all operations
#
class Insert extends Operation
class types.Insert extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
#
constructor: (uid, prev_cl, next_cl, origin)->
constructor: (uid, prev_cl, next_cl, origin, parent)->
@saveOperation 'parent', parent
@saveOperation 'prev_cl', prev_cl
@saveOperation 'next_cl', next_cl
if origin?
@@ -283,7 +295,6 @@ module.exports = (HB)->
@prev_cl.applyDelete()
cleanup: ()->
# TODO: Debugging
if @next_cl.isDeleted()
# delete all ops that delete this insertion
for d in @deleted_by
@@ -300,8 +311,9 @@ module.exports = (HB)->
@prev_cl.next_cl = @next_cl
@next_cl.prev_cl = @prev_cl
super
else if @next_cl? and @prev_cl?
throw new Error "This insertion was not supposed to be deleted!"
# else
# Someone inserted something in the meantime.
# Remember: this can only be garbage collected when next_cl is deleted
#
# @private
@@ -324,6 +336,13 @@ module.exports = (HB)->
if not @validateSavedOperations()
return false
else
if @parent?
if not @prev_cl?
@prev_cl = @parent.beginning
if not @origin?
@origin = @parent.beginning
if not @next_cl?
@next_cl = @parent.end
if @prev_cl?
distance_to_origin = @getDistanceToOrigin() # most cases: 0
o = @prev_cl.next_cl
@@ -402,7 +421,7 @@ module.exports = (HB)->
position = 0
prev = @prev_cl
while true
if prev instanceof Delimiter
if prev instanceof types.Delimiter
break
if not prev.isDeleted()
position++
@@ -413,14 +432,14 @@ module.exports = (HB)->
# @nodoc
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
#
class ImmutableObject extends Operation
class types.ImmutableObject extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} content
#
constructor: (uid, @content, prev, next, origin)->
super uid, prev, next, origin
constructor: (uid, @content)->
super uid
type: "ImmutableObject"
@@ -435,27 +454,18 @@ module.exports = (HB)->
#
_encode: ()->
json = {
'type': "ImmutableObject"
'type': @type
'uid' : @getUid()
'content' : @content
}
if @prev_cl?
json['prev'] = @prev_cl.getUid()
if @next_cl?
json['next'] = @next_cl.getUid()
if @origin? # and @origin isnt @prev_cl
json["origin"] = @origin().getUid()
json
parser['ImmutableObject'] = (json)->
types.ImmutableObject.parse = (json)->
{
'uid' : uid
'content' : content
'prev': prev
'next': next
'origin' : origin
} = json
new ImmutableObject uid, content, prev, next, origin
new this(uid, content)
#
# @nodoc
@@ -463,17 +473,17 @@ module.exports = (HB)->
# This is necessary in order to have a beginning and an end even if the content
# of the Engine is empty.
#
class Delimiter extends Operation
class types.Delimiter extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
#
constructor: (uid, prev_cl, next_cl, origin)->
constructor: (prev_cl, next_cl, origin)->
@saveOperation 'prev_cl', prev_cl
@saveOperation 'next_cl', next_cl
@saveOperation 'origin', prev_cl
super uid
super {noOperation: true}
type: "Delimiter"
@@ -516,29 +526,23 @@ module.exports = (HB)->
#
_encode: ()->
{
'type' : "Delimiter"
'type' : @type
'uid' : @getUid()
'prev' : @prev_cl?.getUid()
'next' : @next_cl?.getUid()
}
parser['Delimiter'] = (json)->
types.Delimiter.parse = (json)->
{
'uid' : uid
'prev' : prev
'next' : next
} = json
new Delimiter uid, prev, next
new this(uid, prev, next)
# This is what this module exports after initializing it with the HistoryBuffer
{
'types' :
'Delete' : Delete
'Insert' : Insert
'Delimiter': Delimiter
'Operation': Operation
'ImmutableObject' : ImmutableObject
'parser' : parser
'types' : types
'execution_listener' : execution_listener
}

View File

@@ -3,100 +3,11 @@ text_types_uninitialized = require "./TextTypes"
module.exports = (HB)->
text_types = text_types_uninitialized HB
types = text_types.types
parser = text_types.parser
createJsonTypeWrapper = (_jsonType)->
#
# @note EXPERIMENTAL
#
# A JsonTypeWrapper was intended to be a convenient wrapper for the JsonType.
# But it can make things more difficult than they are.
# @see JsonType
#
# @example create a JsonTypeWrapper
# # You get a JsonTypeWrapper from a JsonType by calling
# w = yatta.value
#
# It creates Javascripts -getter and -setter methods for each property that JsonType maintains.
# @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
#
# @example Getter Example
# # you can access the x property of yatta by calling
# w.x
# # instead of
# yatta.val('x')
#
# @note You can only overwrite existing values! Setting a new property won't have any effect!
#
# @example Setter Example
# # you can set an existing x property of yatta by calling
# w.x = "text"
# # instead of
# yatta.val('x', "text")
#
# In order to set a new property you have to overwrite an existing property.
# Therefore the JsonTypeWrapper supports a special feature that should make things more convenient
# (we can argue about that, use the JsonType if you don't like it ;).
# If you overwrite an object property of the JsonTypeWrapper with a new object, it will result in a merged version of the objects.
# Let `yatta.value.p` the property that is to be overwritten and o the new value. E.g. `yatta.value.p = o`
# * The result has all properties of o
# * The result has all properties of w.p if they don't occur under the same property-name in o.
#
# @example Conflict Example
# yatta.value = {a : "string"}
# w = yatta.value
# console.log(w) # {a : "string"}
# w.a = {a : {b : "string"}}
# console.log(w) # {a : {b : "String"}}
# w.a = {a : {c : 4}}
# console.log(w) # {a : {b : "String", c : 4}}
#
# @example Common Pitfalls
# w = yatta.value
# # Setting a new property
# w.newProperty = "Awesome"
# console.log(w.newProperty == "Awesome") # false, w.newProperty is undefined
# # overwrite the w object
# w = {newProperty : "Awesome"}
# console.log(w.newProperty == "Awesome") # true!, but ..
# console.log(yatta.value.newProperty == "Awesome") # false, you are only allowed to set properties!
# # The solution
# yatta.value = {newProperty : "Awesome"}
# console.log(w.newProperty == "Awesome") # true!
#
class JsonTypeWrapper
#
# @param {JsonType} jsonType Instance of the JsonType that this class wrappes.
#
constructor: (jsonType)->
for name, obj of jsonType.map
do (name, obj)->
Object.defineProperty JsonTypeWrapper.prototype, name,
get : ->
x = obj.val()
if x instanceof JsonType
createJsonTypeWrapper x
else if x instanceof types.ImmutableObject
x.val()
else
x
set : (o)->
overwrite = jsonType.val(name)
if o.constructor is {}.constructor and overwrite instanceof types.Operation
for o_name,o_obj of o
overwrite.val(o_name, o_obj, 'immutable')
else
jsonType.val(name, o, 'immutable')
enumerable: true
configurable: false
new JsonTypeWrapper _jsonType
#
# Manages Object-like values.
#
class JsonType extends types.MapManager
class types.Object extends types.MapManager
#
# Identifies this class.
@@ -104,11 +15,11 @@ module.exports = (HB)->
#
# @example
# var x = yatta.val('unknown')
# if (x.type === "JsonType") {
# if (x.type === "Object") {
# console.log JSON.stringify(x.toJson())
# }
#
type: "JsonType"
type: "Object"
applyDelete: ()->
super()
@@ -130,7 +41,9 @@ module.exports = (HB)->
val = @val()
json = {}
for name, o of val
if o instanceof JsonType
if o instanceof types.Object
json[name] = o.toJson(transform_to_value)
else if o instanceof types.Array
json[name] = o.toJson(transform_to_value)
else if transform_to_value and o instanceof types.Operation
json[name] = o.val()
@@ -171,24 +84,6 @@ module.exports = (HB)->
changedBy:event.changedBy
@bound_json
#
# Whether the default is 'mutable' (true) or 'immutable' (false)
#
mutable_default:
false
#
# Set if the default is 'mutable' or 'immutable'
# @param {String|Boolean} mutable Set either 'mutable' / true or 'immutable' / false
setMutableDefault: (mutable)->
if mutable is true or mutable is 'mutable'
JsonType.prototype.mutable_default = true
else if mutable is false or mutable is 'immutable'
JsonType.prototype.mutable_default = false
else
throw new Error 'Set mutable either "mutable" or "immutable"!'
'OK'
#
# @overload val()
# Get this as a Json object.
@@ -197,70 +92,56 @@ module.exports = (HB)->
# @overload val(name)
# Get value of a property.
# @param {String} name Name of the object property.
# @return [JsonType|WordType|String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object.
# @return [Object Type||String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object.
#
# @overload val(name, content)
# Set a new property.
# @param {String} name Name of the object property.
# @param {Object|String} content Content of the object property.
# @return [JsonType] This object. (supports chaining)
# @return [Object Type] This object. (supports chaining)
#
val: (name, content, mutable)->
val: (name, content)->
if name? and arguments.length > 1
if mutable?
if mutable is true or mutable is 'mutable'
mutable = true
if content? and content.constructor?
type = types[content.constructor.name]
if type? and type.create?
args = []
for i in [1...arguments.length]
args.push arguments[i]
o = type.create.apply null, args
super name, o
else
mutable = false
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Yatta."
else
mutable = @mutable_default
if typeof content is 'function'
@ # Just do nothing
else if (not content?) or (((not mutable) or typeof content is 'number') and content.constructor isnt Object)
super name, (new types.ImmutableObject undefined, content).execute()
else
if typeof content is 'string'
word = (new types.WordType undefined).execute()
word.insertText 0, content
super name, word
else if content.constructor is Object
json = new JsonType().execute()
for n,o of content
json.val n, o, mutable
super name, json
else
throw new Error "You must not set #{typeof content}-types in collaborative Json-objects!"
else
super name, content
Object.defineProperty JsonType.prototype, 'value',
get : -> createJsonTypeWrapper @
set : (o)->
if o.constructor is {}.constructor
for o_name,o_obj of o
@val(o_name, o_obj, 'immutable')
else
throw new Error "You must only set Object values!"
super name, content
else # is this even necessary ? I have to define every type anyway.. (see Number type below)
super name
#
# @private
#
_encode: ()->
{
'type' : "JsonType"
'type' : @type
'uid' : @getUid()
}
parser['JsonType'] = (json)->
types.Object.parse = (json)->
{
'uid' : uid
} = json
new JsonType uid
new this(uid)
types.Object.create = (content, mutable)->
json = new types.Object().execute()
for n,o of content
json.val n, o, mutable
json
types['JsonType'] = JsonType
types.Number = {}
types.Number.create = (content)->
content
text_types

View File

@@ -3,13 +3,12 @@ basic_types_uninitialized = require "./BasicTypes"
module.exports = (HB)->
basic_types = basic_types_uninitialized HB
types = basic_types.types
parser = basic_types.parser
#
# @nodoc
# Manages map like objects. E.g. Json-Type and XML attributes.
#
class MapManager extends types.Operation
class types.MapManager extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
@@ -32,136 +31,60 @@ module.exports = (HB)->
# @see JsonTypes.val
#
val: (name, content)->
if content?
if not @map[name]?
(new AddName undefined, @, name).execute()
@map[name].replace content
if arguments.length > 1
@retrieveSub(name).replace content
@
else if name?
prop = @map[name]
if prop? and not prop.isContentDeleted()
obj = prop.val()
if obj instanceof types.ImmutableObject
obj.val()
else
obj
prop.val()
else
undefined
else
result = {}
for name,o of @map
if not o.isContentDeleted()
obj = o.val()
if obj instanceof types.ImmutableObject # or obj instanceof MapManager TODO: do you want deep json?
obj = obj.val()
result[name] = obj
result[name] = o.val()
result
delete: (name)->
@map[name]?.deleteContent()
@
#
# @nodoc
# When a new property in a map manager is created, then the uids of the inserted Operations
# must be unique (think about concurrent operations). Therefore only an AddName operation is allowed to
# add a property in a MapManager. If two AddName operations on the same MapManager name happen concurrently
# only one will AddName operation will be executed.
#
class AddName extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} map_manager Uid or reference to the MapManager.
# @param {String} name Name of the property that will be added.
#
constructor: (uid, map_manager, @name)->
@saveOperation 'map_manager', map_manager
super uid
type: "AddName"
applyDelete: ()->
super()
cleanup: ()->
super()
#
# If map_manager doesn't have the property name, then add it.
# The ReplaceManager that is being written on the property is unique
# in such a way that if AddName is executed (from another peer) it will
# always have the same result (ReplaceManager, and its beginning and end are the same)
#
execute: ()->
if not @validateSavedOperations()
return false
else
# helper for cloning an object
clone = (o)->
p = {}
for name,value of o
p[name] = value
p
uid_r = clone(@map_manager.getUid())
uid_r.doSync = false
uid_r.op_number = "_#{uid_r.op_number}_RM_#{@name}"
if not HB.getOperation(uid_r)?
uid_beg = clone(uid_r)
uid_beg.op_number = "#{uid_r.op_number}_beginning"
uid_end = clone(uid_r)
uid_end.op_number = "#{uid_r.op_number}_end"
beg = (new types.Delimiter uid_beg, undefined, uid_end).execute()
end = (new types.Delimiter uid_end, beg, undefined).execute()
event_properties =
name: @name
event_this = @map_manager
@map_manager.map[@name] = new ReplaceManager event_properties, event_this, uid_r, beg, end
@map_manager.map[@name].setParent @map_manager, @name
(@map_manager.map[@name].add_name_ops ?= []).push @
@map_manager.map[@name].execute()
super
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
{
'type' : "AddName"
'uid' : @getUid()
'map_manager' : @map_manager.getUid()
'name' : @name
}
parser['AddName'] = (json)->
{
'map_manager' : map_manager
'uid' : uid
'name' : name
} = json
new AddName uid, map_manager, name
retrieveSub: (property_name)->
if not @map[property_name]?
event_properties =
name: property_name
event_this = @
map_uid = @cloneUid()
map_uid.sub = property_name
rm_uid =
noOperation: true
alt: map_uid
rm = new types.ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB
@map[property_name] = rm
rm.setParent @, property_name
rm.execute()
@map[property_name]
#
# @nodoc
# Manages a list of Insert-type operations.
#
class ListManager extends types.Operation
class types.ListManager extends types.Operation
#
# A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!)
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Delimiter} beginning Reference or Object.
# @param {Delimiter} end Reference or Object.
constructor: (uid, beginning, end, prev, next, origin)->
if beginning? and end?
@saveOperation 'beginning', beginning
@saveOperation 'end', end
else
@beginning = new types.Delimiter undefined, undefined, undefined
@end = new types.Delimiter undefined, @beginning, undefined
@beginning.next_cl = @end
@beginning.execute()
@end.execute()
super uid, prev, next, origin
constructor: (uid)->
@beginning = new types.Delimiter undefined, undefined
@end = new types.Delimiter @beginning, undefined
@beginning.next_cl = @end
@beginning.execute()
@end.execute()
super uid
type: "ListManager"
@@ -225,10 +148,10 @@ module.exports = (HB)->
# Adds support for replace. The ReplaceManager manages Replaceable operations.
# Each Replaceable holds a value that is now replaceable.
#
# The WordType-type has implemented support for replace
# @see WordType
# The TextType-type has implemented support for replace
# @see TextType
#
class ReplaceManager extends ListManager
class types.ReplaceManager extends types.ListManager
#
# @param {Object} event_properties Decorates the event that is thrown by the RM
# @param {Object} event_this The object on which the event shall be executed
@@ -236,10 +159,10 @@ module.exports = (HB)->
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Delimiter} beginning Reference or Object.
# @param {Delimiter} end Reference or Object.
constructor: (@event_properties, @event_this, uid, beginning, end, prev, next, origin)->
constructor: (@event_properties, @event_this, uid, beginning, end)->
if not @event_properties['object']?
@event_properties['object'] = @event_this
super uid, beginning, end, prev, next, origin
super uid, beginning, end
type: "ReplaceManager"
@@ -248,10 +171,6 @@ module.exports = (HB)->
while o?
o.applyDelete()
o = o.next_cl
# if this was created by an AddName operation, delete it too
if @add_name_ops?
for o in @add_name_ops
o.applyDelete()
super()
cleanup: ()->
@@ -280,7 +199,7 @@ module.exports = (HB)->
#
replace: (content, replaceable_uid)->
o = @getLastOperation()
relp = (new Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
relp = (new types.Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
# TODO: delete repl (for debugging)
undefined
@@ -292,7 +211,7 @@ module.exports = (HB)->
undefined
#
# Get the value of this WordType
# Get the value of this
# @return {String}
#
val: ()->
@@ -307,36 +226,19 @@ module.exports = (HB)->
_encode: ()->
json =
{
'type': "ReplaceManager"
'type': @type
'uid' : @getUid()
'beginning' : @beginning.getUid()
'end' : @end.getUid()
}
if @prev_cl? and @next_cl?
json['prev'] = @prev_cl.getUid()
json['next'] = @next_cl.getUid()
if @origin? # TODO: do this everywhere: and @origin isnt @prev_cl
json["origin"] = @origin().getUid()
json
parser["ReplaceManager"] = (json)->
{
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'beginning' : beginning
'end' : end
} = json
new ReplaceManager uid, beginning, end, prev, next, origin
#
# @nodoc
# The ReplaceManager manages Replaceables.
# @see ReplaceManager
#
class Replaceable extends types.Insert
class types.Replaceable extends types.Insert
#
# @param {Operation} content The value that this Replaceable holds.
@@ -344,11 +246,13 @@ module.exports = (HB)->
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (content, parent, uid, prev, next, origin, is_deleted)->
@saveOperation 'content', content
# see encode to see, why we are doing it this way
if content? and content.creator?
@saveOperation 'content', content
else
@content = content
@saveOperation 'parent', parent
if not (prev? and next?)
throw new Error "You must define prev, and next for Replaceable-types!"
super uid, prev, next, origin
super uid, prev, next, origin # Parent is already saved by Replaceable
@is_deleted = is_deleted
type: "Replaceable"
@@ -363,9 +267,9 @@ module.exports = (HB)->
res = super
if @content?
if @next_cl.type isnt "Delimiter"
@content.deleteAllObservers()
@content.applyDelete()
@content.dontSync()
@content.deleteAllObservers?()
@content.applyDelete?()
@content.dontSync?()
@content = null
res
@@ -413,34 +317,36 @@ module.exports = (HB)->
_encode: ()->
json =
{
'type': "Replaceable"
'content': @content?.getUid()
'replace_manager' : @parent.getUid()
'type': @type
'parent' : @parent.getUid()
'prev': @prev_cl.getUid()
'next': @next_cl.getUid()
'origin' : @origin.getUid()
'uid' : @getUid()
'is_deleted': @is_deleted
}
if @origin? and @origin isnt @prev_cl
json["origin"] = @origin.getUid()
if @content instanceof types.Operation
json['content'] = @content.getUid()
else
# This could be a security concern.
# Throw error if the users wants to trick us
if @content? and @content.creator?
throw new Error "You must not set creator here!"
json['content'] = @content
json
parser["Replaceable"] = (json)->
types.Replaceable.parse = (json)->
{
'content' : content
'replace_manager' : parent
'parent' : parent
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'is_deleted': is_deleted
} = json
new Replaceable content, parent, uid, prev, next, origin, is_deleted
new this(content, parent, uid, prev, next, origin, is_deleted)
types['ListManager'] = ListManager
types['MapManager'] = MapManager
types['ReplaceManager'] = ReplaceManager
types['Replaceable'] = Replaceable
basic_types

View File

@@ -5,31 +5,21 @@ module.exports = (HB)->
types = structured_types.types
parser = structured_types.parser
#
# @nodoc
# At the moment TextDelete type equals the Delete type in BasicTypes.
# @see BasicTypes.Delete
#
class TextDelete extends types.Delete
parser["TextDelete"] = parser["Delete"]
#
# @nodoc
# Extends the basic Insert type to an operation that holds a text value
#
class TextInsert extends types.Insert
class types.TextInsert extends types.Insert
#
# @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (content, uid, prev, next, origin)->
if content?.uid?.creator
constructor: (content, uid, prev, next, origin, parent)->
if content?.creator
@saveOperation 'content', content
else
@content = content
if not (prev? and next?)
throw new Error "You must define prev, and next for TextInsert-types!"
super uid, prev, next, origin
super uid, prev, next, origin, parent
type: "TextInsert"
@@ -74,54 +64,35 @@ module.exports = (HB)->
_encode: ()->
json =
{
'type': "TextInsert"
'type': @type
'uid' : @getUid()
'prev': @prev_cl.getUid()
'next': @next_cl.getUid()
'origin': @origin.getUid()
'parent': @parent.getUid()
}
if @content?.getUid?
json['content'] = @content.getUid()
else
json['content'] = @content
if @origin isnt @prev_cl
json["origin"] = @origin.getUid()
json
parser["TextInsert"] = (json)->
types.TextInsert.parse = (json)->
{
'content' : content
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'parent' : parent
} = json
new TextInsert content, uid, prev, next, origin
new types.TextInsert content, uid, prev, next, origin, parent
#
# Handles a WordType-like data structures with support for insertText/deleteText at a word-position.
# @note Currently, only Text is supported!
#
class WordType extends types.ListManager
#
# @private
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (uid, beginning, end, prev, next, origin)->
@textfields = []
super uid, beginning, end, prev, next, origin
class types.Array extends types.ListManager
#
# Identifies this class.
# Use it to check whether this is a word-type or something else.
#
# @example
# var x = yatta.val('unknown')
# if (x.type === "WordType") {
# console.log JSON.stringify(x.toJson())
# }
#
type: "WordType"
type: "Array"
applyDelete: ()->
o = @end
@@ -133,51 +104,144 @@ module.exports = (HB)->
cleanup: ()->
super()
toJson: (transform_to_value = false)->
val = @val()
for i, o in val
if o instanceof types.Object
o.toJson(transform_to_value)
else if o instanceof types.Array
o.toJson(transform_to_value)
else if transform_to_value and o instanceof types.Operation
o.val()
else
o
val: (pos)->
if pos?
o = @getOperationByPosition(pos+1)
if not (o instanceof types.Delimiter)
o.val()
else
throw new Error "this position does not exist"
else
o = @beginning.next_cl
result = []
while o isnt @end
result.push o.val()
o = o.next_cl
result
push: (content)->
@insertAfter @end.prev_cl, content
insertAfter: (left, content)->
insertAfter: (left, content, options)->
createContent = (content, options)->
if content? and content.constructor?
type = types[content.constructor.name]
if type? and type.create?
type.create content, options
else
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Yatta."
else
content
right = left.next_cl
while right.isDeleted()
right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
left = right.prev_cl
if content.type?
(new TextInsert content, undefined, left, right).execute()
if content instanceof types.Operation
(new types.TextInsert content, undefined, left, right).execute()
else
for c in content
tmp = (new TextInsert c, undefined, left, right).execute()
tmp = (new types.TextInsert createContent(c, options), undefined, left, right).execute()
left = tmp
@
#
# Inserts a string into the word.
#
# @return {WordType} This WordType object.
# @return {Array Type} This String object.
#
insertText: (position, content)->
insert: (position, content, options)->
ith = @getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@insertAfter ith, content
@insertAfter ith, [content], options
#
# Deletes a part of the word.
#
# @return {WordType} This WordType object
# @return {Array Type} This String object
#
deleteText: (position, length)->
delete: (position, length)->
o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character
delete_ops = []
for i in [0...length]
if o instanceof types.Delimiter
break
d = (new TextDelete undefined, o).execute()
d = (new types.Delete undefined, o).execute()
o = o.next_cl
while not (o instanceof types.Delimiter) and o.isDeleted()
o = o.next_cl
delete_ops.push d._encode()
@
#
# @private
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json = {
'type': @type
'uid' : @getUid()
}
json
types.Array.parse = (json)->
{
'uid' : uid
} = json
new this(uid)
types.Array.create = (content, mutable)->
if (mutable is "mutable")
list = new types.Array().execute()
ith = list.getOperationByPosition 0
list.insertAfter ith, content
list
else if (not mutable?) or (mutable is "immutable")
content
else
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
#
# Handles a String-like data structures with support for insert/delete at a word-position.
# @note Currently, only Text is supported!
#
class types.String extends types.Array
#
# @private
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (uid)->
@textfields = []
super uid
#
# Identifies this class.
# Use it to check whether this is a word-type or something else.
#
# @example
# var x = yatta.val('unknown')
# if (x.type === "String") {
# console.log JSON.stringify(x.toJson())
# }
#
type: "String"
#
# Get the String-representation of this word.
# @return {String} The String-representation of this object.
@@ -191,55 +255,129 @@ module.exports = (HB)->
c.join('')
#
# Same as WordType.val
# @see WordType.val
# Same as String.val
# @see String.val
#
toString: ()->
@val()
#
# Bind this WordType to a textfield or input field.
# Inserts a string into the word.
#
# @return {Array Type} This String object.
#
insert: (position, content, options)->
ith = @getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@insertAfter ith, content, options
#
# Bind this String to a textfield or input field.
#
# @example
# var textbox = document.getElementById("textfield");
# yatta.bind(textbox);
#
bind: (textfield)->
bind: (textfield, dom_root)->
dom_root ?= window;
# don't duplicate!
for t in @textfields
if t is textfield
return
creator_token = false;
word = @
textfield.value = @val()
@textfields.push textfield
if textfield.selectionStart? and textfield.setSelectionRange?
createRange = (fix)->
left = textfield.selectionStart
right = textfield.selectionEnd
if fix?
left = fix left
right = fix right
{
left: left
right: right
}
writeRange = (range)->
writeContent word.val()
textfield.setSelectionRange range.left, range.right
writeContent = (content)->
textfield.value = content
else
createRange = (fix)->
s = dom_root.getSelection()
clength = textfield.textContent.length
left = Math.min s.anchorOffset, clength
right = Math.min s.focusOffset, clength
if fix?
left = fix left
right = fix right
{
left: left
right: right
isReal: true
}
writeRange = (range)->
writeContent word.val()
textnode = textfield.childNodes[0]
if range.isReal and textnode?
if range.left < 0
range.left = 0
range.right = Math.max range.left, range.right
if range.right > textnode.length
range.right = textnode.length
range.left = Math.min range.left, range.right
r = new Range()
r.setStart(textnode, range.left)
r.setEnd(textnode, range.right)
s = window.getSelection()
s.removeAllRanges()
s.addRange(r)
writeContent = (content)->
append = ""
if content[content.length - 1] is " "
content = content.slice(0,content.length-1)
append = '&nbsp;'
textfield.textContent = content
textfield.innerHTML += append
writeContent this.val()
@observe (events)->
for event in events
if event.type is "insert"
o_pos = event.position
fix = (cursor)->
if cursor <= o_pos
cursor
else
cursor += 1
cursor
left = fix textfield.selectionStart
right = fix textfield.selectionEnd
if not creator_token
if event.type is "insert"
o_pos = event.position
fix = (cursor)->
if cursor <= o_pos
cursor
else
cursor += 1
cursor
r = createRange fix
writeRange r
textfield.value = word.val()
textfield.setSelectionRange left, right
else if event.type is "delete"
o_pos = event.position
fix = (cursor)->
if cursor < o_pos
cursor
else
cursor -= 1
cursor
left = fix textfield.selectionStart
right = fix textfield.selectionEnd
textfield.value = word.val()
textfield.setSelectionRange left, right
else if event.type is "delete"
o_pos = event.position
fix = (cursor)->
if cursor < o_pos
cursor
else
cursor -= 1
cursor
r = createRange fix
writeRange r
# consume all text-insert changes.
textfield.onkeypress = (event)->
creator_token = true
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.onkeypress = null
@@ -253,17 +391,20 @@ module.exports = (HB)->
else
char = event.key
else
char = String.fromCharCode event.keyCode
char = window.String.fromCharCode event.keyCode
if char.length > 0
pos = Math.min textfield.selectionStart, textfield.selectionEnd
diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
word.deleteText (pos), diff
word.insertText pos, char
new_pos = pos + char.length
textfield.setSelectionRange new_pos, new_pos
r = createRange()
pos = Math.min r.left, r.right
diff = Math.abs(r.right - r.left)
word.delete pos, diff
word.insert pos, char
r.left = pos + char.length
r.right = r.left
writeRange r
event.preventDefault()
else
event.preventDefault()
creator_token = false
textfield.onpaste = (event)->
if word.is_deleted
@@ -286,19 +427,26 @@ module.exports = (HB)->
# Every browser supports keyCode. Let's stick with it for now..
#
textfield.onkeydown = (event)->
creator_token = true
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.onkeydown = null
return true
pos = Math.min textfield.selectionStart, textfield.selectionEnd
diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
r = createRange()
pos = Math.min r.left, r.right
diff = Math.abs(r.left - r.right)
if event.keyCode? and event.keyCode is 8 # Backspace
if diff > 0
word.deleteText pos, diff
textfield.setSelectionRange pos, pos
word.delete pos, diff
r.left = pos
r.right = pos
writeRange r
else
if event.ctrlKey? and event.ctrlKey
val = textfield.value
if textfield.value?
val = textfield.value
else
val = textfield.textContent
new_pos = pos
del_length = 0
if pos > 0
@@ -307,21 +455,30 @@ module.exports = (HB)->
while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n'
new_pos--
del_length++
word.deleteText new_pos, (pos-new_pos)
textfield.setSelectionRange new_pos, new_pos
word.delete new_pos, (pos-new_pos)
r.left = new_pos
r.right = new_pos
writeRange r
else
word.deleteText (pos-1), 1
if pos > 0
word.delete (pos-1), 1
r.left = pos-1
r.right = pos-1
writeRange r
event.preventDefault()
else if event.keyCode? and event.keyCode is 46 # Delete
if diff > 0
word.deleteText pos, diff
textfield.setSelectionRange pos, pos
word.delete pos, diff
r.left = pos
r.right = pos
writeRange r
else
word.deleteText pos, 1
textfield.setSelectionRange pos, pos
event.preventDefault()
word.delete pos, 1
r.left = pos
r.right = pos
writeRange r
creator_token = false
true
#
# @private
@@ -329,33 +486,28 @@ module.exports = (HB)->
#
_encode: ()->
json = {
'type': "WordType"
'type': @type
'uid' : @getUid()
'beginning' : @beginning.getUid()
'end' : @end.getUid()
}
if @prev_cl?
json['prev'] = @prev_cl.getUid()
if @next_cl?
json['next'] = @next_cl.getUid()
if @origin? # and @origin isnt @prev_cl
json["origin"] = @origin().getUid()
json
parser['WordType'] = (json)->
types.String.parse = (json)->
{
'uid' : uid
'beginning' : beginning
'end' : end
'prev': prev
'next': next
'origin' : origin
} = json
new WordType uid, beginning, end, prev, next, origin
new this(uid)
types.String.create = (content, mutable)->
if (mutable is "mutable")
word = new types.String().execute()
word.insert 0, content
word
else if (not mutable?) or (mutable is "immutable")
content
else
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
types['TextInsert'] = TextInsert
types['TextDelete'] = TextDelete
types['WordType'] = WordType
structured_types

View File

@@ -25,7 +25,7 @@ Polymer "yatta-element",
bindToChildren @
valChanged: ()->
if @val? and @val.type is "JsonType"
if @val? and @val.type is "Object"
bindToChildren @
connectorChanged: ()->
@@ -42,7 +42,7 @@ Polymer "yatta-property",
# since it is more safe (consider someone putting a custom Object type here)
else if typeof @val is "string"
@parentElement.val(@name,@val)
if @val.type is "JsonType"
if @val.type is "Object"
bindToChildren @
valChanged: ()->
@@ -51,7 +51,7 @@ Polymer "yatta-property",
@val = @parentElement.val.val(@name,@val).val(@name)
# TODO: please use instanceof instead of .type,
# since it is more safe (consider someone putting a custom Object type here)
else if @val.type is "JsonType"
else if @val.type is "Object"
bindToChildren @
else if @parentElement.val?.val? and @val isnt @parentElement.val.val(@name)
@parentElement.val.val @name, @val

View File

@@ -24,7 +24,7 @@ createYatta = (connector)->
# * Integer
# * Array
#
class Yatta extends types.JsonType
class Yatta extends types.Object
#
# @param {String} user_id Unique id of the peer.
@@ -34,7 +34,7 @@ createYatta = (connector)->
@connector = connector
@HB = HB
@types = types
@engine = new Engine @HB, type_manager.parser
@engine = new Engine @HB, type_manager.types
adaptConnector @connector, @engine, @HB, type_manager.execution_listener
super

View File

@@ -25,17 +25,25 @@ class JsonTest extends Test
if _.random(0,1) is 1 # take root
root
else # take child
properties =
for oname,val of root.val()
oname
properties.filter (oname)->
root[oname] instanceof types.Operation
if properties.length is 0
elems = null
if root.type is "Object"
elems =
for oname,val of root.val()
val
else if root.type is "Array"
elems = root.val()
else
return root
elems = elems.filter (elem)->
(elem.type is "Array") or (elem.type is "Object")
if elems.length is 0
root
else
p = root[properties[_.random(0, properties.length-1)]]
p = elems[_.random(0, elems.length-1)]
@getRandomRoot user_num, p
getContent: (user_num)->
@users[user_num].toJson(true)
@@ -43,21 +51,39 @@ class JsonTest extends Test
types = @users[user_num].types
super(user_num).concat [
f : (y)=> # SET PROPERTY
y.val(@getRandomKey(), @getRandomText(), 'immutable')
l = y.val().length
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
null
types : [types.JsonType]
,
f : (y)=> # SET Object Property 1)
y.val(@getRandomObject())
types: [types.JsonType]
,
f : (y)=> # SET Object Property 2)
types : [types.Array]
, f : (y)=> # Delete Array Element
list = y.val()
if list.length > 0
key = list[_random(0,list.length-1)]
y.delete(key)
types: [types.Array]
, f : (y)=> # insert TEXT mutable
l = y.val().length
y.val(_.random(0, l-1), @getRamdomObject())
types: [types.Array]
, f : (y)=> # insert string
l = y.val().length
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
null
types : [types.Array]
, f : (y)=> # Delete Object Property
list = for name, o of y.val()
name
if list.length > 0
key = list[_random(0,list.length-1)]
y.delete(key)
types: [types.Object]
, f : (y)=> # SET Object Property
y.val(@getRandomKey(), @getRandomObject())
types: [types.JsonType]
types: [types.Object]
,
f : (y)=> # SET PROPERTY TEXT
y.val(@getRandomKey(), @getRandomText(), 'mutable')
types: [types.JsonType]
types: [types.Object]
]
describe "JsonFramework", ->
@@ -73,46 +99,6 @@ describe "JsonFramework", ->
console.log "" # TODO
@yTest.run()
### TODO
it "has a update listener", ()->
addName = false
change = false
change2 = 0
@test_user.on 'add', (eventname, property_name)->
if property_name is 'x'
addName = true
@test_user.val('x',5)
@test_user.on 'change', (eventname, property_name)->
if property_name is 'x'
change = true
@test_user.val('x', 6)
@test_user.val('ins', "text", 'mutable')
@test_user.on 'update', (eventname, property_name)->
if property_name is 'ins'
change2++
@test_user.val('ins').insertText 4, " yay"
@test_user.val('ins').deleteText 0, 4
expect(addName).to.be.ok
expect(change).to.be.ok
expect(change2).to.equal 8
###
it "has a JsonTypeWrapper", ->
y = this.yTest.getSomeUser()
y.val('x',"dtrn", 'immutable')
y.val('set',{x:"x"}, 'immutable')
w = y.value
w.x
w.set = {y:""}
w.x
w.set
w.set.x
expect(w.x).to.equal("dtrn")
expect(w.set.x).to.equal("x")
y.value.x = {q:4}
expect(y.value.x.q).to.equal(4)
it "has a working test suite", ->
@yTest.compareAll()
@@ -131,25 +117,35 @@ describe "JsonFramework", ->
expect(test.getContent(0)).to.deep.equal(@yTest.getContent(1))
it "can handle creaton of complex json (1)", ->
@yTest.users[0].val('a', 'q')
@yTest.users[2].val('a', 't')
@yTest.users[0].val('a', 'q', "mutable")
@yTest.users[1].val('a', 't', "mutable")
@yTest.compareAll()
q = @yTest.users[1].val('a')
q.insertText(0,'A')
q = @yTest.users[2].val('a')
q.insert(0,'A')
@yTest.compareAll()
expect(@yTest.getSomeUser().value.a.val()).to.equal("At")
expect(@yTest.getSomeUser().val("a").val()).to.equal("At")
it "can handle creaton of complex json (2)", ->
@yTest.getSomeUser().val('x', {'a':'b'})
@yTest.getSomeUser().val('a', {'a':{q:"dtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt"}})
@yTest.getSomeUser().val('a', {'a':{q:"dtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt"}}, "mutable")
@yTest.getSomeUser().val('b', {'a':{}})
@yTest.getSomeUser().val('c', {'a':'c'})
@yTest.getSomeUser().val('c', {'a':'b'})
@yTest.compareAll()
q = @yTest.getSomeUser().value.a.a.q
q.insertText(0,'A')
q = @yTest.getSomeUser().val("a").val("a").val("q")
q.insert(0,'A')
@yTest.compareAll()
expect(@yTest.getSomeUser().val("a").val("a").val("q").val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
it "can handle creaton of complex json (3)", ->
@yTest.users[0].val('l', [1,2,3], "mutable")
@yTest.users[1].val('l', [4,5,6], "mutable")
@yTest.compareAll()
@yTest.users[2].val('l').insert(0,'A')
w = @yTest.users[1].val('l').insert(0,'B', "mutable").val(0)
w.insert 1, "C"
expect(w.val()).to.equal("BC")
@yTest.compareAll()
expect(@yTest.getSomeUser().value.a.a.q.val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
it "handles immutables and primitive data types", ->
@yTest.getSomeUser().val('string', "text", "immutable")
@@ -162,6 +158,16 @@ describe "JsonFramework", ->
expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
expect(@yTest.getSomeUser().val('null') is null).to.be.ok
it "handles immutables and primitive data types (2)", ->
@yTest.users[0].val('string', "text", "immutable")
@yTest.users[1].val('number', 4, "immutable")
@yTest.users[2].val('object', {q:"rr"}, "immutable")
@yTest.users[0].val('null', null)
@yTest.compareAll()
expect(@yTest.getSomeUser().val('string')).to.equal "text"
expect(@yTest.getSomeUser().val('number')).to.equal 4
expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
expect(@yTest.getSomeUser().val('null') is null).to.be.ok
it "Observers work on JSON Types (add type observers, local and foreign)", ->
u = @yTest.users[0]
@@ -176,7 +182,7 @@ describe "JsonFramework", ->
expect(change.name).to.equal("newStuff")
last_task = "observer1"
u.observe observer1
u.val("newStuff","someStuff")
u.val("newStuff","someStuff","mutable")
expect(last_task).to.equal("observer1")
u.unobserve observer1
@@ -196,7 +202,7 @@ describe "JsonFramework", ->
u.unobserve observer2
it "Observers work on JSON Types (update type observers, local and foreign)", ->
u = @yTest.users[0].val("newStuff","oldStuff").val("moreStuff","moreOldStuff")
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
@@ -231,7 +237,7 @@ describe "JsonFramework", ->
it "Observers work on JSON Types (delete type observers, local and foreign)", ->
u = @yTest.users[0].val("newStuff","oldStuff").val("moreStuff","moreOldStuff")
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
@yTest.flushAll()
last_task = null
observer1 = (changes)->

View File

@@ -12,9 +12,9 @@ Connector = require "../bower_components/connector/lib/test-connector/test-conne
module.exports = class Test
constructor: (@name_suffix = "")->
@number_of_test_cases_multiplier = 1
@repeat_this = 1 * @number_of_test_cases_multiplier
@doSomething_amount = 50 * @number_of_test_cases_multiplier
@number_of_engines = 4 + @number_of_test_cases_multiplier - 1
@repeat_this = 3 * @number_of_test_cases_multiplier
@doSomething_amount = 123 * @number_of_test_cases_multiplier
@number_of_engines = 5 + @number_of_test_cases_multiplier - 1
@time = 0 # denotes to the time when run was started
@ops = 0 # number of operations (used with @time)
@@ -31,6 +31,7 @@ module.exports = class Test
for user in @users
u.getConnector().join(user.getConnector()) # TODO: change the test-connector to make this more convenient
@users.push u
@initUsers?(@users[0])
@flushAll()
# is called by implementing class
@@ -74,17 +75,17 @@ module.exports = class Test
f : (y)=> # INSERT TEXT
y
pos = _.random 0, (y.val().length-1)
y.insertText pos, @getRandomText()
y.insert pos, @getRandomText()
null
types: [types.WordType]
types: [types.String]
,
f : (y)-> # DELETE TEXT
if y.val().length > 0
pos = _.random 0, (y.val().length-1)
pos = _.random 0, (y.val().length-1) # TODO: put here also arbitrary number (test behaviour in error cases)
length = _.random 0, (y.val().length - pos)
ops1 = y.deleteText pos, length
ops1 = y.delete pos, length
undefined
types : [types.WordType]
types : [types.String]
]
getRandomRoot: (user_num)->
throw new Error "overwrite me!"
@@ -99,7 +100,8 @@ module.exports = class Test
y instanceof type
if choices.length is 0
throw new Error "You forgot to specify a test generation methot for this Operation!"
console.dir(y)
throw new Error "You forgot to specify a test generation methot for this Operation! (#{y.type})"
i = _.random 0, (choices.length-1)
choices[i].f y

View File

@@ -17,9 +17,10 @@ class TextTest extends Test
makeNewUser: (userId)->
conn = new Connector userId
y = new Yatta conn
y.val("TextTest","","mutable")
y
new Yatta conn
initUsers: (u)->
u.val("TextTest","","mutable")
getRandomRoot: (user_num)->
@users[user_num].val("TextTest")
@@ -31,22 +32,20 @@ describe "TextFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new TextTest()
@users = @yTest.users
test_user_connector = new Connector 'test_user'
@test_user = @yTest.makeNewUser 'test_user', test_user_connector
test_user_connector.join @users[0].connector
done()
it "simple multi-char insert", ->
u = @yTest.users[0].val("TextTest")
u.insertText 0, "abc"
u.insert 0, "abc"
u = @yTest.users[1].val("TextTest")
u.insertText 0, "xyz"
u.insert 0, "xyz"
@yTest.compareAll()
expect(u.val()).to.equal("abcxyz")
u.delete 0, 1
@yTest.compareAll()
expect(u.val()).to.equal("bcxyz")
it "Observers work on shared Text (insert type observers, local and foreign)", ->
u = @yTest.users[0].val("TextTest","my awesome Text").val("TextTest")
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
@@ -59,7 +58,7 @@ describe "TextFramework", ->
expect(change.changedBy).to.equal('0')
last_task = "observer1"
u.observe observer1
u.insertText 1, "a"
u.insert 1, "a"
expect(last_task).to.equal("observer1")
u.unobserve observer1
@@ -74,13 +73,13 @@ describe "TextFramework", ->
last_task = "observer2"
u.observe observer2
v = @yTest.users[1].val("TextTest")
v.insertText 0, "x"
v.insert 0, "x"
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "Observers work on shared Text (delete type observers, local and foreign)", ->
u = @yTest.users[0].val("TextTest","my awesome Text").val("TextTest")
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
@@ -93,7 +92,7 @@ describe "TextFramework", ->
expect(change.changedBy).to.equal('0')
last_task = "observer1"
u.observe observer1
u.deleteText 1, 1
u.delete 1, 1
expect(last_task).to.equal("observer1")
u.unobserve observer1
@@ -108,7 +107,7 @@ describe "TextFramework", ->
last_task = "observer2"
u.observe observer2
v = @yTest.users[1].val("TextTest")
v.deleteText 0, 1
v.delete 0, 1
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long