Merge pull request #18 from y-js/0.5

Added support for custom connectors. Not compatible with 0.4!!
This commit is contained in:
Kevin Jahns 2015-04-30 12:42:38 +02:00
commit 0314a1b709
44 changed files with 112654 additions and 39022 deletions

View File

@ -5,4 +5,7 @@ before_install:
node_js:
- "0.12"
- "0.11"
- "0.10"
- "0.10"
branches:
only:
- master

113
README.md
View File

@ -1,40 +1,51 @@
# ![Yatta!](https://dadamonad.github.io/files/layout/yjs.svg)
# ![Yjs](http://y-js.org/files/layout/yjs.svg)
[![Build Status](https://travis-ci.org/rwth-acis/yjs.svg)](https://travis-ci.org/rwth-acis/yjs)
[![Build Status](https://travis-ci.org/y-js/yjs.svg)](https://travis-ci.org/y-js/yjs)
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on arbitrary data types. The framework implements a new OT-like concurrency algorithm and provides similar functionality as [ShareJs] and [OpenCoweb]. Yjs was designed to take away the pain from concurrently editing complex data types like Text, Json, and XML. You can find some applications for this framework [here](https://dadamonad.github.io/yjs/examples/).
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on arbitrary data types. The framework implements a new OT-like concurrency algorithm and provides similar functionality as [ShareJs] and [OpenCoweb]. Yjs was designed to handle concurrent actions on arbitrary complex data types like Text, Json, and XML. You can find some applications for this framework [here](http://y-js.org/examples/).
In the future, we want to enable users to implement their own collaborative types. Currently we provide data types for
* Text
* Json
* XML
You can create your own shared data types easily. Therefore, you can take matters into your own hand by defining the meaning of the shared types and ensure that it is valid, while Yjs ensures data consistency (everyone will eventually end up with the same data).
You can use existing types in your custom data type as well. Learn in [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Types) how to craft your own custom data types. We already provide data types for
| Name | Description
| ---------------------------------------------------- | ---------------------------------------------
y-object | Add, update, and remove properties of an object. Circular references are supported. Included in Yjs
[y-list](https://github.com/y-js/y-list) | A shared linked list implementation. Circular references are supported
[y-selections](https://github.com/y-js/y-selections) | Manages selections on types that use linear structures (e.g. the y-list type). You can select a range of elements and assign meaning to them.
[y-xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects
[y-text](https://github.com/y-js/y-text) | Collaborate on text. You can create a two way binding to textareas, input elements, or HTML elements (e.g. *h1*, or *p*)
[y-richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. You can create a two way binding to several editors
Unlike other frameworks, Yjs supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
We support several communication protocols as so called *Connectors*. You can create your own connector too - as it is described [here](https://dadamonad.github.io/yjs/connector/Howto-create-your-own-Connector.html). Currently, we support the following communication protocols:
* [XMPP-Connector](http://xmpp.org) - Propagates updates in a XMPP multi-user-chat room
* [WebRTC-Connector](http://peerjs.com) - Propagate updates directly with WebRTC
* [IWC-Connector](http://dbis.rwth-aachen.de/cms/projects/the-xmpp-experience#interwidget-communication) - Inter-widget Communication
We support several communication protocols as so called *Connectors*. You can create your own connector too - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors). Currently, we support the following communication protocols:
You can use Yjs client-, and server- side. You can get it as via npm, and bower. We even provide a polymer element for Yjs!
Name | Description
---------------------------------------- | -------------------------------------------------------
[y-xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))
[y-webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC
[y-test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios
The theoretical advantages over similar frameworks are support for
You can use Yjs client-, and server- side. You can get it as via npm, and bower. We even provide polymer elements for Yjs!
The advantages over similar frameworks are support for
* .. P2P message propagation and arbitrary communication protocols
* .. arbitrary complex data types
* .. offline editing: Only relevant changes are propagated on rejoin (unimplemented)
* .. AnyUndo: Undo *any* action that was executed in constant time (unimplemented)
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline.
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline. Every type has a notion on how we define Intention Preservation on it.
## Use it!
You find a tutorial, examples, and documentation on the [website](https://dadamonad.github.io/yjs/).
You can find a tutorial, and examples on the [website](http://y-js.org). Furthermore, the [github wiki](https://github.com/y-js/yjs/wiki) offers more information about how you can use Yjs in your application.
Either clone this git repository, install it with [bower](http://bower.io/), or install it with [npm](https://www.npmjs.org/package/yjs).
### Bower
```
bower install rwth-acis/yjs
bower install y-js/yjs
```
Then you include the libraries directly from the installation folder.
```
@ -51,21 +62,69 @@ And use it like this with *npm*:
Y = require("yjs");
```
## Status
Yjs is still in an early development phase. Don't expect that everything is working fine.
But I would become really motivated if you gave me some feedback :) ([github](https://github.com/rwth-acis/yjs/issues)).
# Y()
In order to create an instance of Y, you need to have a connection object (instance of a Connector). Then, you can create a shared data type like this:
```
var y = new Y(connector);
```
### Current Issues
* The History Buffer should be able to store operations in a database
* Documentation
* Reimplement support for XML as a data type
* Custom data types
# Y.Object
Yjs includes only one type by default - the Y.Object type. It mimics the behaviour of a JSON Object. You can create, update, and remove properies on the Y.Object type. Furthermore, you can observe changes on this type as you can observe changes on Javascript Objects with [Object.observe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe) - an ECMAScript 7 proposal which is likely to become accepted by the committee. Until then, we have our own implementation.
##### Reference
* Create
```
var y = new Y.Object();
```
* Create with existing Object
```
var y = new Y.Object({number: 73});
```
* Every instance of Y is an Y.Object
```
var y = new Y(connector);
```
* .val()
* Retrieve all properties of this type as a JSON Object
* .val(name)
* Retrieve the value of a property
* .val(name, value)
* Set/update a property. Returns `this` Y.Object
* .delete(name)
* Delete a property
* .observe(observer)
* The `observer` is called whenever something on this object changes. Throws *add*, *update*, and *delete* events
* .unobserve(f)
* Delete an observer
# A note on intention preservation
When users create/update/delete the same property concurrently, only one change will prevail. Changes on different properties do not conflict with each other.
# A note on time complexities
* .val()
* O(|properties|)
* .val(name)
* O(1)
* .val(name, value)
* O(1)
* .delete(name)
* O(1)
* Apply a delete operation from another user
* O(1)
* Apply an update operation from another user (set/update a property)
* Yjs does not transform against operations that do not conflict with each other.
* An operation conflicts with another operation if it changes the same property.
* Overall worst case complexety: O(|conflicts|!)
# Status
Yjs is a work in progress. Different versions of the *y-* repositories may not work together. Just drop me a line if you run into troubles.
## Get help
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/rwth-acis/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Please report _any_ issues to the [Github issue page](https://github.com/rwth-acis/yjs/issues)! I try to fix them very soon, if possible.
Please report _any_ issues to the [Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very soon, if possible.
## Contribution
I created this framework during my bachelor thesis at the chair of computer science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since December 2014 I'm working on Yjs as a part of my student worker job at the i5.
@ -77,5 +136,3 @@ Yjs is licensed under the [MIT License](./LICENSE.txt).
[ShareJs]: https://github.com/share/ShareJS
[OpenCoweb]: https://github.com/opencoweb/coweb

View File

@ -27,8 +27,7 @@
"extras",
"test"
],
"dependencies": {
"polymer" : "Polymer/polymer#~0.5.3",
"devDependencies": {
"y-test" : "y-test#~0.4.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,22 +19,22 @@ adaptConnector = function(connector, engine, HB, execution_listener) {
}
execution_listener.push(send_);
encode_state_vector = function(v) {
var value, _results;
_results = [];
var results, value;
results = [];
for (name in v) {
value = v[name];
_results.push({
results.push({
user: name,
state: value
});
}
return _results;
return results;
};
parse_state_vector = function(v) {
var s, state_vector, _i, _len;
var i, len, s, state_vector;
state_vector = {};
for (_i = 0, _len = v.length; _i < _len; _i++) {
s = v[_i];
for (i = 0, len = v.length; i < len; i++) {
s = v[i];
state_vector[s.user] = s.state;
}
return state_vector;

View File

@ -38,7 +38,11 @@ module.exports = {
this.connections = {};
this.current_sync_target = null;
this.sent_hb_to_all_users = false;
return this.is_initialized = true;
this.is_initialized = true;
return this.connections_listeners = [];
},
onUserEvent: function(f) {
return this.connections_listeners.push(f);
},
isRoleMaster: function() {
return this.role === "master";
@ -47,12 +51,12 @@ module.exports = {
return this.role === "slave";
},
findNewSyncTarget: function() {
var c, user, _ref;
var c, ref, user;
this.current_sync_target = null;
if (this.syncMethod === "syncAll") {
_ref = this.connections;
for (user in _ref) {
c = _ref[user];
ref = this.connections;
for (user in ref) {
c = ref[user];
if (!c.is_synced) {
this.performSync(user);
break;
@ -65,25 +69,47 @@ module.exports = {
return null;
},
userLeft: function(user) {
var f, i, len, ref, results;
delete this.connections[user];
return this.findNewSyncTarget();
this.findNewSyncTarget();
ref = this.connections_listeners;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
f = ref[i];
results.push(f({
action: "userLeft",
user: user
}));
}
return results;
},
userJoined: function(user, role) {
var _base;
var base, f, i, len, ref, results;
if (role == null) {
throw new Error("Internal: You must specify the role of the joined user! E.g. userJoined('uid:3939','slave')");
}
if ((_base = this.connections)[user] == null) {
_base[user] = {};
if ((base = this.connections)[user] == null) {
base[user] = {};
}
this.connections[user].is_synced = false;
if ((!this.is_synced) || this.syncMethod === "syncAll") {
if (this.syncMethod === "syncAll") {
return this.performSync(user);
this.performSync(user);
} else if (role === "master") {
return this.performSyncWithMaster(user);
this.performSyncWithMaster(user);
}
}
ref = this.connections_listeners;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
f = ref[i];
results.push(f({
action: "userJoined",
user: user,
role: role
}));
}
return results;
},
whenSynced: function(args) {
if (args.constructore === Function) {
@ -116,7 +142,7 @@ module.exports = {
throw new Error "You must implement send!"
*/
performSync: function(user) {
var hb, o, _hb, _i, _len;
var _hb, hb, i, len, o;
if (this.current_sync_target == null) {
this.current_sync_target = user;
this.send(user, {
@ -128,10 +154,10 @@ module.exports = {
this.sent_hb_to_all_users = true;
hb = this.getHB([]).hb;
_hb = [];
for (_i = 0, _len = hb.length; _i < _len; _i++) {
o = hb[_i];
for (i = 0, len = hb.length; i < len; i++) {
o = hb[i];
_hb.push(o);
if (_hb.length > 30) {
if (_hb.length > 10) {
this.broadcast({
sync_step: "applyHB_",
data: _hb
@ -147,7 +173,7 @@ module.exports = {
}
},
performSyncWithMaster: function(user) {
var hb, o, _hb, _i, _len;
var _hb, hb, i, len, o;
this.current_sync_target = user;
this.send(user, {
sync_step: "getHB",
@ -156,10 +182,10 @@ module.exports = {
});
hb = this.getHB([]).hb;
_hb = [];
for (_i = 0, _len = hb.length; _i < _len; _i++) {
o = hb[_i];
for (i = 0, len = hb.length; i < len; i++) {
o = hb[i];
_hb.push(o);
if (_hb.length > 30) {
if (_hb.length > 10) {
this.broadcast({
sync_step: "applyHB_",
data: _hb
@ -173,13 +199,13 @@ module.exports = {
});
},
setStateSynced: function() {
var f, _i, _len, _ref;
var f, i, len, ref;
if (!this.is_synced) {
this.is_synced = true;
if (this.compute_when_synced != null) {
_ref = this.compute_when_synced;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
f = _ref[_i];
ref = this.compute_when_synced;
for (i = 0, len = ref.length; i < len; i++) {
f = ref[i];
f();
}
delete this.compute_when_synced;
@ -188,15 +214,15 @@ module.exports = {
}
},
receiveMessage: function(sender, res) {
var data, f, hb, o, sendApplyHB, send_again, _hb, _i, _j, _len, _len1, _ref, _results;
var _hb, data, f, hb, i, j, len, len1, o, ref, results, sendApplyHB, send_again;
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));
ref = this.receive_handlers;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
f = ref[i];
results.push(f(sender, res));
}
return _results;
return results;
} else {
if (sender === this.user_id) {
return;
@ -218,10 +244,10 @@ module.exports = {
};
})(this);
}
for (_j = 0, _len1 = hb.length; _j < _len1; _j++) {
o = hb[_j];
for (j = 0, len1 = hb.length; j < len1; j++) {
o = hb[j];
_hb.push(o);
if (_hb.length > 30) {
if (_hb.length > 10) {
sendApplyHB({
sync_step: "applyHB_",
data: _hb
@ -262,25 +288,25 @@ module.exports = {
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];
var i, len, n, 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));
results.push(parse_array(n));
} else {
_results.push(parse_object(n));
results.push(parse_object(n));
}
}
return _results;
return results;
};
parse_object = function(node) {
var int, json, n, name, value, _i, _len, _ref, _ref1;
var i, int, json, len, n, name, ref, ref1, value;
json = {};
_ref = node.attrs;
for (name in _ref) {
value = _ref[name];
ref = node.attrs;
for (name in ref) {
value = ref[name];
int = parseInt(value);
if (isNaN(int) || ("" + int) !== value) {
json[name] = value;
@ -288,9 +314,9 @@ module.exports = {
json[name] = int;
}
}
_ref1 = node.children;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
n = _ref1[_i];
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);
@ -321,10 +347,10 @@ module.exports = {
return m;
};
encode_array = function(m, array) {
var e, _i, _len;
var e, i, len;
m.setAttribute("isArray", "true");
for (_i = 0, _len = array.length; _i < _len; _i++) {
e = array[_i];
for (i = 0, len = array.length; i < len; i++) {
e = array[i];
if (e.constructor === Object) {
encode_object(m.c("array-element"), e);
} else {

View File

@ -13,9 +13,9 @@ if (typeof window !== "undefined" && window !== null) {
}
Engine = (function() {
function Engine(_at_HB, _at_types) {
this.HB = _at_HB;
this.types = _at_types;
function Engine(HB, types) {
this.HB = HB;
this.types = types;
this.unprocessed_ops = [];
}
@ -42,17 +42,17 @@ Engine = (function() {
*/
Engine.prototype.applyOpsCheckDouble = function(ops_json) {
var o, _i, _len, _results;
_results = [];
for (_i = 0, _len = ops_json.length; _i < _len; _i++) {
o = ops_json[_i];
var i, len, o, results;
results = [];
for (i = 0, len = ops_json.length; i < len; i++) {
o = ops_json[i];
if (this.HB.getOperation(o.uid) == null) {
_results.push(this.applyOp(o));
results.push(this.applyOp(o));
} else {
_results.push(void 0);
results.push(void 0);
}
}
return _results;
return results;
};
Engine.prototype.applyOps = function(ops_json) {
@ -60,15 +60,15 @@ Engine = (function() {
};
Engine.prototype.applyOp = function(op_json_array, fromHB) {
var o, op_json, _i, _len;
var i, len, o, op_json;
if (fromHB == null) {
fromHB = false;
}
if (op_json_array.constructor !== Array) {
op_json_array = [op_json_array];
}
for (_i = 0, _len = op_json_array.length; _i < _len; _i++) {
op_json = op_json_array[_i];
for (i = 0, len = op_json_array.length; i < len; i++) {
op_json = op_json_array[i];
if (fromHB) {
op_json.fromHB = "true";
}
@ -90,13 +90,13 @@ Engine = (function() {
};
Engine.prototype.tryUnprocessed = function() {
var old_length, op, unprocessed, _i, _len, _ref;
var i, len, old_length, op, ref, unprocessed;
while (true) {
old_length = this.unprocessed_ops.length;
unprocessed = [];
_ref = this.unprocessed_ops;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
op = _ref[_i];
ref = this.unprocessed_ops;
for (i = 0, len = ref.length; i < len; i++) {
op = ref[i];
if (this.HB.getOperation(op) != null) {
} else if ((!this.HB.isExpectedOperation(op) && (op.fromHB == null)) || (!op.execute())) {

View File

@ -1,10 +1,10 @@
var HistoryBuffer,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
HistoryBuffer = (function() {
function HistoryBuffer(_at_user_id) {
this.user_id = _at_user_id;
this.emptyGarbage = __bind(this.emptyGarbage, this);
function HistoryBuffer(user_id1) {
this.user_id = user_id1;
this.emptyGarbage = bind(this.emptyGarbage, this);
this.operation_counter = {};
this.buffer = {};
this.change_listeners = [];
@ -43,10 +43,10 @@ HistoryBuffer = (function() {
};
HistoryBuffer.prototype.emptyGarbage = function() {
var o, _i, _len, _ref;
_ref = this.garbage;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
o = _ref[_i];
var i, len, o, ref;
ref = this.garbage;
for (i = 0, len = ref.length; i < len; i++) {
o = ref[i];
if (typeof o.cleanup === "function") {
o.cleanup();
}
@ -64,18 +64,18 @@ HistoryBuffer = (function() {
};
HistoryBuffer.prototype.addToGarbageCollector = function() {
var o, _i, _len, _results;
var i, len, o, results;
if (this.performGarbageCollection) {
_results = [];
for (_i = 0, _len = arguments.length; _i < _len; _i++) {
o = arguments[_i];
results = [];
for (i = 0, len = arguments.length; i < len; i++) {
o = arguments[i];
if (o != null) {
_results.push(this.garbage.push(o));
results.push(this.garbage.push(o));
} else {
_results.push(void 0);
results.push(void 0);
}
}
return _results;
return results;
}
};
@ -92,8 +92,8 @@ HistoryBuffer = (function() {
return this.garbageCollectTimeoutId = void 0;
};
HistoryBuffer.prototype.setGarbageCollectTimeout = function(_at_garbageCollectTimeout) {
this.garbageCollectTimeout = _at_garbageCollectTimeout;
HistoryBuffer.prototype.setGarbageCollectTimeout = function(garbageCollectTimeout) {
this.garbageCollectTimeout = garbageCollectTimeout;
};
HistoryBuffer.prototype.getReservedUniqueIdentifier = function() {
@ -104,12 +104,12 @@ HistoryBuffer = (function() {
};
HistoryBuffer.prototype.getOperationCounter = function(user_id) {
var ctn, res, user, _ref;
var ctn, ref, res, user;
if (user_id == null) {
res = {};
_ref = this.operation_counter;
for (user in _ref) {
ctn = _ref[user];
ref = this.operation_counter;
for (user in ref) {
ctn = ref[user];
res[user] = ctn;
}
return res;
@ -119,16 +119,16 @@ HistoryBuffer = (function() {
};
HistoryBuffer.prototype.isExpectedOperation = function(o) {
var _base, _name;
if ((_base = this.operation_counter)[_name = o.uid.creator] == null) {
_base[_name] = 0;
var base, name;
if ((base = this.operation_counter)[name = o.uid.creator] == null) {
base[name] = 0;
}
o.uid.op_number <= this.operation_counter[o.uid.creator];
return true;
};
HistoryBuffer.prototype._encode = function(state_vector) {
var json, o, o_json, o_next, o_number, o_prev, u_name, unknown, user, _ref;
var json, o, o_json, o_next, o_number, o_prev, ref, u_name, unknown, user;
if (state_vector == null) {
state_vector = {};
}
@ -139,9 +139,9 @@ HistoryBuffer = (function() {
}
return (state_vector[user] == null) || state_vector[user] <= o_number;
};
_ref = this.buffer;
for (u_name in _ref) {
user = _ref[u_name];
ref = this.buffer;
for (u_name in ref) {
user = ref[u_name];
if (u_name === "_") {
continue;
}
@ -186,11 +186,11 @@ HistoryBuffer = (function() {
};
HistoryBuffer.prototype.getOperation = function(uid) {
var o, _ref;
var o, ref;
if (uid.uid != null) {
uid = uid.uid;
}
o = (_ref = this.buffer[uid.creator]) != null ? _ref[uid.op_number] : void 0;
o = (ref = this.buffer[uid.creator]) != null ? ref[uid.op_number] : void 0;
if ((uid.sub != null) && (o != null)) {
return o.retrieveSub(uid.sub);
} else {
@ -214,8 +214,8 @@ HistoryBuffer = (function() {
};
HistoryBuffer.prototype.removeOperation = function(o) {
var _ref;
return (_ref = this.buffer[o.uid.creator]) != null ? delete _ref[o.uid.op_number] : void 0;
var ref;
return (ref = this.buffer[o.uid.creator]) != null ? delete ref[o.uid.op_number] : void 0;
};
HistoryBuffer.prototype.setInvokeSyncHandler = function(f) {
@ -225,23 +225,23 @@ HistoryBuffer = (function() {
HistoryBuffer.prototype.invokeSync = function() {};
HistoryBuffer.prototype.renewStateVector = function(state_vector) {
var state, user, _results;
_results = [];
var results, state, user;
results = [];
for (user in state_vector) {
state = 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]);
results.push(this.operation_counter[user] = state_vector[user]);
} else {
_results.push(void 0);
results.push(void 0);
}
}
return _results;
return results;
};
HistoryBuffer.prototype.addToCounter = function(o) {
var _base, _name;
if ((_base = this.operation_counter)[_name = o.uid.creator] == null) {
_base[_name] = 0;
var base, name;
if ((base = this.operation_counter)[name = o.uid.creator] == null) {
base[name] = 0;
}
if (o.uid.creator !== this.getUserId()) {
if (o.uid.op_number === this.operation_counter[o.uid.creator]) {

91
build/node/ObjectType.js Normal file
View File

@ -0,0 +1,91 @@
var YObject;
YObject = (function() {
function YObject(_object) {
var name, ref, val;
this._object = _object != null ? _object : {};
if (this._object.constructor === Object) {
ref = this._object;
for (name in ref) {
val = ref[name];
if (val.constructor === Object) {
this._object[name] = new YObject(val);
}
}
} else {
throw new Error("Y.Object accepts Json Objects only");
}
}
YObject.prototype._name = "Object";
YObject.prototype._getModel = function(types, ops) {
var n, o, ref;
if (this._model == null) {
this._model = new ops.MapManager(this).execute();
ref = this._object;
for (n in ref) {
o = ref[n];
this._model.val(n, o);
}
}
delete this._object;
return this._model;
};
YObject.prototype._setModel = function(_model) {
this._model = _model;
return delete this._object;
};
YObject.prototype.observe = function(f) {
this._model.observe(f);
return this;
};
YObject.prototype.unobserve = function(f) {
this._model.unobserve(f);
return this;
};
YObject.prototype.val = function(name, content) {
var n, ref, res, v;
if (this._model != null) {
return this._model.val.apply(this._model, arguments);
} else {
if (content != null) {
return this._object[name] = content;
} else if (name != null) {
return this._object[name];
} else {
res = {};
ref = this._object;
for (n in ref) {
v = ref[n];
res[n] = v;
}
return res;
}
}
};
YObject.prototype["delete"] = function(name) {
this._model["delete"](name);
return this;
};
return YObject;
})();
if (typeof window !== "undefined" && window !== null) {
if (window.Y != null) {
window.Y.Object = YObject;
} else {
throw new Error("You must first import Y!");
}
}
if (typeof module !== "undefined" && module !== null) {
module.exports = YObject;
}

View File

@ -0,0 +1,670 @@
var slice = [].slice,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
module.exports = function() {
var execution_listener, ops;
ops = {};
execution_listener = [];
ops.Operation = (function() {
function Operation(custom_type, uid, content, content_operations) {
var name, op;
if (custom_type != null) {
this.custom_type = custom_type;
}
this.is_deleted = false;
this.garbage_collected = false;
this.event_listeners = [];
if (uid != null) {
this.uid = uid;
}
if (content === void 0) {
} else if ((content != null) && (content.creator != null)) {
this.saveOperation('content', content);
} else {
this.content = content;
}
if (content_operations != null) {
this.content_operations = {};
for (name in content_operations) {
op = content_operations[name];
this.saveOperation(name, op, 'content_operations');
}
}
}
Operation.prototype.type = "Operation";
Operation.prototype.getContent = function(name) {
var content, n, ref, ref1, v;
if (this.content != null) {
if (this.content.getCustomType != null) {
return this.content.getCustomType();
} else if (this.content.constructor === Object) {
if (name != null) {
if (this.content[name] != null) {
return this.content[name];
} else {
return this.content_operations[name].getCustomType();
}
} else {
content = {};
ref = this.content;
for (n in ref) {
v = ref[n];
content[n] = v;
}
if (this.content_operations != null) {
ref1 = this.content_operations;
for (n in ref1) {
v = ref1[n];
v = v.getCustomType();
content[n] = v;
}
}
return content;
}
} else {
return this.content;
}
} else {
return this.content;
}
};
Operation.prototype.retrieveSub = function() {
throw new Error("sub properties are not enable on this operation type!");
};
Operation.prototype.observe = function(f) {
return this.event_listeners.push(f);
};
Operation.prototype.unobserve = function(f) {
return this.event_listeners = this.event_listeners.filter(function(g) {
return f !== g;
});
};
Operation.prototype.deleteAllObservers = function() {
return this.event_listeners = [];
};
Operation.prototype["delete"] = function() {
(new ops.Delete(void 0, this)).execute();
return null;
};
Operation.prototype.callEvent = function() {
var callon;
if (this.custom_type != null) {
callon = this.getCustomType();
} else {
callon = this;
}
return this.forwardEvent.apply(this, [callon].concat(slice.call(arguments)));
};
Operation.prototype.forwardEvent = function() {
var args, f, j, len, op, ref, results;
op = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
ref = this.event_listeners;
results = [];
for (j = 0, len = ref.length; j < len; j++) {
f = ref[j];
results.push(f.call.apply(f, [op].concat(slice.call(args))));
}
return results;
};
Operation.prototype.isDeleted = function() {
return this.is_deleted;
};
Operation.prototype.applyDelete = function(garbagecollect) {
if (garbagecollect == null) {
garbagecollect = true;
}
if (!this.garbage_collected) {
this.is_deleted = true;
if (garbagecollect) {
this.garbage_collected = true;
return this.HB.addToGarbageCollector(this);
}
}
};
Operation.prototype.cleanup = function() {
this.HB.removeOperation(this);
return this.deleteAllObservers();
};
Operation.prototype.setParent = function(parent1) {
this.parent = parent1;
};
Operation.prototype.getParent = function() {
return this.parent;
};
Operation.prototype.getUid = function() {
var map_uid;
if (this.uid.noOperation == null) {
return this.uid;
} else {
if (this.uid.alt != null) {
map_uid = this.uid.alt.cloneUid();
map_uid.sub = this.uid.sub;
return map_uid;
} else {
return void 0;
}
}
};
Operation.prototype.cloneUid = function() {
var n, ref, uid, v;
uid = {};
ref = this.getUid();
for (n in ref) {
v = ref[n];
uid[n] = v;
}
return uid;
};
Operation.prototype.execute = function() {
var j, l, len;
if (this.validateSavedOperations()) {
this.is_executed = true;
if (this.uid == null) {
this.uid = this.HB.getNextOperationIdentifier();
}
if (this.uid.noOperation == null) {
this.HB.addOperation(this);
for (j = 0, len = execution_listener.length; j < len; j++) {
l = execution_listener[j];
l(this._encode());
}
}
return this;
} else {
return false;
}
};
Operation.prototype.saveOperation = function(name, op, base) {
var base1, dest, j, last_path, len, path, paths;
if (base == null) {
base = "this";
}
if ((op != null) && (op._getModel != null)) {
op = op._getModel(this.custom_types, this.operations);
}
if (op == null) {
} else if ((op.execute != null) || !((op.op_number != null) && (op.creator != null))) {
if (base === "this") {
return this[name] = op;
} else {
dest = this[base];
paths = name.split("/");
last_path = paths.pop();
for (j = 0, len = paths.length; j < len; j++) {
path = paths[j];
dest = dest[path];
}
return dest[last_path] = op;
}
} else {
if (this.unchecked == null) {
this.unchecked = {};
}
if ((base1 = this.unchecked)[base] == null) {
base1[base] = {};
}
return this.unchecked[base][name] = op;
}
};
Operation.prototype.validateSavedOperations = function() {
var base, base_name, dest, j, last_path, len, name, op, op_uid, path, paths, ref, success, uninstantiated;
uninstantiated = {};
success = true;
ref = this.unchecked;
for (base_name in ref) {
base = ref[base_name];
for (name in base) {
op_uid = base[name];
op = this.HB.getOperation(op_uid);
if (op) {
if (base_name === "this") {
this[name] = op;
} else {
dest = this[base_name];
paths = name.split("/");
last_path = paths.pop();
for (j = 0, len = paths.length; j < len; j++) {
path = paths[j];
dest = dest[path];
}
dest[last_path] = op;
}
} else {
if (uninstantiated[base_name] == null) {
uninstantiated[base_name] = {};
}
uninstantiated[base_name][name] = op_uid;
success = false;
}
}
}
if (!success) {
this.unchecked = uninstantiated;
return false;
} else {
delete this.unchecked;
return this;
}
};
Operation.prototype.getCustomType = function() {
var Type, j, len, ref, t;
if (this.custom_type == null) {
return this;
} else {
if (this.custom_type.constructor === String) {
Type = this.custom_types;
ref = this.custom_type.split(".");
for (j = 0, len = ref.length; j < len; j++) {
t = ref[j];
Type = Type[t];
}
this.custom_type = new Type();
this.custom_type._setModel(this);
}
return this.custom_type;
}
};
Operation.prototype._encode = function(json) {
var n, o, operations, ref, ref1;
if (json == null) {
json = {};
}
json.type = this.type;
json.uid = this.getUid();
if (this.custom_type != null) {
if (this.custom_type.constructor === String) {
json.custom_type = this.custom_type;
} else {
json.custom_type = this.custom_type._name;
}
}
if (((ref = this.content) != null ? ref.getUid : void 0) != null) {
json.content = this.content.getUid();
} else {
json.content = this.content;
}
if (this.content_operations != null) {
operations = {};
ref1 = this.content_operations;
for (n in ref1) {
o = ref1[n];
if (o._getModel != null) {
o = o._getModel(this.custom_types, this.operations);
}
operations[n] = o.getUid();
}
json.content_operations = operations;
}
return json;
};
return Operation;
})();
ops.Delete = (function(superClass) {
extend(Delete, superClass);
function Delete(custom_type, uid, deletes) {
this.saveOperation('deletes', deletes);
Delete.__super__.constructor.call(this, custom_type, uid);
}
Delete.prototype.type = "Delete";
Delete.prototype._encode = function() {
return {
'type': "Delete",
'uid': this.getUid(),
'deletes': this.deletes.getUid()
};
};
Delete.prototype.execute = function() {
var res;
if (this.validateSavedOperations()) {
res = Delete.__super__.execute.apply(this, arguments);
if (res) {
this.deletes.applyDelete(this);
}
return res;
} else {
return false;
}
};
return Delete;
})(ops.Operation);
ops.Delete.parse = function(o) {
var deletes_uid, uid;
uid = o['uid'], deletes_uid = o['deletes'];
return new this(null, uid, deletes_uid);
};
ops.Insert = (function(superClass) {
extend(Insert, superClass);
function Insert(custom_type, content, content_operations, parent, uid, prev_cl, next_cl, origin) {
this.saveOperation('parent', parent);
this.saveOperation('prev_cl', prev_cl);
this.saveOperation('next_cl', next_cl);
if (origin != null) {
this.saveOperation('origin', origin);
} else {
this.saveOperation('origin', prev_cl);
}
Insert.__super__.constructor.call(this, custom_type, uid, content, content_operations);
}
Insert.prototype.type = "Insert";
Insert.prototype.val = function() {
return this.getContent();
};
Insert.prototype.getNext = function(i) {
var n;
if (i == null) {
i = 1;
}
n = this;
while (i > 0 && (n.next_cl != null)) {
n = n.next_cl;
if (!n.is_deleted) {
i--;
}
}
if (n.is_deleted) {
null;
}
return n;
};
Insert.prototype.getPrev = function(i) {
var n;
if (i == null) {
i = 1;
}
n = this;
while (i > 0 && (n.prev_cl != null)) {
n = n.prev_cl;
if (!n.is_deleted) {
i--;
}
}
if (n.is_deleted) {
return null;
} else {
return n;
}
};
Insert.prototype.applyDelete = function(o) {
var callLater, garbagecollect;
if (this.deleted_by == null) {
this.deleted_by = [];
}
callLater = false;
if ((this.parent != null) && !this.is_deleted && (o != null)) {
callLater = true;
}
if (o != null) {
this.deleted_by.push(o);
}
garbagecollect = false;
if (this.next_cl.isDeleted()) {
garbagecollect = true;
}
Insert.__super__.applyDelete.call(this, garbagecollect);
if (callLater) {
this.parent.callOperationSpecificDeleteEvents(this, o);
}
if ((this.prev_cl != null) && this.prev_cl.isDeleted()) {
return this.prev_cl.applyDelete();
}
};
Insert.prototype.cleanup = function() {
var d, j, len, o, ref;
if (this.next_cl.isDeleted()) {
ref = this.deleted_by;
for (j = 0, len = ref.length; j < len; j++) {
d = ref[j];
d.cleanup();
}
o = this.next_cl;
while (o.type !== "Delimiter") {
if (o.origin === this) {
o.origin = this.prev_cl;
}
o = o.next_cl;
}
this.prev_cl.next_cl = this.next_cl;
this.next_cl.prev_cl = this.prev_cl;
if (this.content instanceof ops.Operation && !(this.content instanceof ops.Insert)) {
this.content.referenced_by--;
if (this.content.referenced_by <= 0 && !this.content.is_deleted) {
this.content.applyDelete();
}
}
delete this.content;
return Insert.__super__.cleanup.apply(this, arguments);
}
};
Insert.prototype.getDistanceToOrigin = function() {
var d, o;
d = 0;
o = this.prev_cl;
while (true) {
if (this.origin === o) {
break;
}
d++;
o = o.prev_cl;
}
return d;
};
Insert.prototype.execute = function() {
var base1, distance_to_origin, i, o;
if (!this.validateSavedOperations()) {
return false;
} else {
if (this.content instanceof ops.Operation) {
this.content.insert_parent = this;
if ((base1 = this.content).referenced_by == null) {
base1.referenced_by = 0;
}
this.content.referenced_by++;
}
if (this.parent != null) {
if (this.prev_cl == null) {
this.prev_cl = this.parent.beginning;
}
if (this.origin == null) {
this.origin = this.prev_cl;
} else if (this.origin === "Delimiter") {
this.origin = this.parent.beginning;
}
if (this.next_cl == null) {
this.next_cl = this.parent.end;
}
}
if (this.prev_cl != null) {
distance_to_origin = this.getDistanceToOrigin();
o = this.prev_cl.next_cl;
i = distance_to_origin;
while (true) {
if (o !== this.next_cl) {
if (o.getDistanceToOrigin() === i) {
if (o.uid.creator < this.uid.creator) {
this.prev_cl = o;
distance_to_origin = i + 1;
} else {
}
} else if (o.getDistanceToOrigin() < i) {
if (i - distance_to_origin <= o.getDistanceToOrigin()) {
this.prev_cl = o;
distance_to_origin = i + 1;
} else {
}
} else {
break;
}
i++;
o = o.next_cl;
} else {
break;
}
}
this.next_cl = this.prev_cl.next_cl;
this.prev_cl.next_cl = this;
this.next_cl.prev_cl = this;
}
this.setParent(this.prev_cl.getParent());
Insert.__super__.execute.apply(this, arguments);
this.parent.callOperationSpecificInsertEvents(this);
return this;
}
};
Insert.prototype.getPosition = function() {
var position, prev;
position = 0;
prev = this.prev_cl;
while (true) {
if (prev instanceof ops.Delimiter) {
break;
}
if (!prev.isDeleted()) {
position++;
}
prev = prev.prev_cl;
}
return position;
};
Insert.prototype._encode = function(json) {
if (json == null) {
json = {};
}
json.prev = this.prev_cl.getUid();
json.next = this.next_cl.getUid();
if (this.origin.type === "Delimiter") {
json.origin = "Delimiter";
} else if (this.origin !== this.prev_cl) {
json.origin = this.origin.getUid();
}
json.parent = this.parent.getUid();
return Insert.__super__._encode.call(this, json);
};
return Insert;
})(ops.Operation);
ops.Insert.parse = function(json) {
var content, content_operations, next, origin, parent, prev, uid;
content = json['content'], content_operations = json['content_operations'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], parent = json['parent'];
return new this(null, content, content_operations, parent, uid, prev, next, origin);
};
ops.Delimiter = (function(superClass) {
extend(Delimiter, superClass);
function Delimiter(prev_cl, next_cl, origin) {
this.saveOperation('prev_cl', prev_cl);
this.saveOperation('next_cl', next_cl);
this.saveOperation('origin', prev_cl);
Delimiter.__super__.constructor.call(this, null, {
noOperation: true
});
}
Delimiter.prototype.type = "Delimiter";
Delimiter.prototype.applyDelete = function() {
var o;
Delimiter.__super__.applyDelete.call(this);
o = this.prev_cl;
while (o != null) {
o.applyDelete();
o = o.prev_cl;
}
return void 0;
};
Delimiter.prototype.cleanup = function() {
return Delimiter.__super__.cleanup.call(this);
};
Delimiter.prototype.execute = function() {
var ref, ref1;
if (((ref = this.unchecked) != null ? ref['next_cl'] : void 0) != null) {
return Delimiter.__super__.execute.apply(this, arguments);
} else if ((ref1 = this.unchecked) != null ? ref1['prev_cl'] : void 0) {
if (this.validateSavedOperations()) {
if (this.prev_cl.next_cl != null) {
throw new Error("Probably duplicated operations");
}
this.prev_cl.next_cl = this;
return Delimiter.__super__.execute.apply(this, arguments);
} else {
return false;
}
} else if ((this.prev_cl != null) && (this.prev_cl.next_cl == null)) {
delete this.prev_cl.unchecked.next_cl;
this.prev_cl.next_cl = this;
return Delimiter.__super__.execute.apply(this, arguments);
} else if ((this.prev_cl != null) || (this.next_cl != null) || true) {
return Delimiter.__super__.execute.apply(this, arguments);
}
};
Delimiter.prototype._encode = function() {
var ref, ref1;
return {
'type': this.type,
'uid': this.getUid(),
'prev': (ref = this.prev_cl) != null ? ref.getUid() : void 0,
'next': (ref1 = this.next_cl) != null ? ref1.getUid() : void 0
};
};
return Delimiter;
})(ops.Operation);
ops.Delimiter.parse = function(json) {
var next, prev, uid;
uid = json['uid'], prev = json['prev'], next = json['next'];
return new this(uid, prev, next);
};
return {
'operations': ops,
'execution_listener': execution_listener
};
};

View File

@ -0,0 +1,571 @@
var basic_ops_uninitialized,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
basic_ops_uninitialized = require("./Basic");
module.exports = function() {
var basic_ops, ops;
basic_ops = basic_ops_uninitialized();
ops = basic_ops.operations;
ops.MapManager = (function(superClass) {
extend(MapManager, superClass);
function MapManager(custom_type, uid, content, content_operations) {
this._map = {};
MapManager.__super__.constructor.call(this, custom_type, uid, content, content_operations);
}
MapManager.prototype.type = "MapManager";
MapManager.prototype.applyDelete = function() {
var name, p, ref;
ref = this._map;
for (name in ref) {
p = ref[name];
p.applyDelete();
}
return MapManager.__super__.applyDelete.call(this);
};
MapManager.prototype.cleanup = function() {
return MapManager.__super__.cleanup.call(this);
};
MapManager.prototype.map = function(f) {
var n, ref, v;
ref = this._map;
for (n in ref) {
v = ref[n];
f(n, v);
}
return void 0;
};
MapManager.prototype.val = function(name, content) {
var o, prop, ref, rep, res, result;
if (arguments.length > 1) {
if ((content != null) && (content._getModel != null)) {
rep = content._getModel(this.custom_types, this.operations);
} else {
rep = content;
}
this.retrieveSub(name).replace(rep);
return this.getCustomType();
} else if (name != null) {
prop = this._map[name];
if ((prop != null) && !prop.isContentDeleted()) {
res = prop.val();
if (res instanceof ops.Operation) {
return res.getCustomType();
} else {
return res;
}
} else {
return void 0;
}
} else {
result = {};
ref = this._map;
for (name in ref) {
o = ref[name];
if (!o.isContentDeleted()) {
result[name] = o.val();
}
}
return result;
}
};
MapManager.prototype["delete"] = function(name) {
var ref;
if ((ref = this._map[name]) != null) {
ref.deleteContent();
}
return this;
};
MapManager.prototype.retrieveSub = function(property_name) {
var event_properties, event_this, rm, rm_uid;
if (this._map[property_name] == null) {
event_properties = {
name: property_name
};
event_this = this;
rm_uid = {
noOperation: true,
sub: property_name,
alt: this
};
rm = new ops.ReplaceManager(null, event_properties, event_this, rm_uid);
this._map[property_name] = rm;
rm.setParent(this, property_name);
rm.execute();
}
return this._map[property_name];
};
return MapManager;
})(ops.Operation);
ops.MapManager.parse = function(json) {
var content, content_operations, custom_type, uid;
uid = json['uid'], custom_type = json['custom_type'], content = json['content'], content_operations = json['content_operations'];
return new this(custom_type, uid, content, content_operations);
};
ops.ListManager = (function(superClass) {
extend(ListManager, superClass);
function ListManager(custom_type, uid, content, content_operations) {
this.beginning = new ops.Delimiter(void 0, void 0);
this.end = new ops.Delimiter(this.beginning, void 0);
this.beginning.next_cl = this.end;
this.beginning.execute();
this.end.execute();
ListManager.__super__.constructor.call(this, custom_type, uid, content, content_operations);
}
ListManager.prototype.type = "ListManager";
ListManager.prototype.applyDelete = function() {
var o;
o = this.beginning;
while (o != null) {
o.applyDelete();
o = o.next_cl;
}
return ListManager.__super__.applyDelete.call(this);
};
ListManager.prototype.cleanup = function() {
return ListManager.__super__.cleanup.call(this);
};
ListManager.prototype.toJson = function(transform_to_value) {
var i, j, len, o, results, val;
if (transform_to_value == null) {
transform_to_value = false;
}
val = this.val();
results = [];
for (o = j = 0, len = val.length; j < len; o = ++j) {
i = val[o];
if (o instanceof ops.Object) {
results.push(o.toJson(transform_to_value));
} else if (o instanceof ops.ListManager) {
results.push(o.toJson(transform_to_value));
} else if (transform_to_value && o instanceof ops.Operation) {
results.push(o.val());
} else {
results.push(o);
}
}
return results;
};
ListManager.prototype.execute = function() {
if (this.validateSavedOperations()) {
this.beginning.setParent(this);
this.end.setParent(this);
return ListManager.__super__.execute.apply(this, arguments);
} else {
return false;
}
};
ListManager.prototype.getLastOperation = function() {
return this.end.prev_cl;
};
ListManager.prototype.getFirstOperation = function() {
return this.beginning.next_cl;
};
ListManager.prototype.toArray = function() {
var o, result;
o = this.beginning.next_cl;
result = [];
while (o !== this.end) {
if (!o.is_deleted) {
result.push(o.val());
}
o = o.next_cl;
}
return result;
};
ListManager.prototype.map = function(f) {
var o, result;
o = this.beginning.next_cl;
result = [];
while (o !== this.end) {
if (!o.is_deleted) {
result.push(f(o));
}
o = o.next_cl;
}
return result;
};
ListManager.prototype.fold = function(init, f) {
var o;
o = this.beginning.next_cl;
while (o !== this.end) {
if (!o.is_deleted) {
init = f(init, o);
}
o = o.next_cl;
}
return init;
};
ListManager.prototype.val = function(pos) {
var o;
if (pos != null) {
o = this.getOperationByPosition(pos + 1);
if (!(o instanceof ops.Delimiter)) {
return o.val();
} else {
throw new Error("this position does not exist");
}
} else {
return this.toArray();
}
};
ListManager.prototype.ref = function(pos) {
var o;
if (pos != null) {
o = this.getOperationByPosition(pos + 1);
if (!(o instanceof ops.Delimiter)) {
return o;
} else {
return null;
}
} else {
throw new Error("you must specify a position parameter");
}
};
ListManager.prototype.getOperationByPosition = function(position) {
var o;
o = this.beginning;
while (true) {
if (o instanceof ops.Delimiter && (o.prev_cl != null)) {
o = o.prev_cl;
while (o.isDeleted() && (o.prev_cl != null)) {
o = o.prev_cl;
}
break;
}
if (position <= 0 && !o.isDeleted()) {
break;
}
o = o.next_cl;
if (!o.isDeleted()) {
position -= 1;
}
}
return o;
};
ListManager.prototype.push = function(content) {
return this.insertAfter(this.end.prev_cl, [content]);
};
ListManager.prototype.insertAfter = function(left, contents) {
var c, j, len, right, tmp;
right = left.next_cl;
while (right.isDeleted()) {
right = right.next_cl;
}
left = right.prev_cl;
if (contents instanceof ops.Operation) {
(new ops.Insert(null, content, null, void 0, void 0, left, right)).execute();
} else {
for (j = 0, len = contents.length; j < len; j++) {
c = contents[j];
if ((c != null) && (c._name != null) && (c._getModel != null)) {
c = c._getModel(this.custom_types, this.operations);
}
tmp = (new ops.Insert(null, c, null, void 0, void 0, left, right)).execute();
left = tmp;
}
}
return this;
};
ListManager.prototype.insert = function(position, contents) {
var ith;
ith = this.getOperationByPosition(position);
return this.insertAfter(ith, contents);
};
ListManager.prototype["delete"] = function(position, length) {
var d, delete_ops, i, j, o, ref;
if (length == null) {
length = 1;
}
o = this.getOperationByPosition(position + 1);
delete_ops = [];
for (i = j = 0, ref = length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
if (o instanceof ops.Delimiter) {
break;
}
d = (new ops.Delete(null, void 0, o)).execute();
o = o.next_cl;
while ((!(o instanceof ops.Delimiter)) && o.isDeleted()) {
o = o.next_cl;
}
delete_ops.push(d._encode());
}
return this;
};
ListManager.prototype.callOperationSpecificInsertEvents = function(op) {
var getContentType;
getContentType = function(content) {
if (content instanceof ops.Operation) {
return content.getCustomType();
} else {
return content;
}
};
return this.callEvent([
{
type: "insert",
reference: op,
position: op.getPosition(),
object: this.getCustomType(),
changedBy: op.uid.creator,
value: getContentType(op.val())
}
]);
};
ListManager.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {
return this.callEvent([
{
type: "delete",
reference: op,
position: op.getPosition(),
object: this.getCustomType(),
length: 1,
changedBy: del_op.uid.creator,
oldValue: op.val()
}
]);
};
return ListManager;
})(ops.Operation);
ops.ListManager.parse = function(json) {
var content, content_operations, custom_type, uid;
uid = json['uid'], custom_type = json['custom_type'], content = json['content'], content_operations = json['content_operations'];
return new this(custom_type, uid, content, content_operations);
};
ops.Composition = (function(superClass) {
extend(Composition, superClass);
function Composition(custom_type, _composition_value, composition_value_operations, uid, tmp_composition_ref) {
var n, o;
this._composition_value = _composition_value;
Composition.__super__.constructor.call(this, custom_type, uid);
if (tmp_composition_ref != null) {
this.tmp_composition_ref = tmp_composition_ref;
} else {
this.composition_ref = this.end.prev_cl;
}
if (composition_value_operations != null) {
this.composition_value_operations = {};
for (n in composition_value_operations) {
o = composition_value_operations[n];
this.saveOperation(n, o, '_composition_value');
}
}
}
Composition.prototype.type = "Composition";
Composition.prototype.execute = function() {
if (this.validateSavedOperations()) {
this.getCustomType()._setCompositionValue(this._composition_value);
delete this._composition_value;
return Composition.__super__.execute.apply(this, arguments);
} else {
return false;
}
};
Composition.prototype.callOperationSpecificInsertEvents = function(op) {
var o;
if (this.tmp_composition_ref != null) {
if (op.uid.creator === this.tmp_composition_ref.creator && op.uid.op_number === this.tmp_composition_ref.op_number) {
this.composition_ref = op;
delete this.tmp_composition_ref;
op = op.next_cl;
if (op === this.end) {
return;
}
} else {
return;
}
}
o = this.end.prev_cl;
while (o !== op) {
this.getCustomType()._unapply(o.undo_delta);
o = o.prev_cl;
}
while (o !== this.end) {
o.undo_delta = this.getCustomType()._apply(o.val());
o = o.next_cl;
}
this.composition_ref = this.end.prev_cl;
return this.callEvent([
{
type: "update",
changedBy: op.uid.creator,
newValue: this.val()
}
]);
};
Composition.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {};
Composition.prototype.applyDelta = function(delta, operations) {
(new ops.Insert(null, delta, operations, this, null, this.end.prev_cl, this.end)).execute();
return void 0;
};
Composition.prototype._encode = function(json) {
var custom, n, o, ref;
if (json == null) {
json = {};
}
custom = this.getCustomType()._getCompositionValue();
json.composition_value = custom.composition_value;
if (custom.composition_value_operations != null) {
json.composition_value_operations = {};
ref = custom.composition_value_operations;
for (n in ref) {
o = ref[n];
json.composition_value_operations[n] = o.getUid();
}
}
if (this.composition_ref != null) {
json.composition_ref = this.composition_ref.getUid();
} else {
json.composition_ref = this.tmp_composition_ref;
}
return Composition.__super__._encode.call(this, json);
};
return Composition;
})(ops.ListManager);
ops.Composition.parse = function(json) {
var composition_ref, composition_value, composition_value_operations, custom_type, uid;
uid = json['uid'], custom_type = json['custom_type'], composition_value = json['composition_value'], composition_value_operations = json['composition_value_operations'], composition_ref = json['composition_ref'];
return new this(custom_type, composition_value, composition_value_operations, uid, composition_ref);
};
ops.ReplaceManager = (function(superClass) {
extend(ReplaceManager, superClass);
function ReplaceManager(custom_type, event_properties1, event_this1, uid) {
this.event_properties = event_properties1;
this.event_this = event_this1;
if (this.event_properties['object'] == null) {
this.event_properties['object'] = this.event_this.getCustomType();
}
ReplaceManager.__super__.constructor.call(this, custom_type, uid);
}
ReplaceManager.prototype.type = "ReplaceManager";
ReplaceManager.prototype.callEventDecorator = function(events) {
var event, j, len, name, prop, ref;
if (!this.isDeleted()) {
for (j = 0, len = events.length; j < len; j++) {
event = events[j];
ref = this.event_properties;
for (name in ref) {
prop = ref[name];
event[name] = prop;
}
}
this.event_this.callEvent(events);
}
return void 0;
};
ReplaceManager.prototype.callOperationSpecificInsertEvents = function(op) {
var old_value;
if (op.next_cl.type === "Delimiter" && op.prev_cl.type !== "Delimiter") {
if (!op.is_deleted) {
old_value = op.prev_cl.val();
this.callEventDecorator([
{
type: "update",
changedBy: op.uid.creator,
oldValue: old_value
}
]);
}
op.prev_cl.applyDelete();
} else if (op.next_cl.type !== "Delimiter") {
op.applyDelete();
} else {
this.callEventDecorator([
{
type: "add",
changedBy: op.uid.creator
}
]);
}
return void 0;
};
ReplaceManager.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {
if (op.next_cl.type === "Delimiter") {
return this.callEventDecorator([
{
type: "delete",
changedBy: del_op.uid.creator,
oldValue: op.val()
}
]);
}
};
ReplaceManager.prototype.replace = function(content, replaceable_uid) {
var o, relp;
o = this.getLastOperation();
relp = (new ops.Insert(null, content, null, this, replaceable_uid, o, o.next_cl)).execute();
return void 0;
};
ReplaceManager.prototype.isContentDeleted = function() {
return this.getLastOperation().isDeleted();
};
ReplaceManager.prototype.deleteContent = function() {
var last_op;
last_op = this.getLastOperation();
if ((!last_op.isDeleted()) && last_op.type !== "Delimiter") {
(new ops.Delete(null, void 0, this.getLastOperation().uid)).execute();
}
return void 0;
};
ReplaceManager.prototype.val = function() {
var o;
o = this.getLastOperation();
return typeof o.val === "function" ? o.val() : void 0;
};
return ReplaceManager;
})(ops.ListManager);
return basic_ops;
};

View File

@ -1,542 +0,0 @@
var __slice = [].slice,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__hasProp = {}.hasOwnProperty;
module.exports = function(HB) {
var execution_listener, types;
types = {};
execution_listener = [];
types.Operation = (function() {
function Operation(uid) {
this.is_deleted = false;
this.garbage_collected = false;
this.event_listeners = [];
if (uid != null) {
this.uid = uid;
}
}
Operation.prototype.type = "Operation";
Operation.prototype.retrieveSub = function() {
throw new Error("sub properties are not enable on this operation type!");
};
Operation.prototype.observe = function(f) {
return this.event_listeners.push(f);
};
Operation.prototype.unobserve = function(f) {
return this.event_listeners = this.event_listeners.filter(function(g) {
return f !== g;
});
};
Operation.prototype.deleteAllObservers = function() {
return this.event_listeners = [];
};
Operation.prototype["delete"] = function() {
(new types.Delete(void 0, this)).execute();
return null;
};
Operation.prototype.callEvent = function() {
return this.forwardEvent.apply(this, [this].concat(__slice.call(arguments)));
};
Operation.prototype.forwardEvent = function() {
var args, f, op, _i, _len, _ref, _results;
op = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
_ref = this.event_listeners;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
f = _ref[_i];
_results.push(f.call.apply(f, [op].concat(__slice.call(args))));
}
return _results;
};
Operation.prototype.isDeleted = function() {
return this.is_deleted;
};
Operation.prototype.applyDelete = function(garbagecollect) {
if (garbagecollect == null) {
garbagecollect = true;
}
if (!this.garbage_collected) {
this.is_deleted = true;
if (garbagecollect) {
this.garbage_collected = true;
return HB.addToGarbageCollector(this);
}
}
};
Operation.prototype.cleanup = function() {
HB.removeOperation(this);
return this.deleteAllObservers();
};
Operation.prototype.setParent = function(_at_parent) {
this.parent = _at_parent;
};
Operation.prototype.getParent = function() {
return this.parent;
};
Operation.prototype.getUid = function() {
var map_uid;
if (this.uid.noOperation == null) {
return this.uid;
} else {
if (this.uid.alt != null) {
map_uid = this.uid.alt.cloneUid();
map_uid.sub = this.uid.sub;
return map_uid;
} else {
return void 0;
}
}
};
Operation.prototype.cloneUid = function() {
var n, uid, v, _ref;
uid = {};
_ref = this.getUid();
for (n in _ref) {
v = _ref[n];
uid[n] = v;
}
return uid;
};
Operation.prototype.execute = function() {
var l, _i, _len;
this.is_executed = true;
if (this.uid == null) {
this.uid = HB.getNextOperationIdentifier();
}
if (this.uid.noOperation == null) {
HB.addOperation(this);
for (_i = 0, _len = execution_listener.length; _i < _len; _i++) {
l = execution_listener[_i];
l(this._encode());
}
}
return this;
};
Operation.prototype.saveOperation = function(name, op) {
if (op == null) {
} else if ((op.execute != null) || !((op.op_number != null) && (op.creator != null))) {
return this[name] = op;
} else {
if (this.unchecked == null) {
this.unchecked = {};
}
return this.unchecked[name] = op;
}
};
Operation.prototype.validateSavedOperations = function() {
var name, op, op_uid, success, uninstantiated, _ref;
uninstantiated = {};
success = this;
_ref = this.unchecked;
for (name in _ref) {
op_uid = _ref[name];
op = HB.getOperation(op_uid);
if (op) {
this[name] = op;
} else {
uninstantiated[name] = op_uid;
success = false;
}
}
delete this.unchecked;
if (!success) {
this.unchecked = uninstantiated;
}
return success;
};
return Operation;
})();
types.Delete = (function(_super) {
__extends(Delete, _super);
function Delete(uid, deletes) {
this.saveOperation('deletes', deletes);
Delete.__super__.constructor.call(this, uid);
}
Delete.prototype.type = "Delete";
Delete.prototype._encode = function() {
return {
'type': "Delete",
'uid': this.getUid(),
'deletes': this.deletes.getUid()
};
};
Delete.prototype.execute = function() {
var res;
if (this.validateSavedOperations()) {
res = Delete.__super__.execute.apply(this, arguments);
if (res) {
this.deletes.applyDelete(this);
}
return res;
} else {
return false;
}
};
return Delete;
})(types.Operation);
types.Delete.parse = function(o) {
var deletes_uid, uid;
uid = o['uid'], deletes_uid = o['deletes'];
return new this(uid, deletes_uid);
};
types.Insert = (function(_super) {
__extends(Insert, _super);
function Insert(content, uid, prev_cl, next_cl, origin, parent) {
if (content === void 0) {
} else if ((content != null) && (content.creator != null)) {
this.saveOperation('content', content);
} else {
this.content = content;
}
this.saveOperation('parent', parent);
this.saveOperation('prev_cl', prev_cl);
this.saveOperation('next_cl', next_cl);
if (origin != null) {
this.saveOperation('origin', origin);
} else {
this.saveOperation('origin', prev_cl);
}
Insert.__super__.constructor.call(this, uid);
}
Insert.prototype.type = "Insert";
Insert.prototype.val = function() {
return this.content;
};
Insert.prototype.applyDelete = function(o) {
var callLater, garbagecollect, _ref;
if (this.deleted_by == null) {
this.deleted_by = [];
}
callLater = false;
if ((this.parent != null) && !this.isDeleted() && (o != null)) {
callLater = true;
}
if (o != null) {
this.deleted_by.push(o);
}
garbagecollect = false;
if (this.next_cl.isDeleted()) {
garbagecollect = true;
}
Insert.__super__.applyDelete.call(this, garbagecollect);
if (callLater) {
this.callOperationSpecificDeleteEvents(o);
}
if ((_ref = this.prev_cl) != null ? _ref.isDeleted() : void 0) {
this.prev_cl.applyDelete();
}
if (this.content instanceof types.Operation) {
this.content.applyDelete();
}
return delete this.content;
};
Insert.prototype.cleanup = function() {
var d, o, _i, _len, _ref;
if (this.next_cl.isDeleted()) {
_ref = this.deleted_by;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
d = _ref[_i];
d.cleanup();
}
o = this.next_cl;
while (o.type !== "Delimiter") {
if (o.origin === this) {
o.origin = this.prev_cl;
}
o = o.next_cl;
}
this.prev_cl.next_cl = this.next_cl;
this.next_cl.prev_cl = this.prev_cl;
return Insert.__super__.cleanup.apply(this, arguments);
}
};
Insert.prototype.getDistanceToOrigin = function() {
var d, o;
d = 0;
o = this.prev_cl;
while (true) {
if (this.origin === o) {
break;
}
d++;
o = o.prev_cl;
}
return d;
};
Insert.prototype.execute = function() {
var distance_to_origin, i, o;
if (!this.validateSavedOperations()) {
return false;
} else {
if (this.content instanceof types.Operation) {
this.content.insert_parent = this;
}
if (this.parent != null) {
if (this.prev_cl == null) {
this.prev_cl = this.parent.beginning;
}
if (this.origin == null) {
this.origin = this.prev_cl;
} else if (this.origin === "Delimiter") {
this.origin = this.parent.beginning;
}
if (this.next_cl == null) {
this.next_cl = this.parent.end;
}
}
if (this.prev_cl != null) {
distance_to_origin = this.getDistanceToOrigin();
o = this.prev_cl.next_cl;
i = distance_to_origin;
while (true) {
if (o !== this.next_cl) {
if (o.getDistanceToOrigin() === i) {
if (o.uid.creator < this.uid.creator) {
this.prev_cl = o;
distance_to_origin = i + 1;
} else {
}
} else if (o.getDistanceToOrigin() < i) {
if (i - distance_to_origin <= o.getDistanceToOrigin()) {
this.prev_cl = o;
distance_to_origin = i + 1;
} else {
}
} else {
break;
}
i++;
o = o.next_cl;
} else {
break;
}
}
this.next_cl = this.prev_cl.next_cl;
this.prev_cl.next_cl = this;
this.next_cl.prev_cl = this;
}
this.setParent(this.prev_cl.getParent());
Insert.__super__.execute.apply(this, arguments);
this.callOperationSpecificInsertEvents();
return this;
}
};
Insert.prototype.callOperationSpecificInsertEvents = function() {
var _ref;
return (_ref = this.parent) != null ? _ref.callEvent([
{
type: "insert",
position: this.getPosition(),
object: this.parent,
changedBy: this.uid.creator,
value: this.content
}
]) : void 0;
};
Insert.prototype.callOperationSpecificDeleteEvents = function(o) {
return this.parent.callEvent([
{
type: "delete",
position: this.getPosition(),
object: this.parent,
length: 1,
changedBy: o.uid.creator
}
]);
};
Insert.prototype.getPosition = function() {
var position, prev;
position = 0;
prev = this.prev_cl;
while (true) {
if (prev instanceof types.Delimiter) {
break;
}
if (!prev.isDeleted()) {
position++;
}
prev = prev.prev_cl;
}
return position;
};
Insert.prototype._encode = function() {
var json, _ref;
json = {
'type': this.type,
'uid': this.getUid(),
'prev': this.prev_cl.getUid(),
'next': this.next_cl.getUid(),
'parent': this.parent.getUid()
};
if (this.origin.type === "Delimiter") {
json.origin = "Delimiter";
} else if (this.origin !== this.prev_cl) {
json.origin = this.origin.getUid();
}
if (((_ref = this.content) != null ? _ref.getUid : void 0) != null) {
json['content'] = this.content.getUid();
} else {
json['content'] = JSON.stringify(this.content);
}
return json;
};
return Insert;
})(types.Operation);
types.Insert.parse = function(json) {
var content, next, origin, parent, prev, uid;
content = json['content'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], parent = json['parent'];
if (typeof content === "string") {
content = JSON.parse(content);
}
return new this(content, uid, prev, next, origin, parent);
};
types.ImmutableObject = (function(_super) {
__extends(ImmutableObject, _super);
function ImmutableObject(uid, _at_content) {
this.content = _at_content;
ImmutableObject.__super__.constructor.call(this, uid);
}
ImmutableObject.prototype.type = "ImmutableObject";
ImmutableObject.prototype.val = function() {
return this.content;
};
ImmutableObject.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid(),
'content': this.content
};
return json;
};
return ImmutableObject;
})(types.Operation);
types.ImmutableObject.parse = function(json) {
var content, uid;
uid = json['uid'], content = json['content'];
return new this(uid, content);
};
types.Delimiter = (function(_super) {
__extends(Delimiter, _super);
function Delimiter(prev_cl, next_cl, origin) {
this.saveOperation('prev_cl', prev_cl);
this.saveOperation('next_cl', next_cl);
this.saveOperation('origin', prev_cl);
Delimiter.__super__.constructor.call(this, {
noOperation: true
});
}
Delimiter.prototype.type = "Delimiter";
Delimiter.prototype.applyDelete = function() {
var o;
Delimiter.__super__.applyDelete.call(this);
o = this.prev_cl;
while (o != null) {
o.applyDelete();
o = o.prev_cl;
}
return void 0;
};
Delimiter.prototype.cleanup = function() {
return Delimiter.__super__.cleanup.call(this);
};
Delimiter.prototype.execute = function() {
var _ref, _ref1;
if (((_ref = this.unchecked) != null ? _ref['next_cl'] : void 0) != null) {
return Delimiter.__super__.execute.apply(this, arguments);
} else if ((_ref1 = this.unchecked) != null ? _ref1['prev_cl'] : void 0) {
if (this.validateSavedOperations()) {
if (this.prev_cl.next_cl != null) {
throw new Error("Probably duplicated operations");
}
this.prev_cl.next_cl = this;
return Delimiter.__super__.execute.apply(this, arguments);
} else {
return false;
}
} else if ((this.prev_cl != null) && (this.prev_cl.next_cl == null)) {
delete this.prev_cl.unchecked.next_cl;
this.prev_cl.next_cl = this;
return Delimiter.__super__.execute.apply(this, arguments);
} else if ((this.prev_cl != null) || (this.next_cl != null) || true) {
return Delimiter.__super__.execute.apply(this, arguments);
}
};
Delimiter.prototype._encode = function() {
var _ref, _ref1;
return {
'type': this.type,
'uid': this.getUid(),
'prev': (_ref = this.prev_cl) != null ? _ref.getUid() : void 0,
'next': (_ref1 = this.next_cl) != null ? _ref1.getUid() : void 0
};
};
return Delimiter;
})(types.Operation);
types.Delimiter.parse = function(json) {
var next, prev, uid;
uid = json['uid'], prev = json['prev'], next = json['next'];
return new this(uid, prev, next);
};
return {
'types': types,
'execution_listener': execution_listener
};
};

View File

@ -1,158 +0,0 @@
var text_types_uninitialized,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__hasProp = {}.hasOwnProperty;
text_types_uninitialized = require("./TextTypes");
module.exports = function(HB) {
var text_types, types;
text_types = text_types_uninitialized(HB);
types = text_types.types;
types.Object = (function(_super) {
__extends(Object, _super);
function Object() {
return Object.__super__.constructor.apply(this, arguments);
}
Object.prototype.type = "Object";
Object.prototype.applyDelete = function() {
return Object.__super__.applyDelete.call(this);
};
Object.prototype.cleanup = function() {
return Object.__super__.cleanup.call(this);
};
Object.prototype.toJson = function(transform_to_value) {
var json, name, o, that, val;
if (transform_to_value == null) {
transform_to_value = false;
}
if ((this.bound_json == null) || (Object.observe == null) || true) {
val = this.val();
json = {};
for (name in val) {
o = val[name];
if (o instanceof types.Object) {
json[name] = o.toJson(transform_to_value);
} else if (o instanceof types.ListManager) {
json[name] = o.toJson(transform_to_value);
} else if (transform_to_value && o instanceof types.Operation) {
json[name] = o.val();
} else {
json[name] = o;
}
}
this.bound_json = json;
if (Object.observe != null) {
that = this;
Object.observe(this.bound_json, function(events) {
var event, _i, _len, _results;
_results = [];
for (_i = 0, _len = events.length; _i < _len; _i++) {
event = events[_i];
if ((event.changedBy == null) && (event.type === "add" || (event.type = "update"))) {
_results.push(that.val(event.name, event.object[event.name]));
} else {
_results.push(void 0);
}
}
return _results;
});
this.observe(function(events) {
var event, notifier, oldVal, _i, _len, _results;
_results = [];
for (_i = 0, _len = events.length; _i < _len; _i++) {
event = events[_i];
if (event.created_ !== HB.getUserId()) {
notifier = Object.getNotifier(that.bound_json);
oldVal = that.bound_json[event.name];
if (oldVal != null) {
notifier.performChange('update', function() {
return that.bound_json[event.name] = that.val(event.name);
}, that.bound_json);
_results.push(notifier.notify({
object: that.bound_json,
type: 'update',
name: event.name,
oldValue: oldVal,
changedBy: event.changedBy
}));
} else {
notifier.performChange('add', function() {
return that.bound_json[event.name] = that.val(event.name);
}, that.bound_json);
_results.push(notifier.notify({
object: that.bound_json,
type: 'add',
name: event.name,
oldValue: oldVal,
changedBy: event.changedBy
}));
}
} else {
_results.push(void 0);
}
}
return _results;
});
}
}
return this.bound_json;
};
Object.prototype.val = function(name, content) {
var args, i, o, type, _i, _ref;
if ((name != null) && arguments.length > 1) {
if ((content != null) && (content.constructor != null)) {
type = types[content.constructor.name];
if ((type != null) && (type.create != null)) {
args = [];
for (i = _i = 1, _ref = arguments.length; 1 <= _ref ? _i < _ref : _i > _ref; i = 1 <= _ref ? ++_i : --_i) {
args.push(arguments[i]);
}
o = type.create.apply(null, args);
return Object.__super__.val.call(this, name, o);
} else {
throw new Error("The " + content.constructor.name + "-type is not (yet) supported in Y.");
}
} else {
return Object.__super__.val.call(this, name, content);
}
} else {
return Object.__super__.val.call(this, name);
}
};
Object.prototype._encode = function() {
return {
'type': this.type,
'uid': this.getUid()
};
};
return Object;
})(types.MapManager);
types.Object.parse = function(json) {
var uid;
uid = json['uid'];
return new this(uid);
};
types.Object.create = function(content, mutable) {
var json, n, o;
json = new types.Object().execute();
for (n in content) {
o = content[n];
json.val(n, o, mutable);
}
return json;
};
types.Number = {};
types.Number.create = function(content) {
return content;
};
return text_types;
};

View File

@ -1,517 +0,0 @@
var basic_types_uninitialized,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__hasProp = {}.hasOwnProperty;
basic_types_uninitialized = require("./BasicTypes");
module.exports = function(HB) {
var basic_types, types;
basic_types = basic_types_uninitialized(HB);
types = basic_types.types;
types.MapManager = (function(_super) {
__extends(MapManager, _super);
function MapManager(uid) {
this.map = {};
MapManager.__super__.constructor.call(this, uid);
}
MapManager.prototype.type = "MapManager";
MapManager.prototype.applyDelete = function() {
var name, p, _ref;
_ref = this.map;
for (name in _ref) {
p = _ref[name];
p.applyDelete();
}
return MapManager.__super__.applyDelete.call(this);
};
MapManager.prototype.cleanup = function() {
return MapManager.__super__.cleanup.call(this);
};
MapManager.prototype.val = function(name, content) {
var o, prop, result, _ref;
if (arguments.length > 1) {
this.retrieveSub(name).replace(content);
return this;
} else if (name != null) {
prop = this.map[name];
if ((prop != null) && !prop.isContentDeleted()) {
return prop.val();
} else {
return void 0;
}
} else {
result = {};
_ref = this.map;
for (name in _ref) {
o = _ref[name];
if (!o.isContentDeleted()) {
result[name] = o.val();
}
}
return result;
}
};
MapManager.prototype["delete"] = function(name) {
var _ref;
if ((_ref = this.map[name]) != null) {
_ref.deleteContent();
}
return this;
};
MapManager.prototype.retrieveSub = function(property_name) {
var event_properties, event_this, rm, rm_uid;
if (this.map[property_name] == null) {
event_properties = {
name: property_name
};
event_this = this;
rm_uid = {
noOperation: true,
sub: property_name,
alt: this
};
rm = new types.ReplaceManager(event_properties, event_this, rm_uid);
this.map[property_name] = rm;
rm.setParent(this, property_name);
rm.execute();
}
return this.map[property_name];
};
return MapManager;
})(types.Operation);
types.ListManager = (function(_super) {
__extends(ListManager, _super);
function ListManager(uid) {
this.beginning = new types.Delimiter(void 0, void 0);
this.end = new types.Delimiter(this.beginning, void 0);
this.beginning.next_cl = this.end;
this.beginning.execute();
this.end.execute();
ListManager.__super__.constructor.call(this, uid);
}
ListManager.prototype.type = "ListManager";
ListManager.prototype.applyDelete = function() {
var o;
o = this.end;
while (o != null) {
o.applyDelete();
o = o.prev_cl;
}
return ListManager.__super__.applyDelete.call(this);
};
ListManager.prototype.cleanup = function() {
return ListManager.__super__.cleanup.call(this);
};
ListManager.prototype.toJson = function(transform_to_value) {
var i, o, val, _i, _len, _results;
if (transform_to_value == null) {
transform_to_value = false;
}
val = this.val();
_results = [];
for (o = _i = 0, _len = val.length; _i < _len; o = ++_i) {
i = val[o];
if (o instanceof types.Object) {
_results.push(o.toJson(transform_to_value));
} else if (o instanceof types.ListManager) {
_results.push(o.toJson(transform_to_value));
} else if (transform_to_value && o instanceof types.Operation) {
_results.push(o.val());
} else {
_results.push(o);
}
}
return _results;
};
ListManager.prototype.execute = function() {
if (this.validateSavedOperations()) {
this.beginning.setParent(this);
this.end.setParent(this);
return ListManager.__super__.execute.apply(this, arguments);
} else {
return false;
}
};
ListManager.prototype.getLastOperation = function() {
return this.end.prev_cl;
};
ListManager.prototype.getFirstOperation = function() {
return this.beginning.next_cl;
};
ListManager.prototype.toArray = function() {
var o, result;
o = this.beginning.next_cl;
result = [];
while (o !== this.end) {
if (!o.is_deleted) {
result.push(o);
}
o = o.next_cl;
}
return result;
};
ListManager.prototype.map = function(f) {
var o, result;
o = this.beginning.next_cl;
result = [];
while (o !== this.end) {
if (!o.is_deleted) {
result.push(f(o));
}
o = o.next_cl;
}
return result;
};
ListManager.prototype.fold = function(init, f) {
var o;
o = this.beginning.next_cl;
while (o !== this.end) {
if (!o.is_deleted) {
init = f(init, o);
}
o = o.next_cl;
}
return init;
};
ListManager.prototype.val = function(pos) {
var o;
if (pos != null) {
o = this.getOperationByPosition(pos + 1);
if (!(o instanceof types.Delimiter)) {
return o.val();
} else {
throw new Error("this position does not exist");
}
} else {
return this.toArray();
}
};
ListManager.prototype.getOperationByPosition = function(position) {
var o;
o = this.beginning;
while (true) {
if (o instanceof types.Delimiter && (o.prev_cl != null)) {
o = o.prev_cl;
while (o.isDeleted() && (o.prev_cl != null)) {
o = o.prev_cl;
}
break;
}
if (position <= 0 && !o.isDeleted()) {
break;
}
o = o.next_cl;
if (!o.isDeleted()) {
position -= 1;
}
}
return o;
};
ListManager.prototype.push = function(content) {
return this.insertAfter(this.end.prev_cl, content);
};
ListManager.prototype.insertAfter = function(left, content, options) {
var c, createContent, right, tmp, _i, _len;
createContent = function(content, options) {
var type;
if ((content != null) && (content.constructor != null)) {
type = types[content.constructor.name];
if ((type != null) && (type.create != null)) {
return type.create(content, options);
} else {
throw new Error("The " + content.constructor.name + "-type is not (yet) supported in Y.");
}
} else {
return content;
}
};
right = left.next_cl;
while (right.isDeleted()) {
right = right.next_cl;
}
left = right.prev_cl;
if (content instanceof types.Operation) {
(new types.Insert(content, void 0, left, right)).execute();
} else {
for (_i = 0, _len = content.length; _i < _len; _i++) {
c = content[_i];
tmp = (new types.Insert(createContent(c, options), void 0, left, right)).execute();
left = tmp;
}
}
return this;
};
ListManager.prototype.insert = function(position, content, options) {
var ith;
ith = this.getOperationByPosition(position);
return this.insertAfter(ith, [content], options);
};
ListManager.prototype["delete"] = function(position, length) {
var d, delete_ops, i, o, _i;
o = this.getOperationByPosition(position + 1);
delete_ops = [];
for (i = _i = 0; 0 <= length ? _i < length : _i > length; i = 0 <= length ? ++_i : --_i) {
if (o instanceof types.Delimiter) {
break;
}
d = (new types.Delete(void 0, o)).execute();
o = o.next_cl;
while ((!(o instanceof types.Delimiter)) && o.isDeleted()) {
o = o.next_cl;
}
delete_ops.push(d._encode());
}
return this;
};
ListManager.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid()
};
return json;
};
return ListManager;
})(types.Operation);
types.ListManager.parse = function(json) {
var uid;
uid = json['uid'];
return new this(uid);
};
types.Array = function() {};
types.Array.create = function(content, mutable) {
var ith, list;
if (mutable === "mutable") {
list = new types.ListManager().execute();
ith = list.getOperationByPosition(0);
list.insertAfter(ith, content);
return list;
} else if ((mutable == null) || (mutable === "immutable")) {
return content;
} else {
throw new Error("Specify either \"mutable\" or \"immutable\"!!");
}
};
types.ReplaceManager = (function(_super) {
__extends(ReplaceManager, _super);
function ReplaceManager(_at_event_properties, _at_event_this, uid, beginning, end) {
this.event_properties = _at_event_properties;
this.event_this = _at_event_this;
if (this.event_properties['object'] == null) {
this.event_properties['object'] = this.event_this;
}
ReplaceManager.__super__.constructor.call(this, uid, beginning, end);
}
ReplaceManager.prototype.type = "ReplaceManager";
ReplaceManager.prototype.applyDelete = function() {
var o;
o = this.beginning;
while (o != null) {
o.applyDelete();
o = o.next_cl;
}
return ReplaceManager.__super__.applyDelete.call(this);
};
ReplaceManager.prototype.cleanup = function() {
return ReplaceManager.__super__.cleanup.call(this);
};
ReplaceManager.prototype.callEventDecorator = function(events) {
var event, name, prop, _i, _len, _ref;
if (!this.isDeleted()) {
for (_i = 0, _len = events.length; _i < _len; _i++) {
event = events[_i];
_ref = this.event_properties;
for (name in _ref) {
prop = _ref[name];
event[name] = prop;
}
}
this.event_this.callEvent(events);
}
return void 0;
};
ReplaceManager.prototype.replace = function(content, replaceable_uid) {
var o, relp;
o = this.getLastOperation();
relp = (new types.Replaceable(content, this, replaceable_uid, o, o.next_cl)).execute();
return void 0;
};
ReplaceManager.prototype.isContentDeleted = function() {
return this.getLastOperation().isDeleted();
};
ReplaceManager.prototype.deleteContent = function() {
(new types.Delete(void 0, this.getLastOperation().uid)).execute();
return void 0;
};
ReplaceManager.prototype.val = function() {
var o;
o = this.getLastOperation();
return typeof o.val === "function" ? o.val() : void 0;
};
ReplaceManager.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid(),
'beginning': this.beginning.getUid(),
'end': this.end.getUid()
};
return json;
};
return ReplaceManager;
})(types.ListManager);
types.Replaceable = (function(_super) {
__extends(Replaceable, _super);
function Replaceable(content, parent, uid, prev, next, origin, is_deleted) {
this.saveOperation('parent', parent);
Replaceable.__super__.constructor.call(this, content, uid, prev, next, origin);
this.is_deleted = is_deleted;
}
Replaceable.prototype.type = "Replaceable";
Replaceable.prototype.val = function() {
return this.content;
};
Replaceable.prototype.applyDelete = function() {
var res, _base, _base1, _base2;
res = Replaceable.__super__.applyDelete.apply(this, arguments);
if (this.content != null) {
if (this.next_cl.type !== "Delimiter") {
if (typeof (_base = this.content).deleteAllObservers === "function") {
_base.deleteAllObservers();
}
}
if (typeof (_base1 = this.content).applyDelete === "function") {
_base1.applyDelete();
}
if (typeof (_base2 = this.content).dontSync === "function") {
_base2.dontSync();
}
}
this.content = null;
return res;
};
Replaceable.prototype.cleanup = function() {
return Replaceable.__super__.cleanup.apply(this, arguments);
};
Replaceable.prototype.callOperationSpecificInsertEvents = function() {
var old_value;
if (this.next_cl.type === "Delimiter" && this.prev_cl.type !== "Delimiter") {
if (!this.is_deleted) {
old_value = this.prev_cl.content;
this.parent.callEventDecorator([
{
type: "update",
changedBy: this.uid.creator,
oldValue: old_value
}
]);
}
this.prev_cl.applyDelete();
} else if (this.next_cl.type !== "Delimiter") {
this.applyDelete();
} else {
this.parent.callEventDecorator([
{
type: "add",
changedBy: this.uid.creator
}
]);
}
return void 0;
};
Replaceable.prototype.callOperationSpecificDeleteEvents = function(o) {
if (this.next_cl.type === "Delimiter") {
return this.parent.callEventDecorator([
{
type: "delete",
changedBy: o.uid.creator,
oldValue: this.content
}
]);
}
};
Replaceable.prototype._encode = function() {
var json;
json = {
'type': this.type,
'parent': this.parent.getUid(),
'prev': this.prev_cl.getUid(),
'next': this.next_cl.getUid(),
'uid': this.getUid(),
'is_deleted': this.is_deleted
};
if (this.origin.type === "Delimiter") {
json.origin = "Delimiter";
} else if (this.origin !== this.prev_cl) {
json.origin = this.origin.getUid();
}
if (this.content instanceof types.Operation) {
json['content'] = this.content.getUid();
} else {
if ((this.content != null) && (this.content.creator != null)) {
throw new Error("You must not set creator here!");
}
json['content'] = this.content;
}
return json;
};
return Replaceable;
})(types.Insert);
types.Replaceable.parse = function(json) {
var content, is_deleted, next, origin, parent, prev, uid;
content = json['content'], parent = json['parent'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], is_deleted = json['is_deleted'];
return new this(content, parent, uid, prev, next, origin, is_deleted);
};
return basic_types;
};

View File

@ -1,322 +0,0 @@
var structured_types_uninitialized,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__hasProp = {}.hasOwnProperty;
structured_types_uninitialized = require("./StructuredTypes");
module.exports = function(HB) {
var parser, structured_types, types;
structured_types = structured_types_uninitialized(HB);
types = structured_types.types;
parser = structured_types.parser;
types.String = (function(_super) {
__extends(String, _super);
function String(uid) {
this.textfields = [];
String.__super__.constructor.call(this, uid);
}
String.prototype.type = "String";
String.prototype.val = function() {
return this.fold("", function(left, o) {
return left + o.val();
});
};
String.prototype.toString = function() {
return this.val();
};
String.prototype.insert = function(position, content, options) {
var ith;
ith = this.getOperationByPosition(position);
return this.insertAfter(ith, content, options);
};
String.prototype.bind = function(textfield, dom_root) {
var createRange, creator_token, t, word, writeContent, writeRange, _i, _len, _ref;
if (dom_root == null) {
dom_root = window;
}
if (dom_root.getSelection == null) {
dom_root = window;
}
_ref = this.textfields;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
t = _ref[_i];
if (t === textfield) {
return;
}
}
creator_token = false;
word = this;
textfield.value = this.val();
this.textfields.push(textfield);
if ((textfield.selectionStart != null) && (textfield.setSelectionRange != null)) {
createRange = function(fix) {
var left, right;
left = textfield.selectionStart;
right = textfield.selectionEnd;
if (fix != null) {
left = fix(left);
right = fix(right);
}
return {
left: left,
right: right
};
};
writeRange = function(range) {
writeContent(word.val());
return textfield.setSelectionRange(range.left, range.right);
};
writeContent = function(content) {
return textfield.value = content;
};
} else {
createRange = function(fix) {
var clength, edited_element, range, s;
range = {};
s = dom_root.getSelection();
clength = textfield.textContent.length;
range.left = Math.min(s.anchorOffset, clength);
range.right = Math.min(s.focusOffset, clength);
if (fix != null) {
range.left = fix(range.left);
range.right = fix(range.right);
}
edited_element = s.focusNode;
if (edited_element === textfield || edited_element === textfield.childNodes[0]) {
range.isReal = true;
} else {
range.isReal = false;
}
return range;
};
writeRange = function(range) {
var r, s, textnode;
writeContent(word.val());
textnode = textfield.childNodes[0];
if (range.isReal && (textnode != null)) {
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 = document.createRange();
r.setStart(textnode, range.left);
r.setEnd(textnode, range.right);
s = window.getSelection();
s.removeAllRanges();
return s.addRange(r);
}
};
writeContent = function(content) {
var c, content_array, i, _j, _len1, _results;
content_array = content.replace(new RegExp("\n", 'g'), " ").split(" ");
textfield.innerText = "";
_results = [];
for (i = _j = 0, _len1 = content_array.length; _j < _len1; i = ++_j) {
c = content_array[i];
textfield.innerText += c;
if (i !== content_array.length - 1) {
_results.push(textfield.innerHTML += '&nbsp;');
} else {
_results.push(void 0);
}
}
return _results;
};
}
writeContent(this.val());
this.observe(function(events) {
var event, fix, o_pos, r, _j, _len1, _results;
_results = [];
for (_j = 0, _len1 = events.length; _j < _len1; _j++) {
event = events[_j];
if (!creator_token) {
if (event.type === "insert") {
o_pos = event.position;
fix = function(cursor) {
if (cursor <= o_pos) {
return cursor;
} else {
cursor += 1;
return cursor;
}
};
r = createRange(fix);
_results.push(writeRange(r));
} else if (event.type === "delete") {
o_pos = event.position;
fix = function(cursor) {
if (cursor < o_pos) {
return cursor;
} else {
cursor -= 1;
return cursor;
}
};
r = createRange(fix);
_results.push(writeRange(r));
} else {
_results.push(void 0);
}
} else {
_results.push(void 0);
}
}
return _results;
});
textfield.onkeypress = function(event) {
var char, diff, pos, r;
if (word.is_deleted) {
textfield.onkeypress = null;
return true;
}
creator_token = true;
char = null;
if (event.keyCode === 13) {
char = '\n';
} else if (event.key != null) {
if (event.charCode === 32) {
char = " ";
} else {
char = event.key;
}
} else {
char = window.String.fromCharCode(event.keyCode);
}
if (char.length > 1) {
return true;
} else if (char.length > 0) {
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();
creator_token = false;
return false;
};
textfield.onpaste = function(event) {
if (word.is_deleted) {
textfield.onpaste = null;
return true;
}
return event.preventDefault();
};
textfield.oncut = function(event) {
if (word.is_deleted) {
textfield.oncut = null;
return true;
}
return event.preventDefault();
};
return textfield.onkeydown = function(event) {
var del_length, diff, new_pos, pos, r, val;
creator_token = true;
if (word.is_deleted) {
textfield.onkeydown = null;
return true;
}
r = createRange();
pos = Math.min(r.left, r.right, word.val().length);
diff = Math.abs(r.left - r.right);
if ((event.keyCode != null) && event.keyCode === 8) {
if (diff > 0) {
word["delete"](pos, diff);
r.left = pos;
r.right = pos;
writeRange(r);
} else {
if ((event.ctrlKey != null) && event.ctrlKey) {
val = word.val();
new_pos = pos;
del_length = 0;
if (pos > 0) {
new_pos--;
del_length++;
}
while (new_pos > 0 && val[new_pos] !== " " && val[new_pos] !== '\n') {
new_pos--;
del_length++;
}
word["delete"](new_pos, pos - new_pos);
r.left = new_pos;
r.right = new_pos;
writeRange(r);
} else {
if (pos > 0) {
word["delete"](pos - 1, 1);
r.left = pos - 1;
r.right = pos - 1;
writeRange(r);
}
}
}
event.preventDefault();
creator_token = false;
return false;
} else if ((event.keyCode != null) && event.keyCode === 46) {
if (diff > 0) {
word["delete"](pos, diff);
r.left = pos;
r.right = pos;
writeRange(r);
} else {
word["delete"](pos, 1);
r.left = pos;
r.right = pos;
writeRange(r);
}
event.preventDefault();
creator_token = false;
return false;
} else {
creator_token = false;
return true;
}
};
};
String.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid()
};
return json;
};
return String;
})(types.ListManager);
types.String.parse = function(json) {
var uid;
uid = json['uid'];
return new this(uid);
};
types.String.create = function(content, mutable) {
var word;
if (mutable === "mutable") {
word = new types.String().execute();
word.insert(0, content);
return word;
} else if ((mutable == null) || (mutable === "immutable")) {
return content;
} else {
throw new Error("Specify either \"mutable\" or \"immutable\"!!");
}
};
return structured_types;
};

View File

@ -3,42 +3,42 @@ var Y, bindToChildren;
Y = require('./y');
bindToChildren = function(that) {
var attr, i, _i, _ref;
for (i = _i = 0, _ref = that.children.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
var attr, i, j, ref;
for (i = j = 0, ref = that.children.length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
attr = that.children.item(i);
if (attr.name != null) {
attr.val = that.val.val(attr.name);
}
}
return that.val.observe(function(events) {
var event, newVal, _j, _len, _results;
_results = [];
for (_j = 0, _len = events.length; _j < _len; _j++) {
event = events[_j];
var event, k, len, newVal, results;
results = [];
for (k = 0, len = events.length; k < len; k++) {
event = events[k];
if (event.name != null) {
_results.push((function() {
var _k, _ref1, _results1;
_results1 = [];
for (i = _k = 0, _ref1 = that.children.length; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
results.push((function() {
var l, ref1, results1;
results1 = [];
for (i = l = 0, ref1 = that.children.length; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) {
attr = that.children.item(i);
if ((attr.name != null) && attr.name === event.name) {
newVal = that.val.val(attr.name);
if (attr.val !== newVal) {
_results1.push(attr.val = newVal);
results1.push(attr.val = newVal);
} else {
_results1.push(void 0);
results1.push(void 0);
}
} else {
_results1.push(void 0);
results1.push(void 0);
}
}
return _results1;
return results1;
})());
} else {
_results.push(void 0);
results.push(void 0);
}
}
return _results;
return results;
});
};
@ -78,13 +78,13 @@ Polymer("y-property", {
}
},
valChanged: function() {
var _ref;
var ref;
if ((this.val != null) && (this.name != null)) {
if (this.val.constructor === Object) {
return this.val = this.parentElement.val.val(this.name, this.val).val(this.name);
} else if (this.val.type === "Object") {
return bindToChildren(this);
} else if ((((_ref = this.parentElement.val) != null ? _ref.val : void 0) != null) && this.val !== this.parentElement.val.val(this.name)) {
} else if ((((ref = this.parentElement.val) != null ? ref.val : void 0) != null) && this.val !== this.parentElement.val.val(this.name)) {
return this.parentElement.val.val(this.name, this.val);
}
}

View File

@ -1,8 +1,6 @@
var Engine, HistoryBuffer, adaptConnector, createY, json_types_uninitialized,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__hasProp = {}.hasOwnProperty;
var Engine, HistoryBuffer, adaptConnector, createY, structured_ops_uninitialized;
json_types_uninitialized = require("./Types/JsonTypes");
structured_ops_uninitialized = require("./Operations/Structured");
HistoryBuffer = require("./HistoryBuffer");
@ -11,7 +9,7 @@ Engine = require("./Engine");
adaptConnector = require("./ConnectorAdapter");
createY = function(connector) {
var HB, Y, type_manager, types, user_id;
var HB, ct, engine, model, ops, ops_manager, user_id;
user_id = null;
if (connector.user_id != null) {
user_id = connector.user_id;
@ -23,32 +21,25 @@ createY = function(connector) {
};
}
HB = new HistoryBuffer(user_id);
type_manager = json_types_uninitialized(HB);
types = type_manager.types;
Y = (function(_super) {
__extends(Y, _super);
function Y() {
this.connector = connector;
this.HB = HB;
this.types = types;
this.engine = new Engine(this.HB, type_manager.types);
adaptConnector(this.connector, this.engine, this.HB, type_manager.execution_listener);
Y.__super__.constructor.apply(this, arguments);
}
Y.prototype.getConnector = function() {
return this.connector;
};
return Y;
})(types.Object);
return new Y(HB.getReservedUniqueIdentifier()).execute();
ops_manager = structured_ops_uninitialized(HB, this.constructor);
ops = ops_manager.operations;
engine = new Engine(HB, ops);
adaptConnector(connector, engine, HB, ops_manager.execution_listener);
ops.Operation.prototype.HB = HB;
ops.Operation.prototype.operations = ops;
ops.Operation.prototype.engine = engine;
ops.Operation.prototype.connector = connector;
ops.Operation.prototype.custom_types = this.constructor;
ct = new createY.Object();
model = new ops.MapManager(ct, HB.getReservedUniqueIdentifier()).execute();
ct._setModel(model);
return ct;
};
module.exports = createY;
if ((typeof window !== "undefined" && window !== null) && (window.Y == null)) {
if (typeof window !== "undefined" && window !== null) {
window.Y = createY;
}
createY.Object = require("./ObjectType");

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,20 +2,20 @@
<head>
<meta charset="utf-8">
<title>Test Yjs!</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
</head>
<body>
<div id="mocha"></div>
<div id="test_dom" test_attribute="the test" class="stuffy" style="color: blue"><p id="replaceme">replace me</p><p id="removeme">remove me</p><p>This is a test object for <b>XmlFramework</b></p><span class="span_element"><p>span</p></span></div>
<script src="../../node_modules/mocha/mocha.js" class="awesome"></script>
<script>
mocha.setup('bdd');
mocha.ui('bdd');
mocha.reporter('html');
</script>
<script src="Text_test.js"></script>
<script src="Json_test.js"></script>
<!--script src="Xml_test_browser.js"></script-->
<script src="object-test.js"></script>
<script src="xml-test.js"></script>
<script src="list-test.js"></script>
<script src="text-test.js"></script>
<script>
//mocha.checkLeaks();
//mocha.run();

18687
build/test/list-test.js Normal file

File diff suppressed because one or more lines are too long

18383
build/test/object-test.js Normal file

File diff suppressed because one or more lines are too long

32697
build/test/richtext-test.js Normal file

File diff suppressed because one or more lines are too long

20116
build/test/selections-test.js Normal file

File diff suppressed because one or more lines are too long

18705
build/test/text-test.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,6 @@ mocha = require 'gulp-mocha'
run = require 'gulp-run'
ljs = require 'gulp-ljs'
plumber = require 'gulp-plumber'
mochaPhantomJS = require 'gulp-mocha-phantomjs'
cache = require 'gulp-cached'
coffeeify = require 'gulp-coffeeify'
exit = require 'gulp-exit'
@ -25,11 +24,11 @@ gulp.task 'default', ['build_browser']
files =
lib : ['./lib/**/*.coffee']
browser : ['./lib/y.coffee','./lib/y-object.coffee']
#test : ['./test/**/*_test.coffee']
test : ['./test/Json_test.coffee', './test/Text_test.coffee']
test : ['./test/**/*test.coffee', '../y-*/test/*test.coffee']
#test : ['./test/Json_test.coffee', './test/Text_test.coffee']
gulp : ['./gulpfile.coffee']
examples : ['./examples/**/*.js']
other: ['./lib/**/*']
other: ['./lib/**/*', './test/*']
files.all = []
for name,file_list of files
@ -44,7 +43,7 @@ gulp.task 'deploy_nodejs', ->
.pipe gulp.dest 'build/node/'
.pipe gulpif '!**/', git.add({args : "-A"})
gulp.task 'deploy', ['mocha', 'build_browser', 'deploy_nodejs', 'lint', 'phantom_test', 'codo']
gulp.task 'deploy', ['mocha', 'build_browser', 'deploy_nodejs', 'lint', 'codo']
gulp.task 'build_browser', ->
gulp.src files.browser, { read: false }
@ -67,6 +66,7 @@ gulp.task 'build_browser', ->
debug: true
.pipe rename
extname: ".js"
dirname: "./"
.pipe gulp.dest './build/test/'
gulp.task 'build_node', ->
@ -94,9 +94,6 @@ gulp.task 'lint', ->
}
.pipe coffeelint.reporter()
gulp.task 'phantom_watch', ['phantom_test'], ->
gulp.watch files.all, ['phantom_test']
gulp.task 'literate', ->
gulp.src files.examples
.pipe ljs { code : true }
@ -110,13 +107,8 @@ gulp.task 'codo', [], ()->
command = './node_modules/codo/bin/codo -o "./doc" --name "yjs" --readme "README.md" --undocumented false --private true --title "yjs API" ./lib - LICENSE.txt '
run(command).exec()
gulp.task 'phantom_test', ['build_browser'], ()->
gulp.src 'build/test/index.html'
.pipe mochaPhantomJS()
gulp.task 'clean', ->
gulp.src ['./build/{browser,test,node}/**/*.{js,map}','./doc/'], { read: false }
.pipe rimraf()
gulp.task 'default', ['clean','build'], ->

View File

@ -58,4 +58,4 @@ adaptConnector = (connector, engine, HB, execution_listener)->
engine.applyOp op
module.exports = adaptConnector
module.exports = adaptConnector

View File

@ -46,6 +46,10 @@ module.exports =
@sent_hb_to_all_users = false
@is_initialized = true
onUserEvent: (f)->
@connections_listeners ?= []
@connections_listeners.push f
isRoleMaster: ->
@role is "master"
@ -66,6 +70,12 @@ module.exports =
userLeft: (user)->
delete @connections[user]
@findNewSyncTarget()
for f in @connections_listeners
f {
action: "userLeft"
user: user
}
userJoined: (user, role)->
if not role?
@ -81,6 +91,12 @@ module.exports =
# TODO: What if there are two masters? Prevent sending everything two times!
@performSyncWithMaster user
for f in @connections_listeners
f {
action: "userJoined"
user: user
role: role
}
#
# Execute a function _when_ we are connected. If not connected, wait until connected.
@ -133,7 +149,7 @@ module.exports =
_hb = []
for o in hb
_hb.push o
if _hb.length > 30
if _hb.length > 10
@broadcast
sync_step: "applyHB_"
data: _hb
@ -158,7 +174,7 @@ module.exports =
_hb = []
for o in hb
_hb.push o
if _hb.length > 30
if _hb.length > 10
@broadcast
sync_step: "applyHB_"
data: _hb
@ -207,7 +223,7 @@ module.exports =
for o in hb
_hb.push o
if _hb.length > 30
if _hb.length > 10
sendApplyHB
sync_step: "applyHB_"
data: _hb

82
lib/ObjectType.coffee Normal file
View File

@ -0,0 +1,82 @@
class YObject
constructor: (@_object = {})->
if @_object.constructor is Object
for name, val of @_object
if val.constructor is Object
@_object[name] = new YObject(val)
else
throw new Error "Y.Object accepts Json Objects only"
_name: "Object"
_getModel: (types, ops)->
if not @_model?
@_model = new ops.MapManager(@).execute()
for n,o of @_object
@_model.val n, o
delete @_object
@_model
_setModel: (@_model)->
delete @_object
observe: (f)->
@_model.observe f
@
unobserve: (f)->
@_model.unobserve f
@
#
# @overload val()
# Get this as a Json object.
# @return [Json]
#
# @overload val(name)
# Get value of a property.
# @param {String} name Name of the object property.
# @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 [Object Type] This object. (supports chaining)
#
val: (name, content)->
if @_model?
@_model.val.apply @_model, arguments
else
if content?
@_object[name] = content
else if name?
@_object[name]
else
res = {}
for n,v of @_object
res[n] = v
res
delete: (name)->
@_model.delete(name)
@
if window?
if window.Y?
window.Y.Object = YObject
else
throw new Error "You must first import Y!"
if module?
module.exports = YObject

View File

@ -1,13 +1,13 @@
module.exports = (HB)->
module.exports = ()->
# @see Engine.parse
types = {}
ops = {}
execution_listener = []
#
# @private
# @abstract
# @nodoc
# A generic interface to operations.
# A generic interface to ops.
#
# An operation has the following methods:
# * _encode: encodes an operation (needed only if instance of this operation is sent).
@ -16,21 +16,59 @@ module.exports = (HB)->
#
# Furthermore an encodable operation has a parser. We extend the parser object in order to parse encoded operations.
#
class types.Operation
class ops.Operation
#
# @param {Object} uid A unique identifier.
# If uid is undefined, a new uid will be created before at the end of the execution sequence
#
constructor: (uid)->
constructor: (custom_type, uid, content, content_operations)->
if custom_type?
@custom_type = custom_type
@is_deleted = false
@garbage_collected = false
@event_listeners = [] # TODO: rename to observers or sth like that
if uid?
@uid = uid
# see encode to see, why we are doing it this way
if content is undefined
# nop
else if content? and content.creator?
@saveOperation 'content', content
else
@content = content
if content_operations?
@content_operations = {}
for name, op of content_operations
@saveOperation name, op, 'content_operations'
type: "Operation"
getContent: (name)->
if @content?
if @content.getCustomType?
@content.getCustomType()
else if @content.constructor is Object
if name?
if @content[name]?
@content[name]
else
@content_operations[name].getCustomType()
else
content = {}
for n,v of @content
content[n] = v
if @content_operations?
for n,v of @content_operations
v = v.getCustomType()
content[n] = v
content
else
@content
else
@content
retrieveSub: ()->
throw new Error "sub properties are not enable on this operation type!"
@ -46,7 +84,7 @@ module.exports = (HB)->
# @see Operation.observe
#
# @overload unobserve(event, f)
# @param f {Function} The function that you want to delete
# @param f {Function} The function that you want to delete
unobserve: (f)->
@event_listeners = @event_listeners.filter (g)->
f isnt g
@ -60,7 +98,7 @@ module.exports = (HB)->
@event_listeners = []
delete: ()->
(new types.Delete undefined, @).execute()
(new ops.Delete undefined, @).execute()
null
#
@ -68,7 +106,11 @@ module.exports = (HB)->
# TODO: Do something with timeouts. You don't want this to fire for every operation (e.g. insert).
# TODO: do you need callEvent+forwardEvent? Only one suffices probably
callEvent: ()->
@forwardEvent @, arguments...
if @custom_type?
callon = @getCustomType()
else
callon = @
@forwardEvent callon, arguments...
#
# Fire an event and specify in which context the listener is called (set 'this').
@ -86,11 +128,11 @@ module.exports = (HB)->
@is_deleted = true
if garbagecollect
@garbage_collected = true
HB.addToGarbageCollector @
@HB.addToGarbageCollector @
cleanup: ()->
#console.log "cleanup: #{@type}"
HB.removeOperation @
@HB.removeOperation @
@deleteAllObservers()
#
@ -131,17 +173,20 @@ module.exports = (HB)->
# Notify the all the listeners.
#
execute: ()->
@is_executed = true
if not @uid?
# When this operation was created without a uid, then set it here.
# 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()
if not @uid.noOperation?
HB.addOperation @
for l in execution_listener
l @_encode()
@
if @validateSavedOperations()
@is_executed = true
if not @uid?
# When this operation was created without a uid, then set it here.
# 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()
if not @uid.noOperation?
@HB.addOperation @
for l in execution_listener
l @_encode()
@
else
false
#
# @private
@ -161,8 +206,9 @@ module.exports = (HB)->
# @param {String} name The name of the operation. After calling this function op is accessible via this[name].
# @param {Operation} op An Operation object
#
saveOperation: (name, op)->
saveOperation: (name, op, base = "this")->
if op? and op._getModel?
op = op._getModel(@custom_types, @operations)
#
# Every instance of $Operation must have an $execute function.
# We use duck-typing to check if op is instantiated since there
@ -173,11 +219,20 @@ module.exports = (HB)->
else if op.execute? or not (op.op_number? and op.creator?)
# is instantiated, or op is string. Currently "Delimiter" is saved as string
# (in combination with @parent you can retrieve the delimiter..)
@[name] = op
if base is "this"
@[name] = op
else
dest = @[base]
paths = name.split("/")
last_path = paths.pop()
for path in paths
dest = dest[path]
dest[last_path] = op
else
# not initialized. Do it when calling $validateSavedOperations()
@unchecked ?= {}
@unchecked[name] = op
@unchecked[base] ?= {}
@unchecked[base][name] = op
#
# @private
@ -188,32 +243,84 @@ module.exports = (HB)->
#
validateSavedOperations: ()->
uninstantiated = {}
success = @
for name, op_uid of @unchecked
op = HB.getOperation op_uid
if op
@[name] = op
else
uninstantiated[name] = op_uid
success = false
delete @unchecked
success = true
for base_name, base of @unchecked
for name, op_uid of base
op = @HB.getOperation op_uid
if op
if base_name is "this"
@[name] = op
else
dest = @[base_name]
paths = name.split("/")
last_path = paths.pop()
for path in paths
dest = dest[path]
dest[last_path] = op
else
uninstantiated[base_name] ?= {}
uninstantiated[base_name][name] = op_uid
success = false
if not success
@unchecked = uninstantiated
success
return false
else
delete @unchecked
return @
getCustomType: ()->
if not @custom_type?
# throw new Error "This operation was not initialized with a custom type"
@
else
if @custom_type.constructor is String
# has not been initialized yet (only the name is specified)
Type = @custom_types
for t in @custom_type.split(".")
Type = Type[t]
@custom_type = new Type()
@custom_type._setModel @
@custom_type
#
# @private
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: (json = {})->
json.type = @type
json.uid = @getUid()
if @custom_type?
if @custom_type.constructor is String
json.custom_type = @custom_type
else
json.custom_type = @custom_type._name
if @content?.getUid?
json.content = @content.getUid()
else
json.content = @content
if @content_operations?
operations = {}
for n,o of @content_operations
if o._getModel?
o = o._getModel(@custom_types, @operations)
operations[n] = o.getUid()
json.content_operations = operations
json
#
# @nodoc
# A simple Delete-type operation that deletes an operation.
#
class types.Delete extends types.Operation
class ops.Delete extends ops.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} deletes UID or reference of the operation that this to be deleted.
#
constructor: (uid, deletes)->
constructor: (custom_type, uid, deletes)->
@saveOperation 'deletes', deletes
super uid
super custom_type, uid
type: "Delete"
@ -245,12 +352,12 @@ module.exports = (HB)->
#
# Define how to parse Delete operations.
#
types.Delete.parse = (o)->
ops.Delete.parse = (o)->
{
'uid' : uid
'deletes': deletes_uid
} = o
new this(uid, deletes_uid)
new this(null, uid, deletes_uid)
#
# @nodoc
@ -259,24 +366,17 @@ module.exports = (HB)->
# An insert operation is always positioned between two other insert operations.
# Internally this is realized as associative lists, whereby each insert operation has a predecessor and a successor.
# For the sake of efficiency we maintain two lists:
# - The short-list (abbrev. sl) maintains only the operations that are not deleted
# - The short-list (abbrev. sl) maintains only the operations that are not deleted (unimplemented, good idea?)
# - The complete-list (abbrev. cl) maintains all operations
#
class types.Insert extends types.Operation
class ops.Insert extends ops.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: (content, uid, prev_cl, next_cl, origin, parent)->
# see encode to see, why we are doing it this way
if content is undefined
# nop
else if content? and content.creator?
@saveOperation 'content', content
else
@content = content
constructor: (custom_type, content, content_operations, parent, uid, prev_cl, next_cl, origin)->
@saveOperation 'parent', parent
@saveOperation 'prev_cl', prev_cl
@saveOperation 'next_cl', next_cl
@ -284,12 +384,33 @@ module.exports = (HB)->
@saveOperation 'origin', origin
else
@saveOperation 'origin', prev_cl
super uid
super custom_type, uid, content, content_operations
type: "Insert"
val: ()->
@content
@getContent()
getNext: (i=1)->
n = @
while i > 0 and n.next_cl?
n = n.next_cl
if not n.is_deleted
i--
if n.is_deleted
null
n
getPrev: (i=1)->
n = @
while i > 0 and n.prev_cl?
n = n.prev_cl
if not n.is_deleted
i--
if n.is_deleted
null
else
n
#
# set content to null and other stuff
@ -298,7 +419,7 @@ module.exports = (HB)->
applyDelete: (o)->
@deleted_by ?= []
callLater = false
if @parent? and not @isDeleted() and o? # o? : if not o?, then the delimiter deleted this Insertion. Furthermore, it would be wrong to call it. TODO: make this more expressive and save
if @parent? and not @is_deleted and o? # o? : if not o?, then the delimiter deleted this Insertion. Furthermore, it would be wrong to call it. TODO: make this more expressive and save
# call iff wasn't deleted earlyer
callLater = true
if o?
@ -308,17 +429,11 @@ module.exports = (HB)->
garbagecollect = true
super garbagecollect
if callLater
@callOperationSpecificDeleteEvents(o)
if @prev_cl?.isDeleted()
@parent.callOperationSpecificDeleteEvents(this, o)
if @prev_cl? and @prev_cl.isDeleted()
# garbage collect prev_cl
@prev_cl.applyDelete()
# delete content
if @content instanceof types.Operation
@content.applyDelete()
delete @content
cleanup: ()->
if @next_cl.isDeleted()
# delete all ops that delete this insertion
@ -335,6 +450,18 @@ module.exports = (HB)->
# reconnect left/right
@prev_cl.next_cl = @next_cl
@next_cl.prev_cl = @prev_cl
# delete content
# - we must not do this in applyDelete, because this would lead to inconsistencies
# (e.g. the following operation order must be invertible :
# Insert refers to content, then the content is deleted)
# Therefore, we have to do this in the cleanup
# * NODE: We never delete Insertions!
if @content instanceof ops.Operation and not (@content instanceof ops.Insert)
@content.referenced_by--
if @content.referenced_by <= 0 and not @content.is_deleted
@content.applyDelete()
delete @content
super
# else
# Someone inserted something in the meantime.
@ -361,8 +488,10 @@ module.exports = (HB)->
if not @validateSavedOperations()
return false
else
if @content instanceof types.Operation
if @content instanceof ops.Operation
@content.insert_parent = @ # TODO: this is probably not necessary and only nice for debugging
@content.referenced_by ?= 0
@content.referenced_by++
if @parent?
if not @prev_cl?
@prev_cl = @parent.beginning
@ -422,27 +551,9 @@ module.exports = (HB)->
@setParent @prev_cl.getParent() # do Insertions always have a parent?
super # notify the execution_listeners
@callOperationSpecificInsertEvents()
@parent.callOperationSpecificInsertEvents(this)
@
callOperationSpecificInsertEvents: ()->
@parent?.callEvent [
type: "insert"
position: @getPosition()
object: @parent
changedBy: @uid.creator
value: @content
]
callOperationSpecificDeleteEvents: (o)->
@parent.callEvent [
type: "delete"
position: @getPosition()
object: @parent # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
length: 1
changedBy: o.uid.creator
]
#
# Compute the position of this operation.
#
@ -450,7 +561,7 @@ module.exports = (HB)->
position = 0
prev = @prev_cl
while true
if prev instanceof types.Delimiter
if prev instanceof ops.Delimiter
break
if not prev.isDeleted()
position++
@ -461,80 +572,31 @@ module.exports = (HB)->
# Convert all relevant information of this operation to the json-format.
# This result can be send to other clients.
#
_encode: ()->
json =
{
'type': @type
'uid' : @getUid()
'prev': @prev_cl.getUid()
'next': @next_cl.getUid()
'parent': @parent.getUid()
}
_encode: (json = {})->
json.prev = @prev_cl.getUid()
json.next = @next_cl.getUid()
if @origin.type is "Delimiter"
json.origin = "Delimiter"
else if @origin isnt @prev_cl
json.origin = @origin.getUid()
if @content?.getUid?
json['content'] = @content.getUid()
else
json['content'] = JSON.stringify @content
json
# if not (json.prev? and json.next?)
json.parent = @parent.getUid()
types.Insert.parse = (json)->
super json
ops.Insert.parse = (json)->
{
'content' : content
'content_operations' : content_operations
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'parent' : parent
} = json
if typeof content is "string"
content = JSON.parse(content)
new this content, uid, prev, next, origin, parent
#
# @nodoc
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
#
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)->
super uid
type: "ImmutableObject"
#
# @return [String] The content of this operation.
#
val : ()->
@content
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json = {
'type': @type
'uid' : @getUid()
'content' : @content
}
json
types.ImmutableObject.parse = (json)->
{
'uid' : uid
'content' : content
} = json
new this(uid, content)
new this null, content, content_operations, parent, uid, prev, next, origin
#
# @nodoc
@ -542,7 +604,7 @@ 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 types.Delimiter extends types.Operation
class ops.Delimiter extends ops.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)
@ -552,7 +614,7 @@ module.exports = (HB)->
@saveOperation 'prev_cl', prev_cl
@saveOperation 'next_cl', next_cl
@saveOperation 'origin', prev_cl
super {noOperation: true}
super null, {noOperation: true}
type: "Delimiter"
@ -601,7 +663,7 @@ module.exports = (HB)->
'next' : @next_cl?.getUid()
}
types.Delimiter.parse = (json)->
ops.Delimiter.parse = (json)->
{
'uid' : uid
'prev' : prev
@ -611,10 +673,6 @@ module.exports = (HB)->
# This is what this module exports after initializing it with the HistoryBuffer
{
'types' : types
'operations' : ops
'execution_listener' : execution_listener
}

View File

@ -0,0 +1,527 @@
basic_ops_uninitialized = require "./Basic"
module.exports = ()->
basic_ops = basic_ops_uninitialized()
ops = basic_ops.operations
#
# @nodoc
# Manages map like objects. E.g. Json-Type and XML attributes.
#
class ops.MapManager extends ops.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (custom_type, uid, content, content_operations)->
@_map = {}
super custom_type, uid, content, content_operations
type: "MapManager"
applyDelete: ()->
for name,p of @_map
p.applyDelete()
super()
cleanup: ()->
super()
map: (f)->
for n,v of @_map
f(n,v)
undefined
#
# @see JsonOperations.val
#
val: (name, content)->
if arguments.length > 1
if content? and content._getModel?
rep = content._getModel(@custom_types, @operations)
else
rep = content
@retrieveSub(name).replace rep
@getCustomType()
else if name?
prop = @_map[name]
if prop? and not prop.isContentDeleted()
res = prop.val()
if res instanceof ops.Operation
res.getCustomType()
else
res
else
undefined
else
result = {}
for name,o of @_map
if not o.isContentDeleted()
result[name] = o.val()
result
delete: (name)->
@_map[name]?.deleteContent()
@
retrieveSub: (property_name)->
if not @_map[property_name]?
event_properties =
name: property_name
event_this = @
rm_uid =
noOperation: true
sub: property_name
alt: @
rm = new ops.ReplaceManager null, 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]
ops.MapManager.parse = (json)->
{
'uid' : uid
'custom_type' : custom_type
'content' : content
'content_operations' : content_operations
} = json
new this(custom_type, uid, content, content_operations)
#
# @nodoc
# Manages a list of Insert-type operations.
#
class ops.ListManager extends ops.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: (custom_type, uid, content, content_operations)->
@beginning = new ops.Delimiter undefined, undefined
@end = new ops.Delimiter @beginning, undefined
@beginning.next_cl = @end
@beginning.execute()
@end.execute()
super custom_type, uid, content, content_operations
type: "ListManager"
applyDelete: ()->
o = @beginning
while o?
o.applyDelete()
o = o.next_cl
super()
cleanup: ()->
super()
toJson: (transform_to_value = false)->
val = @val()
for i, o in val
if o instanceof ops.Object
o.toJson(transform_to_value)
else if o instanceof ops.ListManager
o.toJson(transform_to_value)
else if transform_to_value and o instanceof ops.Operation
o.val()
else
o
#
# @private
# @see Operation.execute
#
execute: ()->
if @validateSavedOperations()
@beginning.setParent @
@end.setParent @
super
else
false
# Get the element previous to the delemiter at the end
getLastOperation: ()->
@end.prev_cl
# similar to the above
getFirstOperation: ()->
@beginning.next_cl
# Transforms the the list to an array
# Doesn't return left-right delimiter.
toArray: ()->
o = @beginning.next_cl
result = []
while o isnt @end
if not o.is_deleted
result.push o.val()
o = o.next_cl
result
map: (f)->
o = @beginning.next_cl
result = []
while o isnt @end
if not o.is_deleted
result.push f(o)
o = o.next_cl
result
fold: (init, f)->
o = @beginning.next_cl
while o isnt @end
if not o.is_deleted
init = f(init, o)
o = o.next_cl
init
val: (pos)->
if pos?
o = @getOperationByPosition(pos+1)
if not (o instanceof ops.Delimiter)
o.val()
else
throw new Error "this position does not exist"
else
@toArray()
ref: (pos)->
if pos?
o = @getOperationByPosition(pos+1)
if not (o instanceof ops.Delimiter)
o
else
null
# throw new Error "this position does not exist"
else
throw new Error "you must specify a position parameter"
#
# Retrieves the x-th not deleted element.
# e.g. "abc" : the 1th character is "a"
# the 0th character is the left Delimiter
#
getOperationByPosition: (position)->
o = @beginning
while true
# find the i-th op
if o instanceof ops.Delimiter and o.prev_cl?
# the user or you gave a position parameter that is to big
# for the current array. Therefore we reach a Delimiter.
# Then, we'll just return the last character.
o = o.prev_cl
while o.isDeleted() and o.prev_cl?
o = o.prev_cl
break
if position <= 0 and not o.isDeleted()
break
o = o.next_cl
if not o.isDeleted()
position -= 1
o
push: (content)->
@insertAfter @end.prev_cl, [content]
insertAfter: (left, contents)->
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
# TODO: always expect an array as content. Then you can combine this with the other option (else)
if contents instanceof ops.Operation
(new ops.Insert null, content, null, undefined, undefined, left, right).execute()
else
for c in contents
if c? and c._name? and c._getModel?
c = c._getModel(@custom_types, @operations)
tmp = (new ops.Insert null, c, null, undefined, undefined, left, right).execute()
left = tmp
@
#
# Inserts an array of content into this list.
# @Note: This expects an array as content!
#
# @return {ListManager Type} This String object.
#
insert: (position, contents)->
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, contents
#
# Deletes a part of the word.
#
# @return {ListManager Type} This String object
#
delete: (position, length = 1)->
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 ops.Delimiter
break
d = (new ops.Delete null, undefined, o).execute()
o = o.next_cl
while (not (o instanceof ops.Delimiter)) and o.isDeleted()
o = o.next_cl
delete_ops.push d._encode()
@
callOperationSpecificInsertEvents: (op)->
getContentType = (content)->
if content instanceof ops.Operation
content.getCustomType()
else
content
@callEvent [
type: "insert"
reference: op
position: op.getPosition()
object: @getCustomType()
changedBy: op.uid.creator
value: getContentType op.val()
]
callOperationSpecificDeleteEvents: (op, del_op)->
@callEvent [
type: "delete"
reference: op
position: op.getPosition()
object: @getCustomType() # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
length: 1
changedBy: del_op.uid.creator
oldValue: op.val()
]
ops.ListManager.parse = (json)->
{
'uid' : uid
'custom_type': custom_type
'content' : content
'content_operations' : content_operations
} = json
new this(custom_type, uid, content, content_operations)
class ops.Composition extends ops.ListManager
constructor: (custom_type, @_composition_value, composition_value_operations, uid, tmp_composition_ref)->
# we can't use @seveOperation 'composition_ref', tmp_composition_ref here,
# because then there is a "loop" (insertion refers to parent, refers to insertion..)
# This is why we have to check in @callOperationSpecificInsertEvents until we find it
super custom_type, uid
if tmp_composition_ref?
@tmp_composition_ref = tmp_composition_ref
else
@composition_ref = @end.prev_cl
if composition_value_operations?
@composition_value_operations = {}
for n,o of composition_value_operations
@saveOperation n, o, '_composition_value'
type: "Composition"
#
# @private
# @see Operation.execute
#
execute: ()->
if @validateSavedOperations()
@getCustomType()._setCompositionValue @_composition_value
delete @_composition_value
super
else
false
#
# This is called, when the Insert-operation was successfully executed.
#
callOperationSpecificInsertEvents: (op)->
if @tmp_composition_ref?
if op.uid.creator is @tmp_composition_ref.creator and op.uid.op_number is @tmp_composition_ref.op_number
@composition_ref = op
delete @tmp_composition_ref
op = op.next_cl
if op is @end
return
else
return
o = @end.prev_cl
while o isnt op
@getCustomType()._unapply o.undo_delta
o = o.prev_cl
while o isnt @end
o.undo_delta = @getCustomType()._apply o.val()
o = o.next_cl
@composition_ref = @end.prev_cl
@callEvent [
type: "update"
changedBy: op.uid.creator
newValue: @val()
]
callOperationSpecificDeleteEvents: (op, del_op)->
return
#
# Create a new Delta
# - inserts new Content at the end of the list
# - updates the composition_value
# - updates the composition_ref
#
# @param delta The delta that is applied to the composition_value
#
applyDelta: (delta, operations)->
(new ops.Insert null, delta, operations, @, null, @end.prev_cl, @end).execute()
undefined
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: (json = {})->
custom = @getCustomType()._getCompositionValue()
json.composition_value = custom.composition_value
if custom.composition_value_operations?
json.composition_value_operations = {}
for n,o of custom.composition_value_operations
json.composition_value_operations[n] = o.getUid()
if @composition_ref?
json.composition_ref = @composition_ref.getUid()
else
json.composition_ref = @tmp_composition_ref
super json
ops.Composition.parse = (json)->
{
'uid' : uid
'custom_type': custom_type
'composition_value' : composition_value
'composition_value_operations' : composition_value_operations
'composition_ref' : composition_ref
} = json
new this(custom_type, composition_value, composition_value_operations, uid, composition_ref)
#
# @nodoc
# Adds support for replace. The ReplaceManager manages Replaceable operations.
# Each Replaceable holds a value that is now replaceable.
#
# The TextType-type has implemented support for replace
# @see TextType
#
class ops.ReplaceManager extends ops.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
# @param {Operation} initial_content Initialize this with a Replaceable that holds the initial_content.
# @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: (custom_type, @event_properties, @event_this, uid)->
if not @event_properties['object']?
@event_properties['object'] = @event_this.getCustomType()
super custom_type, uid
type: "ReplaceManager"
#
# This doesn't throw the same events as the ListManager. Therefore, the
# Replaceables also not throw the same events.
# So, ReplaceManager and ListManager both implement
# these functions that are called when an Insertion is executed (at the end).
#
#
callEventDecorator: (events)->
if not @isDeleted()
for event in events
for name,prop of @event_properties
event[name] = prop
@event_this.callEvent events
undefined
#
# This is called, when the Insert-type was successfully executed.
# TODO: consider doing this in a more consistent manner. This could also be
# done with execute. But currently, there are no specital Insert-ops for ListManager.
#
callOperationSpecificInsertEvents: (op)->
if op.next_cl.type is "Delimiter" and op.prev_cl.type isnt "Delimiter"
# this replaces another Replaceable
if not op.is_deleted # When this is received from the HB, this could already be deleted!
old_value = op.prev_cl.val()
@callEventDecorator [
type: "update"
changedBy: op.uid.creator
oldValue: old_value
]
op.prev_cl.applyDelete()
else if op.next_cl.type isnt "Delimiter"
# This won't be recognized by the user, because another
# concurrent operation is set as the current value of the RM
op.applyDelete()
else # prev _and_ next are Delimiters. This is the first created Replaceable in the RM
@callEventDecorator [
type: "add"
changedBy: op.uid.creator
]
undefined
callOperationSpecificDeleteEvents: (op, del_op)->
if op.next_cl.type is "Delimiter"
@callEventDecorator [
type: "delete"
changedBy: del_op.uid.creator
oldValue: op.val()
]
#
# Replace the existing word with a new word.
#
# @param content {Operation} The new value of this ReplaceManager.
# @param replaceable_uid {UID} Optional: Unique id of the Replaceable that is created
#
replace: (content, replaceable_uid)->
o = @getLastOperation()
relp = (new ops.Insert null, content, null, @, replaceable_uid, o, o.next_cl).execute()
# TODO: delete repl (for debugging)
undefined
isContentDeleted: ()->
@getLastOperation().isDeleted()
deleteContent: ()->
last_op = @getLastOperation()
if (not last_op.isDeleted()) and last_op.type isnt "Delimiter"
(new ops.Delete null, undefined, @getLastOperation().uid).execute()
undefined
#
# Get the value of this
# @return {String}
#
val: ()->
o = @getLastOperation()
#if o instanceof ops.Delimiter
# throw new Error "Replace Manager doesn't contain anything."
o.val?() # ? - for the case that (currently) the RM does not contain anything (then o is a Delimiter)
basic_ops

View File

@ -1,147 +0,0 @@
text_types_uninitialized = require "./TextTypes"
module.exports = (HB)->
text_types = text_types_uninitialized HB
types = text_types.types
#
# Manages Object-like values.
#
class types.Object extends types.MapManager
#
# Identifies this class.
# Use it to check whether this is a json-type or something else.
#
# @example
# var x = y.val('unknown')
# if (x.type === "Object") {
# console.log JSON.stringify(x.toJson())
# }
#
type: "Object"
applyDelete: ()->
super()
cleanup: ()->
super()
#
# Transform this to a Json. If your browser supports Object.observe it will be transformed automatically when a change arrives.
# Otherwise you will loose all the sharing-abilities (the new object will be a deep clone)!
# @return {Json}
#
# TODO: at the moment you don't consider changing of properties.
# E.g.: let x = {a:[]}. Then x.a.push 1 wouldn't change anything
#
toJson: (transform_to_value = false)->
if not @bound_json? or not Object.observe? or true # TODO: currently, you are not watching mutable strings for changes, and, therefore, the @bound_json is not updated. TODO TODO wuawuawua easy
val = @val()
json = {}
for name, o of val
if o instanceof types.Object
json[name] = o.toJson(transform_to_value)
else if o instanceof types.ListManager
json[name] = o.toJson(transform_to_value)
else if transform_to_value and o instanceof types.Operation
json[name] = o.val()
else
json[name] = o
@bound_json = json
if Object.observe?
that = @
Object.observe @bound_json, (events)->
for event in events
if not event.changedBy? and (event.type is "add" or event.type = "update")
# this event is not created by Y.
that.val(event.name, event.object[event.name])
@observe (events)->
for event in events
if event.created_ isnt HB.getUserId()
notifier = Object.getNotifier(that.bound_json)
oldVal = that.bound_json[event.name]
if oldVal?
notifier.performChange 'update', ()->
that.bound_json[event.name] = that.val(event.name)
, that.bound_json
notifier.notify
object: that.bound_json
type: 'update'
name: event.name
oldValue: oldVal
changedBy: event.changedBy
else
notifier.performChange 'add', ()->
that.bound_json[event.name] = that.val(event.name)
, that.bound_json
notifier.notify
object: that.bound_json
type: 'add'
name: event.name
oldValue: oldVal
changedBy:event.changedBy
@bound_json
#
# @overload val()
# Get this as a Json object.
# @return [Json]
#
# @overload val(name)
# Get value of a property.
# @param {String} name Name of the object property.
# @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 [Object Type] This object. (supports chaining)
#
val: (name, content)->
if name? and arguments.length > 1
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
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Y."
else
super name, content
else # is this even necessary ? I have to define every type anyway.. (see Number type below)
super name
#
# @private
#
_encode: ()->
{
'type' : @type
'uid' : @getUid()
}
types.Object.parse = (json)->
{
'uid' : uid
} = json
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.Number = {}
types.Number.create = (content)->
content
text_types

View File

@ -1,493 +0,0 @@
basic_types_uninitialized = require "./BasicTypes"
module.exports = (HB)->
basic_types = basic_types_uninitialized HB
types = basic_types.types
#
# @nodoc
# Manages map like objects. E.g. Json-Type and XML attributes.
#
class types.MapManager extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (uid)->
@map = {}
super uid
type: "MapManager"
applyDelete: ()->
for name,p of @map
p.applyDelete()
super()
cleanup: ()->
super()
#
# @see JsonTypes.val
#
val: (name, content)->
if arguments.length > 1
@retrieveSub(name).replace content
@
else if name?
prop = @map[name]
if prop? and not prop.isContentDeleted()
prop.val()
else
undefined
else
result = {}
for name,o of @map
if not o.isContentDeleted()
result[name] = o.val()
result
delete: (name)->
@map[name]?.deleteContent()
@
retrieveSub: (property_name)->
if not @map[property_name]?
event_properties =
name: property_name
event_this = @
rm_uid =
noOperation: true
sub: property_name
alt: @
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 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 = new types.Delimiter undefined, undefined
@end = new types.Delimiter @beginning, undefined
@beginning.next_cl = @end
@beginning.execute()
@end.execute()
super uid
type: "ListManager"
applyDelete: ()->
o = @end
while o?
o.applyDelete()
o = o.prev_cl
super()
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.ListManager
o.toJson(transform_to_value)
else if transform_to_value and o instanceof types.Operation
o.val()
else
o
#
# @private
# @see Operation.execute
#
execute: ()->
if @validateSavedOperations()
@beginning.setParent @
@end.setParent @
super
else
false
# Get the element previous to the delemiter at the end
getLastOperation: ()->
@end.prev_cl
# similar to the above
getFirstOperation: ()->
@beginning.next_cl
# Transforms the the list to an array
# Doesn't return left-right delimiter.
toArray: ()->
o = @beginning.next_cl
result = []
while o isnt @end
if not o.is_deleted
result.push o
o = o.next_cl
result
map: (f)->
o = @beginning.next_cl
result = []
while o isnt @end
if not o.is_deleted
result.push f(o)
o = o.next_cl
result
fold: (init, f)->
o = @beginning.next_cl
while o isnt @end
if not o.is_deleted
init = f(init, o)
o = o.next_cl
init
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
@toArray()
#
# Retrieves the x-th not deleted element.
# e.g. "abc" : the 1th character is "a"
# the 0th character is the left Delimiter
#
getOperationByPosition: (position)->
o = @beginning
while true
# find the i-th op
if o instanceof types.Delimiter and o.prev_cl?
# the user or you gave a position parameter that is to big
# for the current array. Therefore we reach a Delimiter.
# Then, we'll just return the last character.
o = o.prev_cl
while o.isDeleted() and o.prev_cl?
o = o.prev_cl
break
if position <= 0 and not o.isDeleted()
break
o = o.next_cl
if not o.isDeleted()
position -= 1
o
push: (content)->
@insertAfter @end.prev_cl, 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 Y."
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 instanceof types.Operation
(new types.Insert content, undefined, left, right).execute()
else
for c in content
tmp = (new types.Insert createContent(c, options), undefined, left, right).execute()
left = tmp
@
#
# Inserts a string into the word.
#
# @return {ListManager 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
#
# Deletes a part of the word.
#
# @return {ListManager Type} This String object
#
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 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.ListManager.parse = (json)->
{
'uid' : uid
} = json
new this(uid)
types.Array = ()->
types.Array.create = (content, mutable)->
if (mutable is "mutable")
list = new types.ListManager().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\"!!"
#
# @nodoc
# Adds support for replace. The ReplaceManager manages Replaceable operations.
# Each Replaceable holds a value that is now replaceable.
#
# The TextType-type has implemented support for replace
# @see TextType
#
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
# @param {Operation} initial_content Initialize this with a Replaceable that holds the initial_content.
# @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)->
if not @event_properties['object']?
@event_properties['object'] = @event_this
super uid, beginning, end
type: "ReplaceManager"
applyDelete: ()->
o = @beginning
while o?
o.applyDelete()
o = o.next_cl
super()
cleanup: ()->
super()
#
# This doesn't throw the same events as the ListManager. Therefore, the
# Replaceables also not throw the same events.
# So, ReplaceManager and ListManager both implement
# these functions that are called when an Insertion is executed (at the end).
#
#
callEventDecorator: (events)->
if not @isDeleted()
for event in events
for name,prop of @event_properties
event[name] = prop
@event_this.callEvent events
undefined
#
# Replace the existing word with a new word.
#
# @param content {Operation} The new value of this ReplaceManager.
# @param replaceable_uid {UID} Optional: Unique id of the Replaceable that is created
#
replace: (content, replaceable_uid)->
o = @getLastOperation()
relp = (new types.Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
# TODO: delete repl (for debugging)
undefined
isContentDeleted: ()->
@getLastOperation().isDeleted()
deleteContent: ()->
(new types.Delete undefined, @getLastOperation().uid).execute()
undefined
#
# Get the value of this
# @return {String}
#
val: ()->
o = @getLastOperation()
#if o instanceof types.Delimiter
# throw new Error "Replace Manager doesn't contain anything."
o.val?() # ? - for the case that (currently) the RM does not contain anything (then o is a Delimiter)
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json =
{
'type': @type
'uid' : @getUid()
'beginning' : @beginning.getUid()
'end' : @end.getUid()
}
json
#
# @nodoc
# The ReplaceManager manages Replaceables.
# @see ReplaceManager
#
class types.Replaceable extends types.Insert
#
# @param {Operation} content The value that this Replaceable holds.
# @param {ReplaceManager} parent Used to replace this Replaceable with another one.
# @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 'parent', parent
super content, uid, prev, next, origin # Parent is already saved by Replaceable
@is_deleted = is_deleted
type: "Replaceable"
#
# Return the content that this operation holds.
#
val: ()->
@content
applyDelete: ()->
res = super
if @content?
if @next_cl.type isnt "Delimiter"
@content.deleteAllObservers?()
@content.applyDelete?()
@content.dontSync?()
@content = null
res
cleanup: ()->
super
#
# This is called, when the Insert-type was successfully executed.
# TODO: consider doing this in a more consistent manner. This could also be
# done with execute. But currently, there are no specital Insert-types for ListManager.
#
callOperationSpecificInsertEvents: ()->
if @next_cl.type is "Delimiter" and @prev_cl.type isnt "Delimiter"
# this replaces another Replaceable
if not @is_deleted # When this is received from the HB, this could already be deleted!
old_value = @prev_cl.content
@parent.callEventDecorator [
type: "update"
changedBy: @uid.creator
oldValue: old_value
]
@prev_cl.applyDelete()
else if @next_cl.type isnt "Delimiter"
# This won't be recognized by the user, because another
# concurrent operation is set as the current value of the RM
@applyDelete()
else # prev _and_ next are Delimiters. This is the first created Replaceable in the RM
@parent.callEventDecorator [
type: "add"
changedBy: @uid.creator
]
undefined
callOperationSpecificDeleteEvents: (o)->
if @next_cl.type is "Delimiter"
@parent.callEventDecorator [
type: "delete"
changedBy: o.uid.creator
oldValue: @content
]
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json =
{
'type': @type
'parent' : @parent.getUid()
'prev': @prev_cl.getUid()
'next': @next_cl.getUid()
'uid' : @getUid()
'is_deleted': @is_deleted
}
if @origin.type is "Delimiter"
json.origin = "Delimiter"
else if @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
types.Replaceable.parse = (json)->
{
'content' : content
'parent' : parent
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'is_deleted': is_deleted
} = json
new this(content, parent, uid, prev, next, origin, is_deleted)
basic_types

View File

@ -1,309 +0,0 @@
structured_types_uninitialized = require "./StructuredTypes"
module.exports = (HB)->
structured_types = structured_types_uninitialized HB
types = structured_types.types
parser = structured_types.parser
#
# 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.ListManager
#
# @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 = y.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.
#
val: ()->
@fold "", (left, o)->
left + o.val()
#
# Same as String.val
# @see String.val
#
toString: ()->
@val()
#
# Inserts a string into the word.
#
# @return {ListManager 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");
# y.bind(textbox);
#
bind: (textfield, dom_root)->
dom_root ?= window
if (not dom_root.getSelection?)
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)->
range = {}
s = dom_root.getSelection()
clength = textfield.textContent.length
range.left = Math.min s.anchorOffset, clength
range.right = Math.min s.focusOffset, clength
if fix?
range.left = fix range.left
range.right = fix range.right
edited_element = s.focusNode
if edited_element is textfield or edited_element is textfield.childNodes[0]
range.isReal = true
else
range.isReal = false
range
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 = document.createRange()
r.setStart(textnode, range.left)
r.setEnd(textnode, range.right)
s = window.getSelection()
s.removeAllRanges()
s.addRange(r)
writeContent = (content)->
content_array = content.replace(new RegExp("\n",'g')," ").split(" ")
textfield.innerText = ""
for c, i in content_array
textfield.innerText += c
if i isnt content_array.length-1
textfield.innerHTML += '&nbsp;'
writeContent this.val()
@observe (events)->
for event in events
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
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)->
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.onkeypress = null
return true
creator_token = true
char = null
if event.keyCode is 13
char = '\n'
else if event.key?
if event.charCode is 32
char = " "
else
char = event.key
else
char = window.String.fromCharCode event.keyCode
if char.length > 1
return true
else if char.length > 0
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()
creator_token = false
false
textfield.onpaste = (event)->
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.onpaste = null
return true
event.preventDefault()
textfield.oncut = (event)->
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.oncut = null
return true
event.preventDefault()
#
# consume deletes. Note that
# chrome: won't consume deletions on keypress event.
# keyCode is deprecated. BUT: I don't see another way.
# since event.key is not implemented in the current version of chrome.
# 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
r = createRange()
pos = Math.min(r.left, r.right, word.val().length)
diff = Math.abs(r.left - r.right)
if event.keyCode? and event.keyCode is 8 # Backspace
if diff > 0
word.delete pos, diff
r.left = pos
r.right = pos
writeRange r
else
if event.ctrlKey? and event.ctrlKey
val = word.val()
new_pos = pos
del_length = 0
if pos > 0
new_pos--
del_length++
while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n'
new_pos--
del_length++
word.delete new_pos, (pos-new_pos)
r.left = new_pos
r.right = new_pos
writeRange r
else
if pos > 0
word.delete (pos-1), 1
r.left = pos-1
r.right = pos-1
writeRange r
event.preventDefault()
creator_token = false
return false
else if event.keyCode? and event.keyCode is 46 # Delete
if diff > 0
word.delete pos, diff
r.left = pos
r.right = pos
writeRange r
else
word.delete pos, 1
r.left = pos
r.right = pos
writeRange r
event.preventDefault()
creator_token = false
return false
else
creator_token = false
true
#
# @private
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json = {
'type': @type
'uid' : @getUid()
}
json
types.String.parse = (json)->
{
'uid' : uid
} = json
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\"!!"
structured_types

View File

@ -1,367 +0,0 @@
###
json_types_uninitialized = require "./JsonTypes"
# some dom implementations may call another dom.method that simulates the behavior of another.
# For example xml.insertChild(dom) , wich inserts an element at the end, and xml.insertAfter(dom,null) wich does the same
# But Y's proxy may be called only once!
proxy_token = false
dont_proxy = (f)->
proxy_token = true
try
f()
catch e
proxy_token = false
throw new Error e
proxy_token = false
_proxy = (f_name, f)->
old_f = @[f_name]
if old_f?
@[f_name] = ()->
if not proxy_token and not @_y?.isDeleted()
that = this
args = arguments
dont_proxy ()->
f.apply that, args
old_f.apply that, args
else
old_f.apply this, arguments
#else
# @[f_name] = f
Element?.prototype._proxy = _proxy
module.exports = (HB)->
json_types = json_types_uninitialized HB
types = json_types.types
parser = json_types.parser
#
# Manages XML types
# Not supported:
# * Attribute nodes
# * Real replace of child elements (to much overhead). Currently, the new element is inserted after the 'replaced' element, and then it is deleted.
# * Namespaces (*NS)
# * Browser specific methods (webkit-* operations)
class XmlType extends types.Insert
constructor: (uid, @tagname, attributes, elements, @xml)->
### In case you make this instanceof Insert again
if prev? and (not next?) and prev.type?
# adjust what you actually mean. you want to insert after prev, then
# next is not defined. but we only insert after non-deleted elements.
# This is also handled in TextInsert.
while prev.isDeleted()
prev = prev.prev_cl
next = prev.next_cl
###
super(uid)
if @xml?._y?
d = new types.Delete undefined, @xml._y
HB.addOperation(d).execute()
@xml._y = null
if attributes? and elements?
@saveOperation 'attributes', attributes
@saveOperation 'elements', elements
else if (not attributes?) and (not elements?)
@attributes = new types.JsonType()
@attributes.setMutableDefault 'immutable'
HB.addOperation(@attributes).execute()
@elements = new types.WordType()
@elements.parent = @
HB.addOperation(@elements).execute()
else
throw new Error "Either define attribute and elements both, or none of them"
if @xml?
@tagname = @xml.tagName
for i in [0...@xml.attributes.length]
attr = xml.attributes[i]
@attributes.val(attr.name, attr.value)
for n in @xml.childNodes
if n.nodeType is n.TEXT_NODE
word = new TextNodeType(undefined, n)
HB.addOperation(word).execute()
@elements.push word
else if n.nodeType is n.ELEMENT_NODE
element = new XmlType undefined, undefined, undefined, undefined, n
HB.addOperation(element).execute()
@elements.push element
else
throw new Error "I don't know Node-type #{n.nodeType}!!"
@setXmlProxy()
undefined
#
# Identifies this class.
# Use it in order to check whether this is an xml-type or something else.
#
type: "XmlType"
applyDelete: (op)->
if @insert_parent? and not @insert_parent.isDeleted()
@insert_parent.applyDelete op
else
@attributes.applyDelete()
@elements.applyDelete()
super
cleanup: ()->
super()
setXmlProxy: ()->
@xml._y = @
that = @
@elements.on 'insert', (event, op)->
if op.creator isnt HB.getUserId() and this is that.elements
newNode = op.content.val()
right = op.next_cl
while right? and right.isDeleted()
right = right.next_cl
rightNode = null
if right.type isnt 'Delimiter'
rightNode = right.val().val()
dont_proxy ()->
that.xml.insertBefore newNode, rightNode
@elements.on 'delete', (event, op)->
del_op = op.deleted_by[0]
if del_op? and del_op.creator isnt HB.getUserId() and this is that.elements
deleted = op.content.val()
dont_proxy ()->
that.xml.removeChild deleted
@attributes.on ['add', 'update'], (event, property_name, op)->
if op.creator isnt HB.getUserId() and this is that.attributes
dont_proxy ()->
newval = op.val().val()
if newval?
that.xml.setAttribute(property_name, op.val().val())
else
that.xml.removeAttribute(property_name)
## Here are all methods that proxy the behavior of the xml
# you want to find a specific child element. Since they are carried by an Insert-Type, you want to find that Insert-Operation.
# @param child {DomElement} Dom element.
# @return {InsertType} This carries the XmlType that represents the DomElement (child). false if i couldn't find it.
#
findNode = (child)->
if not child?
throw new Error "you must specify a parameter!"
child = child._y
elem = that.elements.beginning.next_cl
while elem.type isnt 'Delimiter' and elem.content isnt child
elem = elem.next_cl
if elem.type is 'Delimiter'
false
else
elem
insertBefore = (insertedNode_s, adjacentNode)->
next = null
if adjacentNode?
next = findNode adjacentNode
prev = null
if next
prev = next.prev_cl
else
prev = @_y.elements.end.prev_cl
while prev.isDeleted()
prev = prev.prev_cl
inserted_nodes = null
if insertedNode_s.nodeType is insertedNode_s.DOCUMENT_FRAGMENT_NODE
child = insertedNode_s.lastChild
while child?
element = new XmlType undefined, undefined, undefined, undefined, child
HB.addOperation(element).execute()
that.elements.insertAfter prev, element
child = child.previousSibling
else
element = new XmlType undefined, undefined, undefined, undefined, insertedNode_s
HB.addOperation(element).execute()
that.elements.insertAfter prev, element
@xml._proxy 'insertBefore', insertBefore
@xml._proxy 'appendChild', insertBefore
@xml._proxy 'removeAttribute', (name)->
that.attributes.val(name, undefined)
@xml._proxy 'setAttribute', (name, value)->
that.attributes.val name, value
renewClassList = (newclass)->
dont_do_it = false
if newclass?
for elem in this
if newclass is elem
dont_do_it = true
value = Array.prototype.join.call this, " "
if newclass? and not dont_do_it
value += " "+newclass
that.attributes.val('class', value )
_proxy.call @xml.classList, 'add', renewClassList
_proxy.call @xml.classList, 'remove', renewClassList
@xml.__defineSetter__ 'className', (val)->
@setAttribute('class', val)
@xml.__defineGetter__ 'className', ()->
that.attributes.val('class')
@xml.__defineSetter__ 'textContent', (val)->
# remove all nodes
elem = that.xml.firstChild
while elem?
remove = elem
elem = elem.nextSibling
that.xml.removeChild remove
# insert word content
if val isnt ""
text_node = document.createTextNode val
that.xml.appendChild text_node
removeChild = (node)->
elem = findNode node
if not elem
throw new Error "You are only allowed to delete existing (direct) child elements!"
d = new types.Delete undefined, elem
HB.addOperation(d).execute()
node._y = null
@xml._proxy 'removeChild', removeChild
@xml._proxy 'replaceChild', (insertedNode, replacedNode)->
insertBefore.call this, insertedNode, replacedNode
removeChild.call this, replacedNode
val: (enforce = false)->
if document?
if (not @xml?) or enforce
@xml = document.createElement @tagname
attr = @attributes.val()
for attr_name, value of attr
if value?
a = document.createAttribute attr_name
a.value = value
@xml.setAttributeNode a
e = @elements.beginning.next_cl
while e.type isnt "Delimiter"
n = e.content
if not e.isDeleted() and e.content? # TODO: how can this happen? Probably because listeners
if n.type is "XmlType"
@xml.appendChild n.val(enforce)
else if n.type is "TextNodeType"
text_node = n.val()
@xml.appendChild text_node
else
throw new Error "Internal structure cannot be transformed to dom"
e = e.next_cl
@setXmlProxy()
@xml
execute: ()->
super()
###
if not @validateSavedOperations()
return false
else
return true
###
#
# Get the parent of this JsonType.
# @return {XmlType}
#
getParent: ()->
@parent
#
# @private
#
# Convert all relevant information of this operation to the json-format.
# This result can be send to other clients.
#
_encode: ()->
json =
{
'type' : @type
'attributes' : @attributes.getUid()
'elements' : @elements.getUid()
'tagname' : @tagname
'uid' : @getUid()
}
json
parser['XmlType'] = (json)->
{
'uid' : uid
'attributes' : attributes
'elements' : elements
'tagname' : tagname
} = json
new XmlType uid, tagname, attributes, elements, undefined
#
# @nodoc
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
#
class TextNodeType extends types.ImmutableObject
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} content
#
constructor: (uid, content)->
if content._y?
d = new types.Delete undefined, content._y
HB.addOperation(d).execute()
content._y = null
content._y = @
super uid, content
applyDelete: (op)->
if @insert_parent? and not @insert_parent.isDeleted()
@insert_parent.applyDelete op
else
super
type: "TextNodeType"
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json = {
'type': @type
'uid' : @getUid()
'content' : @content.textContent
}
json
parser['TextNodeType'] = (json)->
{
'uid' : uid
'content' : content
} = json
textnode = document.createTextNode content
new TextNodeType uid, textnode
types['XmlType'] = XmlType
json_types
###

View File

@ -1,5 +1,6 @@
json_types_uninitialized = require "./Types/JsonTypes"
structured_ops_uninitialized = require "./Operations/Structured"
HistoryBuffer = require "./HistoryBuffer"
Engine = require "./Engine"
adaptConnector = require "./ConnectorAdapter"
@ -14,35 +15,25 @@ createY = (connector)->
user_id = id
HB.resetUserId id
HB = new HistoryBuffer user_id
type_manager = json_types_uninitialized HB
types = type_manager.types
ops_manager = structured_ops_uninitialized HB, this.constructor
ops = ops_manager.operations
#
# Framework for Json data-structures.
# Known values that are supported:
# * String
# * Integer
# * Array
#
class Y extends types.Object
engine = new Engine HB, ops
adaptConnector connector, engine, HB, ops_manager.execution_listener
#
# @param {String} user_id Unique id of the peer.
# @param {Connector} Connector the connector class.
#
constructor: ()->
@connector = connector
@HB = HB
@types = types
@engine = new Engine @HB, type_manager.types
adaptConnector @connector, @engine, @HB, type_manager.execution_listener
super
ops.Operation.prototype.HB = HB
ops.Operation.prototype.operations = ops
ops.Operation.prototype.engine = engine
ops.Operation.prototype.connector = connector
ops.Operation.prototype.custom_types = this.constructor
getConnector: ()->
@connector
return new Y(HB.getReservedUniqueIdentifier()).execute()
ct = new createY.Object()
model = new ops.MapManager(ct, HB.getReservedUniqueIdentifier()).execute()
ct._setModel model
ct
module.exports = createY
if window? and not window.Y?
if window?
window.Y = createY
createY.Object = require "./ObjectType"

View File

@ -13,7 +13,6 @@
"keywords": [
"OT",
"collaboration",
"Yata",
"synchronization",
"ShareJS",
"Coweb",
@ -27,6 +26,7 @@
},
"homepage": "https://dadamonad.github.io/yjs/",
"dependencies": {
"chai": "^2.2.0"
},
"devDependencies": {
"codo": "^2.0.9",
@ -48,7 +48,6 @@
"gulp-ignore": "^1.2.0",
"gulp-ljs": "^0.1.1",
"gulp-mocha": "^0.5.2",
"gulp-mocha-phantomjs": "^0.5.0",
"gulp-plumber": "^0.6.6",
"gulp-rename": "^1.2.0",
"gulp-rimraf": "^0.1.0",

View File

@ -7,35 +7,44 @@ _ = require("underscore")
chai.use(sinonChai)
Connector = require "../../y-test/lib/y-test.coffee"
Y = null # need global reference!
module.exports = class Test
constructor: (@name_suffix = "")->
constructor: (@name_suffix = "", Yjs)->
Y = Yjs
@number_of_test_cases_multiplier = 1
@repeat_this = 1 * @number_of_test_cases_multiplier
@doSomething_amount = 1230 * @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)
@time_now = 0 # current time
@max_depth = 3
@debug = false
@reinitialize()
for gf in @getGeneratingFunctions(0)
if not (gf.types? and gf.f?)
throw new Error "Generating Functions are not initialized properly!"
for t in gf.types
if not t?
throw new Error "You havent includedt this type in Y (do require 'y-whatever')"
reinitialize: ()->
@users = []
for i in [0...@number_of_engines]
u = @makeNewUser (i+@name_suffix)
for user in @users
u.getConnector().join(user.getConnector()) # TODO: change the test-connector to make this more convenient
u._model.connector.join(user._model.connector) # TODO: change the test-connector to make this more convenient
@users.push u
@initUsers?(@users[0])
@flushAll()
# is called by implementing class
makeNewUser: (user)->
user.HB.stopGarbageCollection()
user._model.HB.stopGarbageCollection()
user
getSomeUser: ()->
@ -69,28 +78,33 @@ module.exports = class Test
@getRandomText [1,2,'x','y'], 1 # only 4 keys
getGeneratingFunctions: (user_num)=>
types = @users[user_num].types
[
f : (y)=> # INSERT TEXT
y
pos = _.random 0, (y.val().length-1)
y.insert pos, @getRandomText()
null
types: [types.String]
,
f : (y)-> # DELETE TEXT
if y.val().length > 0
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.delete pos, length
undefined
types : [types.String]
]
types = @users[user_num]._model.operations
[]
getRandomRoot: (user_num)->
throw new Error "overwrite me!"
throw new Error "implement me!"
getContent: (user_num)->
throw new Error "overwrite me!"
compare: (o1, o2, depth = (@max_depth+1))->
if o1 is o2 or depth <= 0
true
else if o1._name? and o1._name isnt o2._name
throw new Error "different types"
else if o1._model?
@compare o1._model, o2._model, depth
else if o1.type is "MapManager"
for name, val of o1.val()
@compare(val, o2.val(name), depth-1)
else if o1.type is "ListManager"
for val,i in o1.val()
@compare(val, o2.val(i), depth-1)
else if o1.constructor is Array and o2.constructor is Array
if o1.length isnt o2.length
throw new Error "The Arrays do not have the same size!"
for o,i in o1
@compare o, o2[i], (depth-1)
else if o1 isnt o2
throw new Error "different values"
else
throw new Error "I don't know what to do .. "
generateRandomOp: (user_num)=>
y = @getRandomRoot(user_num)
@ -106,7 +120,7 @@ module.exports = class Test
applyRandomOp: (user_num)=>
user = @users[user_num]
user.getConnector().flushOneRandom()
user._model.connector.flushOneRandom()
doSomething: ()->
user_num = _.random (@number_of_engines-1)
@ -119,15 +133,14 @@ module.exports = class Test
final = false
if @users.length <= 1 or not final
for user,user_number in @users
user.getConnector().flushAll()
user._model.connector.flushAll()
else
for user,user_number in @users[1..]
user.getConnector().flushAll()
user._model.connector.flushAll()
ops = @users[1].getHistoryBuffer()._encode @users[0].HB.getOperationCounter()
@users[0].engine.applyOpsCheckDouble ops
compareAll: (test_number)->
@flushAll(true)
@ -135,7 +148,7 @@ module.exports = class Test
number_of_created_operations = 0
for i in [0...(@users.length)]
number_of_created_operations += @users[i].getConnector().getOpsInExecutionOrder().length
number_of_created_operations += @users[i]._model.connector.getOpsInExecutionOrder().length
@ops += number_of_created_operations*@users.length
ops_per_msek = Math.floor(@ops/@time)
@ -146,7 +159,7 @@ module.exports = class Test
if @debug
if not _.isEqual @getContent(i), @getContent(i+1)
printOpsInExecutionOrder = (otnumber, otherotnumber)=>
ops = _.filter @users[otnumber].getConnector().getOpsInExecutionOrder(), (o)->
ops = _.filter @users[otnumber]._model.connector.getOpsInExecutionOrder(), (o)->
typeof o.uid.op_name isnt 'string' and o.uid.creator isnt '_'
for s,j in ops
console.log "op#{j} = " + (JSON.stringify s)
@ -172,7 +185,7 @@ module.exports = class Test
ops = printOpsInExecutionOrder i+1, i
console.log ""
expect(@getContent(i)).to.deep.equal(@getContent(i+1))
expect(@compare(@users[i], @users[i+1])).to.not.be.undefined
run: ()->
if @debug
@ -183,7 +196,7 @@ module.exports = class Test
@doSomething()
@flushAll(false)
for u in @users
u.HB.emptyGarbage()
u._model.HB.emptyGarbage()
for i in [1..Math.floor(@doSomething_amount/2)]
@doSomething()
@ -196,12 +209,11 @@ module.exports = class Test
# in case of JsonFramework, every user will create its JSON first! therefore, the testusers id must be small than all the others (see InsertType)
@users[@users.length] = @makeNewUser (-1) # this does not want to join with anymody
@users[@users.length-1].HB.renewStateVector @users[0].HB.getOperationCounter()
@users[@users.length-1].engine.applyOps @users[0].HB._encode()
@users[@users.length-1]._model.HB.renewStateVector @users[0]._model.HB.getOperationCounter()
@users[@users.length-1]._model.engine.applyOps @users[0]._model.HB._encode()
#if @getContent(@users.length-1) isnt @getContent(0)
# console.log "testHBencoding:"
# console.log "Unprocessed ops first: #{@users[0].engine.unprocessed_ops.length}"
# console.log "Unprocessed ops last: #{@users[@users.length-1].engine.unprocessed_ops.length}"
expect(@getContent(@users.length-1)).to.deep.equal(@getContent(0))
expect(@compare(@users[@users.length-1], @users[0])).to.not.be.undefined

View File

@ -1,166 +0,0 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
chai.use(sinonChai)
Y = require "../lib/y"
Connector = require "../../y-test/lib/y-test.coffee"
Test = require "./TestSuite"
class TextTest extends Test
type: "TextTest"
makeNewUser: (userId)->
conn = new Connector userId
new Y conn
initUsers: (u)->
u.val("TextTest","","mutable")
getRandomRoot: (user_num)->
@users[user_num].val("TextTest")
getContent: (user_num)->
@users[user_num].val("TextTest").val()
describe "TextFramework", ->
@timeout 500000
beforeEach (done)->
@yTest = new TextTest()
done()
it "simple multi-char insert", ->
u = @yTest.users[0].val("TextTest")
u.insert 0, "abc"
u = @yTest.users[1].val("TextTest")
u.insert 0, "xyz"
@yTest.compareAll()
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","mutable").val("TextTest")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("insert")
expect(change.object).to.equal(u)
expect(change.value).to.equal("a")
expect(change.position).to.equal(1)
expect(change.changedBy).to.equal('0')
last_task = "observer1"
u.observe observer1
u.insert 1, "a"
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("insert")
expect(change.object).to.equal(u)
expect(change.value).to.equal("x")
expect(change.position).to.equal(0)
expect(change.changedBy).to.equal('1')
last_task = "observer2"
u.observe observer2
v = @yTest.users[1].val("TextTest")
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","mutable").val("TextTest")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.position).to.equal(1)
expect(change.length).to.equal(1)
expect(change.changedBy).to.equal('0')
last_task = "observer1"
u.observe observer1
u.delete 1, 1
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.position).to.equal(0)
expect(change.length).to.equal(1)
expect(change.changedBy).to.equal('1')
last_task = "observer2"
u.observe observer2
v = @yTest.users[1].val("TextTest")
v.delete 0, 1
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "can handle many engines, many operations, concurrently (random)", ->
console.log("testiy deleted this TODO:dtrn")
@yTest.run()
it "handles double-late-join", ->
test = new TextTest("double")
test.run()
@yTest.run()
u1 = test.users[0]
u2 = @yTest.users[1]
ops1 = u1.HB._encode()
ops2 = u2.HB._encode()
u1.engine.applyOp ops2, true
u2.engine.applyOp ops1, true
compare = (o1, o2)->
if o1.type? and o1.type isnt o2.type
throw new Error "different types"
else if o1.type is "Object"
for name, val of o1.val()
compare(val, o2.val(name))
else if o1.type?
compare(o1.val(), o2.val())
else if o1 isnt o2
throw new Error "different values"
compare u1, u2
expect(test.getContent(0)).to.deep.equal(@yTest.getContent(1))

View File

@ -1,196 +0,0 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
$ = require("jquery")
document?.$ = $ # for browser
require 'coffee-errors'
chai.use(sinonChai)
Y = require "../lib/index"
Connector = require "../../y-test/lib/y-test.coffee"
Test = require "./TestSuite"
class XmlTest extends Test
type: "XmlTest"
makeNewUser: (user, conn)->
super new Y.XmlFramework user, conn
getRandomRoot: (user_num)->
@users[user_num].getSharedObject()
getContent: (user_num)->
@users[user_num].val()
describe "XmlFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new XmlTest()
###
@users = @yTest.users
###
test_users = []
connector = (new Connector 0, test_users)
@test_user = @yTest.makeNewUser 0, connector
test_users.push @test_user
# test_user_listen listens to the actions of test_user. He will update his dom when he receives from test_user.
@test_user_listen = @yTest.makeNewUser 2, connector
test_users.push @test_user_listen
@test_user2 = @yTest.makeNewUser 1, (Connector_uninitialized [])
$("#test_dom").replaceWith('<div id="test_dom" test_attribute="the test" class="stuffy" style="color: blue"><p id="replaceme">replace me</p><p id="removeme">remove me</p><p>This is a test object for <b>XmlFramework</b></p><span class="span_element"><p>span</p></span></div>')
@$dom = $("#test_dom")
@dom = @$dom[0]
@test_user.val(@dom)
@test_user_listen.getConnector().flushAll()
@test_user_listen_dom = @test_user_listen.val()
@check = ()=>
dom_ = @dom.outerHTML
# now test if other collaborators can parse the HB and result in the same content
hb = @test_user.HB._encode()
@test_user2.engine.applyOps(hb)
dom2 = @test_user2.val()
expect(dom_).to.equal(dom2.outerHTML)
@test_user_listen.getConnector().flushAll()
expect(dom_).to.equal(@test_user_listen_dom.outerHTML)
done()
it "can transform to a new real Dom element", ->
dom_ = @test_user.val(true)
expect(dom_ isnt @dom).to.be.true
it "supports dom.insertBefore", ->
newdom = $("<p>dtrn</p>")[0]
newdom2 = $("<p>dtrn2</p>")[0]
n = $("#removeme")[0]
@dom.insertBefore(newdom, null)
@dom.insertBefore(newdom2, n)
@check()
it "supports dom.appendChild", ->
newdom = $("<p>dtrn</p>")[0]
@dom.appendChild(newdom)
@check()
it "supports dom.setAttribute", ->
@dom.setAttribute("test_attribute", "newVal")
@check()
it "supports dom.removeAttribute", ->
@dom.removeAttribute("test_attribute")
@check()
it "supports dom.removeChild", ->
@dom.removeChild($("#removeme")[0])
expect($("#removeme").length).to.equal(0)
@check()
it "supports dom.replaceChild", ->
newdom = $("<p>replaced</p>")[0]
replace = $("#replaceme")[0]
@dom.replaceChild(newdom,replace)
expect($("#replaceme").length).to.equal(0)
@check()
it "supports dom.classList.add", ->
@dom.classList.add "classy"
@check()
it "supports dom.textcontent", -> #TODO!!!!
@dom.classList.add "classy"
@check()
it "supports jquery.addClass", ->
@$dom.addClass("testy")
@check()
it "supports jquery.after", ->
d = $("#test_dom p")
d.after("<div class=\"inserted_after\">after</div>")
@check()
it "supports jquery.append", ->
d = $("#test_dom p")
d.after("<b>appended</b>")
@check()
it "supports jquery.appendTo", ->
$("<b>appendedTo</b>").appendTo("#test_dom p")
$("p").appendTo("#test_dom")
@check()
it "supports jquery.before", ->
d = $("#test_dom p")
d.before("<div>before</div>")
@check()
it "supports jquery.detach", ->
d = $(".inserted_after")
d.detach()
@check()
it "supports jquery.empty", ->
d = $("#test_dom p")
d.empty()
@check()
it "supports jquery.insertAfter", ->
$("<p>after span</p>").insertAfter(".span_element")
@check()
it "supports jquery.insertBefore", ->
$("<p>before span</p>").insertBefore(".span_element")
@check()
it "supports jquery.prepend", ->
d = $("#test_dom div")
d.prepend("<p>prepended</p>")
@check()
it "supports jquery.prependTo", ->
$("<p atone=false attwo=\"dtrn\" class=\"attr_node sudo su\">prepended to</p>").prependTo("#test_dom div")
@check()
it "supports jquery.remove", ->
d = $("#test_dom b")
d.remove()
@check()
it "supports jquery.removeAttr", ->
d = $(".attr_node")
d.removeAttr("attwo")
@check()
it "supports jquery.removeClass", ->
d = $(".attr_node")
d.removeClass("sudo")
@check()
it "supports jquery.attr", ->
d = $(".attr_node")
d.attr("atone", true)
@check()
it "supports jquery.replaceAll", ->
$("<span>New span content </span>").replaceAll("#test_dom div")
@check()
it "supports jquery.replaceWith", ->
d = $("#test_dom span")
d.replaceWith("<div>me is div again </div>")
@check()

View File

@ -7,90 +7,128 @@ _ = require("underscore")
chai.use(sinonChai)
Connector = require "../../y-test/lib/y-test.coffee"
Y = require "../lib/y.coffee"
Y.Test = require "../../y-test/lib/y-test.coffee"
Y.Text = require "../../y-text/lib/y-text"
Y.List = require "../../y-list/lib/y-list"
Test = require "./TestSuite"
TestSuite = require "./TestSuite"
class ObjectTest extends TestSuite
constructor: (suffix)->
super suffix, Y
class JsonTest extends Test
makeNewUser: (userId)->
conn = new Connector userId
conn = new Y.Test userId
super new Y conn
type: "JsonTest"
type: "ObjectTest"
getRandomRoot: (user_num, root)->
getRandomRoot: (user_num, root, depth = @max_depth)->
root ?= @users[user_num]
types = @users[user_num].types
if _.random(0,1) is 1 # take root
if depth is 0 or _.random(0,1) is 1 # take root
root
else # take child
depth--
elems = null
if root.type is "Object"
if root._name is "Object"
elems =
for oname,val of root.val()
val
else if root.type is "Array"
else if root._name is "Array"
elems = root.val()
else
return root
elems = elems.filter (elem)->
elem? and ((elem.type is "Array") or (elem.type is "Object"))
elem? and ((elem._name is "Array") or (elem._name is "Object"))
if elems.length is 0
root
else
p = elems[_.random(0, elems.length-1)]
@getRandomRoot user_num, p
getContent: (user_num)->
@users[user_num].toJson(true)
@getRandomRoot user_num, p, depth
getGeneratingFunctions: (user_num)->
types = @users[user_num].types
super(user_num).concat [
f : (y)=> # SET PROPERTY
l = y.val().length
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
null
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
f : (y)=> # Delete Object Property
list = for name, o of y.val()
name
if list.length > 0
key = list[_random(0,list.length-1)]
key = list[_.random(0,list.length-1)]
y.delete(key)
types: [types.Object]
, f : (y)=> # SET Object Property
y.val(@getRandomKey(), @getRandomObject())
types: [types.Object]
types: [Y.Object]
,
f : (y)=> # SET Object Property
y.val(@getRandomKey(), new Y.Object(@getRandomObject()))
types: [Y.Object]
,
f : (y)=> # SET PROPERTY TEXT
y.val(@getRandomKey(), @getRandomText(), 'mutable')
types: [types.Object]
]
y.val(@getRandomKey(), new Y.Text(@getRandomText()))
types: [Y.Object]
,
f : (y)=> # SET PROPERTY List
y.val(@getRandomKey(), new Y.List(@getRandomText().split("")))
types: [Y.Object]
,
f : (y)=> # Delete Array Element
list = y.val()
if list.length > 0
i = _.random(0,list.length-1)
y.delete(i)
types: [Y.List]
,
f : (y)=> # insert Object mutable
l = y.val().length
y.insert(_.random(0, l-1), new Y.Object(@getRandomObject()))
types: [Y.List]
,
f : (y)=> # insert Text mutable
l = y.val().length
y.insert(_.random(0, l-1), new Y.Text(@getRandomText()))
types : [Y.List]
,
f : (y)=> # insert List mutable
l = y.val().length
y.insert(_.random(0, l-1), new Y.List(@getRandomText().split("")))
types : [Y.List]
,
f : (y)=> # insert Number (primitive object)
l = y.val().length
y.insert(_.random(0,l-1), _.random(0,42))
types : [Y.List]
,
f : (y)=> # SET Object Property (circular)
y.val(@getRandomKey(), @getRandomRoot user_num)
types: [Y.Object]
,
f : (y)=> # insert Object mutable (circular)
l = y.val().length
y.insert(_.random(0, l-1), @getRandomRoot user_num)
types: [Y.List]
,
f : (y)=> # INSERT TEXT
y
pos = _.random 0, (y.val().length-1)
y.insert pos, @getRandomText()
null
types: [Y.Text]
,
f : (y)-> # DELETE TEXT
if y.val().length > 0
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.delete pos, length
undefined
types : [Y.Text]
]
describe "JsonFramework", ->
module.exports = ObjectTest
describe "Object Test", ->
@timeout 500000
beforeEach (done)->
@yTest = new JsonTest()
@yTest = new ObjectTest()
@users = @yTest.users
@test_user = @yTest.makeNewUser "test_user"
@ -104,31 +142,21 @@ describe "JsonFramework", ->
@yTest.compareAll()
it "handles double-late-join", ->
test = new JsonTest("double")
test = new ObjectTest("double")
test.run()
@yTest.run()
u1 = test.users[0]
u2 = @yTest.users[1]
ops1 = u1.HB._encode()
ops2 = u2.HB._encode()
u1.engine.applyOp ops2, true
u2.engine.applyOp ops1, true
compare = (o1, o2)->
if o1.type? and o1.type isnt o2.type
throw new Error "different types"
else if o1.type is "Object"
for name, val of o1.val()
compare(val, o2.val(name))
else if o1.type?
compare(o1.val(), o2.val())
else if o1 isnt o2
throw new Error "different values"
compare u1, u2
expect(test.getContent(0)).to.deep.equal(@yTest.getContent(1))
ops1 = u1._model.HB._encode()
ops2 = u2._model.HB._encode()
u1._model.engine.applyOp ops2, true
u2._model.engine.applyOp ops1, true
expect(@yTest.compare(u1, u2)).to.not.be.undefined
it "can handle creaton of complex json (1)", ->
@yTest.users[0].val('a', 'q', "mutable")
@yTest.users[1].val('a', 't', "mutable")
@yTest.users[0].val('a', new Y.Text('q'))
@yTest.users[1].val('a', new Y.Text('t'))
@yTest.compareAll()
q = @yTest.users[2].val('a')
q.insert(0,'A')
@ -136,11 +164,11 @@ describe "JsonFramework", ->
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"}}, "mutable")
@yTest.getSomeUser().val('b', {'a':{}})
@yTest.getSomeUser().val('c', {'a':'c'})
@yTest.getSomeUser().val('c', {'a':'b'})
@yTest.getSomeUser().val('x', new Y.Object({'a':'b'}))
@yTest.getSomeUser().val('a', new Y.Object({'a':{q: new Y.Text("dtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")}}))
@yTest.getSomeUser().val('b', new Y.Object({'a':{}}))
@yTest.getSomeUser().val('c', new Y.Object({'a':'c'}))
@yTest.getSomeUser().val('c', new Y.Object({'a':'b'}))
@yTest.compareAll()
q = @yTest.getSomeUser().val("a").val("a").val("q")
q.insert(0,'A')
@ -148,19 +176,19 @@ describe "JsonFramework", ->
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.users[0].val('l', new Y.List([1,2,3]))
@yTest.users[1].val('l', new Y.List([4,5,6]))
@yTest.compareAll()
@yTest.users[2].val('l').insert(0,'A')
w = @yTest.users[1].val('l').insert(0,'B', "mutable").val(0)
w = @yTest.users[1].val('l').insert(0,new Y.Text('B')).val(0)
w.insert 1, "C"
expect(w.val()).to.equal("BC")
@yTest.compareAll()
it "handles immutables and primitive data types", ->
@yTest.getSomeUser().val('string', "text", "immutable")
@yTest.getSomeUser().val('number', 4, "immutable")
@yTest.getSomeUser().val('object', {q:"rr"}, "immutable")
@yTest.getSomeUser().val('string', "text")
@yTest.getSomeUser().val('number', 4)
@yTest.getSomeUser().val('object', new Y.Object({q:"rr"}))
@yTest.getSomeUser().val('null', null)
@yTest.compareAll()
expect(@yTest.getSomeUser().val('string')).to.equal "text"
@ -169,9 +197,9 @@ describe "JsonFramework", ->
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('string', "text")
@yTest.users[1].val('number', 4)
@yTest.users[2].val('object', new Y.Object({q:"rr"}))
@yTest.users[0].val('null', null)
@yTest.compareAll()
expect(@yTest.getSomeUser().val('string')).to.equal "text"
@ -192,7 +220,7 @@ describe "JsonFramework", ->
expect(change.name).to.equal("newStuff")
last_task = "observer1"
u.observe observer1
u.val("newStuff","someStuff","mutable")
u.val("newStuff",new Y.Text("someStuff"))
expect(last_task).to.equal("observer1")
u.unobserve observer1
@ -212,7 +240,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","mutable").val("moreStuff","moreOldStuff","mutable")
u = @yTest.users[0].val("newStuff", new Y.Text("oldStuff")).val("moreStuff",new Y.Text("moreOldStuff"))
@yTest.flushAll()
last_task = null
observer1 = (changes)->
@ -247,7 +275,7 @@ describe "JsonFramework", ->
it "Observers work on JSON Types (delete type observers, local and foreign)", ->
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
u = @yTest.users[0].val("newStuff",new Y.Text("oldStuff")).val("moreStuff",new Y.Text("moreOldStuff"))
@yTest.flushAll()
last_task = null
observer1 = (changes)->
@ -280,5 +308,17 @@ describe "JsonFramework", ->
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "can handle circular JSON", ->
u = @yTest.users[0]
u.val("me", u)
@yTest.compareAll()
u.val("stuff", new Y.Object({x: true}))
u.val("same_stuff", u.val("stuff"))
u.val("same_stuff").val("x", 5)
expect(u.val("same_stuff").val("x")).to.equal(5)
@yTest.compareAll()
u.val("stuff").val("y", u.val("stuff"))
@yTest.compareAll()

File diff suppressed because one or more lines are too long

4
y.js

File diff suppressed because one or more lines are too long