Compare commits

...

91 Commits

Author SHA1 Message Date
Kevin Jahns
c5b47e88ac bump version numbers 2015-05-28 15:47:43 +02:00
Kevin Jahns
dc3c6a5d42 added support to use existing user ids! Fixes #23 2015-05-28 15:44:13 +02:00
Kevin Jahns
a9c2ec6ba0 Merge pull request #22 from cphyc/bugfix-21
Bugfix 21
2015-05-21 14:51:48 +02:00
Corentin STG_CADIOU
f166b9efc5 Compiled files 2015-05-21 14:39:13 +02:00
Corentin STG_CADIOU
0441b83f74 Fix error with compute_when_synced 2015-05-21 14:28:47 +02:00
Kevin Jahns
90c82a6a02 Merge pull request #20 from cphyc/Fix-error-in-package.json
Use correct path for gulp
2015-05-19 13:31:56 +02:00
cphyc
da25905b73 Use correct path for gulp 2015-05-19 10:47:36 +02:00
Kevin Jahns
3c07a938cd prepublish yjs 2015-05-07 13:34:42 +02:00
Kevin Jahns
55ccacc442 added doc 2015-05-05 16:09:34 +02:00
Kevin Jahns
946a11f03d fixing possible source of a bug in y-rt 2015-05-05 12:55:21 +02:00
Kevin Jahns
93f3a49396 fixed problem in large sendHB 2015-05-04 13:25:12 +02:00
Kevin Jahns
3eed100b8d updated compiled files 2015-05-03 19:59:41 +02:00
Kevin Jahns
eb136ae1bf update README 2015-05-03 19:57:51 +02:00
Kevin Jahns
006d0a2643 update README 2015-05-03 19:56:44 +02:00
Kevin Jahns
7959bdf5ac Merge pull request #19 from y-js/0.5
0.5
2015-05-03 19:51:50 +02:00
Kevin Jahns
6f9ee0d9ba added tutorial in README 2015-05-03 19:50:59 +02:00
Kevin Jahns
d901d5f5e4 small fixes 2015-04-30 16:29:48 +02:00
Kevin Jahns
b2c7706a2e fixed y-object polymer 2015-04-30 15:09:10 +02:00
Kevin Jahns
4d926cf841 updated examples 2015-04-30 14:15:48 +02:00
Kevin Jahns
0314a1b709 Merge pull request #18 from y-js/0.5
Added support for custom connectors. Not compatible with 0.4!!
2015-04-30 12:42:38 +02:00
Kevin Jahns
8a5b69e86c better fix for #17 2015-04-30 09:48:39 +02:00
Kevin Jahns
ce5250b9d8 updated readme 2015-04-29 01:07:12 +02:00
Kevin Jahns
f51c791490 updated readme 2015-04-29 00:57:36 +02:00
Kevin Jahns
b75305a082 updated readme 2015-04-29 00:55:11 +02:00
Kevin Jahns
8d80fd5614 updated readme 2015-04-29 00:54:27 +02:00
Kevin Jahns
bad6c913fc upd 2015-04-28 16:07:45 +02:00
Kevin Jahns
85d85540e7 updated Composition (error when doing undo) 2015-04-28 14:57:37 +02:00
Kevin Jahns
80f1cfd21b added user events 2015-04-28 11:31:43 +02:00
Kevin Jahns
729d7ed3aa fixed getNext & getPrev 2015-04-24 13:52:17 +02:00
Kevin Jahns
0a89150fab insertions must not be deleted anymore! 2015-04-23 17:40:40 +02:00
Kevin Jahns
6fc33e40bb observer will get the reference too now (ListManager) 2015-04-23 15:27:20 +02:00
Kevin Jahns
7f6592a6b7 removed phantom 2015-04-20 11:51:52 +02:00
Kevin Jahns
b9cdbcc6fa deleted debug log 2015-04-19 19:21:05 +02:00
Kevin Jahns
2a78cdba48 Operation can now save static content/Operations 2015-04-19 18:54:03 +02:00
Kevin Jahns
b02662c36e refs, complex saveOperation' and validateSavedOperations` 2015-04-17 20:11:05 +02:00
Kevin Jahns
f44f463e9d references & composition value
fixing bugs with references
composition type parses now composition value
2015-04-17 00:36:52 +02:00
Kevin Jahns
757bb118ce Insert bug detected (because of references) 2015-04-16 02:31:22 +02:00
DadaMonad
5417ffb999 non-important update 2015-04-14 17:14:43 +00:00
DadaMonad
4de979bc33 Merge branch '0.5' of https://github.com/rwth-acis/yjs into 0.5 2015-04-14 17:13:27 +00:00
DadaMonad
875f56586e added parameter to getNext 2015-04-14 17:13:16 +00:00
Kevin Jahns
249b712648 MapManager returns null, when reference does not exist 2015-04-14 01:30:35 +02:00
DadaMonad
58cefae839 updated composition 2015-04-12 13:09:10 +00:00
DadaMonad
d9c5ab5fa8 added support for getNext/getPrev on insertions 2015-04-07 10:37:50 +00:00
DadaMonad
6d99ed07f0 update 2015-04-07 03:17:03 +00:00
DadaMonad
e55ed9f2b4 composition type & support for list references 2015-04-07 00:16:12 +00:00
DadaMonad
bb0bfcc5c8 cleaned up, removed Replaceable operation, changed Operation specific events method (put it into the Manager types), created Composition type 2015-04-06 21:35:04 +00:00
DadaMonad
b24de43fe2 removed webcomponents from dependencies 2015-03-09 19:39:18 +00:00
DadaMonad
446560d9e8 added support for namespaces on types 2015-03-09 17:38:26 +00:00
DadaMonad
148e46f043 updated README 2015-03-01 15:44:31 +00:00
DadaMonad
e8f20dabd3 updated readme with new types 2015-02-27 18:32:05 +00:00
DadaMonad
96ed8b0f98 outsourced all types (except for object type) 2015-02-27 18:01:21 +00:00
DadaMonad
c663230c1b add xml examples 2015-02-26 21:39:12 +00:00
DadaMonad
0a8118367d yay - every test on xml and dom succeeds 2015-02-26 21:28:06 +00:00
DadaMonad
f932f560bd completed the xml tests - and lots of them run successfully 2015-02-26 18:28:35 +00:00
DadaMonad
f9542b90db made xml tests more expressive 2015-02-26 10:09:41 +00:00
DadaMonad
014495febd added random tests for xml test suite 2015-02-26 09:20:26 +00:00
DadaMonad
82f11c421f fixed parent issue (only one parent per Y.Xml type) 2015-02-25 23:41:57 +00:00
DadaMonad
9059618d1f Y-Xml tests pass 2015-02-25 22:50:26 +00:00
DadaMonad
9a8f8fba05 added tests for new xml type. 2015-02-24 20:34:27 +00:00
DadaMonad
3ba89edf7d support for circular structures (e.g. with JSON) 2015-02-24 16:09:42 +00:00
DadaMonad
fea6de3bf9 starting to refine the Replaceable type. Most types should be a replaceable. 2015-02-23 16:36:55 +00:00
DadaMonad
2a644f2f0c travis: build master branch only 2015-02-23 13:30:58 +00:00
DadaMonad
f189ae11b0 tests are no longer failing :) 2015-02-23 13:20:52 +00:00
DadaMonad
2e9f8f6d03 added text as a custom type, more tests are working 2015-02-23 11:41:04 +00:00
DadaMonad
860934de06 broke the dammn thing 2015-02-19 15:55:05 +00:00
DadaMonad
792440a71d devided ops/types 2015-02-19 10:41:34 +00:00
DadaMonad
1aacc0e967 remove travis 0.8 0.6 version 2015-02-18 00:44:16 +00:00
DadaMonad
d4b0c8cbbd travis build 2015-02-18 00:28:43 +00:00
DadaMonad
d6526f12fb fixed mocha stop 2015-02-18 00:01:43 +00:00
DadaMonad
d3af98cd17 travis - install coffee and bower first 2015-02-17 23:19:41 +00:00
DadaMonad
e33eb6a928 travis build bug 2015-02-17 23:14:00 +00:00
DadaMonad
d1be152983 travis button 2015-02-17 23:06:29 +00:00
DadaMonad
548a77833a added travis stuff 2015-02-17 23:00:25 +00:00
DadaMonad
5ba0a7492a refactoring text types 2015-02-17 21:10:46 +00:00
DadaMonad
c65f11b308 updated mocha 2015-02-17 19:37:15 +00:00
DadaMonad
77b83cae2a fixing double late join test fail 2015-02-17 19:23:27 +00:00
DadaMonad
f609c22be8 fixed text binding (enter, utf8 chars..) 2015-02-17 10:26:32 +00:00
DadaMonad
670854e9d8 added gitter button 2015-02-15 17:48:30 +00:00
DadaMonad
2bb7ba03cd added contrib (finally - heh?) 2015-02-15 17:17:34 +00:00
DadaMonad
686be484fc bumped version numbers 2015-02-15 15:35:09 +00:00
DadaMonad
60de3ce5b0 getting ready for 0.4 realeasy 2015-02-15 15:33:35 +00:00
DadaMonad
b6fe47efe1 changed to syncMethod 2015-02-05 16:19:55 +00:00
DadaMonad
e5f16812b3 added new y-test connector 2015-02-05 14:15:20 +00:00
DadaMonad
3eb933400a fixed doSync bug, fixed connection problems, improved p2p sync method - still
there are some cases that may lead to inconsistencies. Currently, only the master-slave method is a reliable sync method
2015-02-05 10:46:40 +00:00
DadaMonad
58a479be9b included connector type 2015-02-03 18:55:02 +00:00
DadaMonad
f835a72151 v0.3.2 2015-01-30 17:03:18 +00:00
DadaMonad
50fa81d191 fixed focus issue 2015-01-30 17:02:03 +00:00
DadaMonad
93e75e0111 updated npm+bower version to v0.3.1 2015-01-30 11:30:11 +00:00
DadaMonad
cbcdebf33e updated connector functionality 2015-01-30 11:29:05 +00:00
DadaMonad
7c842efd52 updated README 2015-01-27 19:32:51 +00:00
DadaMonad
60daa082f7 updated README 2015-01-27 17:59:58 +00:00
56 changed files with 113699 additions and 39875 deletions

11
.travis.yml Normal file
View File

@@ -0,0 +1,11 @@
language: node_js
before_install:
- "npm install -g bower coffee-script"
- "bower install"
node_js:
- "0.12"
- "0.11"
- "0.10"
branches:
only:
- master

138
README.md
View File

@@ -1,39 +1,50 @@
# ![Yatta!](https://dadamonad.github.io/files/layout/y_logo.png)
[![Build Status](http://layers.dbis.rwth-aachen.de/jenkins/job/Yatta/badge/icon)](http://layers.dbis.rwth-aachen.de/jenkins/job/Yatta/)
# ![Yjs](http://y-js.org/files/layout/yjs.svg)
[![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 handle concurrent actions on arbitrary complex data types like Text, Json, and XML. We provide a tutorial and some applications for this framework on our [homepage](http://y-js.org/).
You can create you own 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). 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 - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors). Currently, we support the following communication protocols:
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
Y 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]. Y was designed to take away the pain from concurrently editing complex data types like Text, Json, and XML. For more information you should check out the [website](https://dadamonad.github.io/yjs/)!
You can use Yjs client-, and server- side. You can get it as via npm, and bower. We even provide polymer elements for Yjs!
In the future, we want to enable users to implement their own collaborative types. Currently we provide data types for
* Text
* Json
* XML
Unlike other frameworks, Y supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Y is extremely scalable and can be used in a wide range of application scenarios.
We support several communication protocols as so called *Connectors*. You find a bunch of Connectors in the [Y-Connectors](https://github.com/rwth-acis/y-connectors) repository. Currently supported 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
You can use Y client-, and server- side. You can get it as via npm, and bower. We even provide a polymer element for Y!
The theoretical advantages over similar frameworks are support for
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 yjs
bower install y-js/yjs
```
Then you include the libraries directly from the installation folder.
```
@@ -50,28 +61,77 @@ 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
## Support
Please report _any_ issues to the [Github issue page](https://github.com/rwth-acis/yjs/issues)!
I would appreciate if developers give me feedback on how _convenient_ the framework is, and if it is easy to use. Particularly the XML-support may not support every DOM-methods - if you encounter a method that does not cause any change on other peers, please state function name, and sample parameters. However, there are browser-specific features, that Y won't support.
# 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/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/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.
## License
yjs is licensed under the [MIT License](./LICENSE.txt).
Yjs is licensed under the [MIT License](./LICENSE.txt).
<yjs@dbis.rwth-aachen.de>
[ShareJs]: https://github.com/share/ShareJS
[OpenCoweb]: https://github.com/opencoweb/coweb
<kevin.jahns@rwth-aachen.de>

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "0.3.0",
"version": "0.5.2",
"homepage": "https://github.com/DadaMonad/yjs",
"authors": [
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
@@ -27,7 +27,7 @@
"extras",
"test"
],
"dependencies": {
"polymer": "Polymer/polymer#~0.5.3"
"devDependencies": {
"y-test" : "y-test#~0.4.0"
}
}

11
build/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Directories
### build/browser
You find the browserified (not minified) version of yjs here. This is nice for debugging, since it also includes sourcemaps. For production, however, you should use the version that you find in the main directory.
### build/node
Yjs for nodejs is located here. You can only use the submodules, or require 'y' in your node project. Also works with browserify.
### build/test
Start build/test/index.html' in your browser, to perform testing Yjs.

View File

@@ -0,0 +1,7 @@
<polymer-element name="y-object" hidden attributes="val connector y">
</polymer-element>
<polymer-element name="y-property" hidden attributes="val name y">
</polymer-element>
<script src="./y-object.js"></script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,16 @@
var adaptConnector;
var ConnectorClass, adaptConnector;
ConnectorClass = require("./ConnectorClass");
adaptConnector = function(connector, engine, HB, execution_listener) {
var applyHB, encode_state_vector, getHB, getStateVector, parse_state_vector, send_;
var applyHB, encode_state_vector, f, getHB, getStateVector, name, parse_state_vector, send_;
for (name in ConnectorClass) {
f = ConnectorClass[name];
connector[name] = f;
}
connector.setIsBoundToY();
send_ = function(o) {
if (o.uid.creator === HB.getUserId() && (typeof o.uid.op_number !== "string")) {
if ((o.uid.creator === HB.getUserId()) && (typeof o.uid.op_number !== "string") && (HB.getUserId() !== "_temp")) {
return connector.broadcast(o);
}
};
@@ -12,22 +19,22 @@ adaptConnector = function(connector, engine, HB, execution_listener) {
}
execution_listener.push(send_);
encode_state_vector = function(v) {
var name, 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;
@@ -36,33 +43,29 @@ adaptConnector = function(connector, engine, HB, execution_listener) {
return encode_state_vector(HB.getOperationCounter());
};
getHB = function(v) {
var hb, json, o, state_vector, _i, _len;
var hb, json, state_vector;
state_vector = parse_state_vector(v);
hb = HB._encode(state_vector);
for (_i = 0, _len = hb.length; _i < _len; _i++) {
o = hb[_i];
o.fromHB = "true";
}
json = {
hb: hb,
state_vector: encode_state_vector(HB.getOperationCounter())
};
return json;
};
applyHB = function(hb) {
return engine.applyOp(hb);
applyHB = function(hb, fromHB) {
return engine.applyOp(hb, fromHB);
};
connector.getStateVector = getStateVector;
connector.getHB = getHB;
connector.applyHB = applyHB;
connector.whenReceiving(function(sender, op) {
if (connector.receive_handlers == null) {
connector.receive_handlers = [];
}
return connector.receive_handlers.push(function(sender, op) {
if (op.uid.creator !== HB.getUserId()) {
return engine.applyOp(op);
}
});
if (connector._whenBoundToY != null) {
return connector._whenBoundToY();
}
};
module.exports = adaptConnector;

View File

@@ -0,0 +1,415 @@
module.exports = {
init: function(options) {
var req;
req = (function(_this) {
return function(name, choices) {
if (options[name] != null) {
if ((choices == null) || choices.some(function(c) {
return c === options[name];
})) {
return _this[name] = options[name];
} else {
throw new Error("You can set the '" + name + "' option to one of the following choices: " + JSON.encode(choices));
}
} else {
throw new Error("You must specify " + name + ", when initializing the Connector!");
}
};
})(this);
req("syncMethod", ["syncAll", "master-slave"]);
req("role", ["master", "slave"]);
req("user_id");
if (typeof this.on_user_id_set === "function") {
this.on_user_id_set(this.user_id);
}
if (options.perform_send_again != null) {
this.perform_send_again = options.perform_send_again;
} else {
this.perform_send_again = true;
}
if (this.role === "master") {
this.syncMethod = "syncAll";
}
this.is_synced = false;
this.connections = {};
if (this.receive_handlers == null) {
this.receive_handlers = [];
}
this.connections = {};
this.current_sync_target = null;
this.sent_hb_to_all_users = false;
return this.is_initialized = true;
},
onUserEvent: function(f) {
if (this.connections_listeners == null) {
this.connections_listeners = [];
}
return this.connections_listeners.push(f);
},
isRoleMaster: function() {
return this.role === "master";
},
isRoleSlave: function() {
return this.role === "slave";
},
findNewSyncTarget: function() {
var c, ref, user;
this.current_sync_target = null;
if (this.syncMethod === "syncAll") {
ref = this.connections;
for (user in ref) {
c = ref[user];
if (!c.is_synced) {
this.performSync(user);
break;
}
}
}
if (this.current_sync_target == null) {
this.setStateSynced();
}
return null;
},
userLeft: function(user) {
var f, i, len, ref, results;
delete this.connections[user];
this.findNewSyncTarget();
if (this.connections_listeners != null) {
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, 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] = {};
}
this.connections[user].is_synced = false;
if ((!this.is_synced) || this.syncMethod === "syncAll") {
if (this.syncMethod === "syncAll") {
this.performSync(user);
} else if (role === "master") {
this.performSyncWithMaster(user);
}
}
if (this.connections_listeners != null) {
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.constructor === Function) {
args = [args];
}
if (this.is_synced) {
return args[0].apply(this, args.slice(1));
} else {
if (this.compute_when_synced == null) {
this.compute_when_synced = [];
}
return this.compute_when_synced.push(args);
}
},
onReceive: function(f) {
return this.receive_handlers.push(f);
},
/*
* Broadcast a message to all connected peers.
* @param message {Object} The message to broadcast.
#
broadcast: (message)->
throw new Error "You must implement broadcast!"
#
* Send a message to a peer, or set of peers
#
send: (peer_s, message)->
throw new Error "You must implement send!"
*/
performSync: function(user) {
var _hb, hb, i, len, o;
if (this.current_sync_target == null) {
this.current_sync_target = user;
this.send(user, {
sync_step: "getHB",
send_again: "true",
data: this.getStateVector()
});
if (!this.sent_hb_to_all_users) {
this.sent_hb_to_all_users = true;
hb = this.getHB([]).hb;
_hb = [];
for (i = 0, len = hb.length; i < len; i++) {
o = hb[i];
_hb.push(o);
if (_hb.length > 10) {
this.broadcast({
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
return this.broadcast({
sync_step: "applyHB",
data: _hb
});
}
}
},
performSyncWithMaster: function(user) {
var _hb, hb, i, len, o;
this.current_sync_target = user;
this.send(user, {
sync_step: "getHB",
send_again: "true",
data: this.getStateVector()
});
hb = this.getHB([]).hb;
_hb = [];
for (i = 0, len = hb.length; i < len; i++) {
o = hb[i];
_hb.push(o);
if (_hb.length > 10) {
this.broadcast({
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
return this.broadcast({
sync_step: "applyHB",
data: _hb
});
},
setStateSynced: function() {
var args, el, 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++) {
el = ref[i];
f = el[0];
args = el.slice(1);
f.apply(args);
}
delete this.compute_when_synced;
}
return null;
}
},
whenReceivedStateVector: function(f) {
if (this.when_received_state_vector_listeners == null) {
this.when_received_state_vector_listeners = [];
}
return this.when_received_state_vector_listeners.push(f);
},
receiveMessage: function(sender, res) {
var _hb, data, f, hb, i, j, k, len, len1, len2, o, ref, ref1, 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));
}
return results;
} else {
if (sender === this.user_id) {
return;
}
if (res.sync_step === "getHB") {
if (this.when_received_state_vector_listeners != null) {
ref1 = this.when_received_state_vector_listeners;
for (j = 0, len1 = ref1.length; j < len1; j++) {
f = ref1[j];
f.call(this, res.data);
}
}
delete this.when_received_state_vector_listeners;
data = this.getHB(res.data);
hb = data.hb;
_hb = [];
if (this.is_synced) {
sendApplyHB = (function(_this) {
return function(m) {
return _this.send(sender, m);
};
})(this);
} else {
sendApplyHB = (function(_this) {
return function(m) {
return _this.broadcast(m);
};
})(this);
}
for (k = 0, len2 = hb.length; k < len2; k++) {
o = hb[k];
_hb.push(o);
if (_hb.length > 10) {
sendApplyHB({
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
sendApplyHB({
sync_step: "applyHB",
data: _hb
});
if ((res.send_again != null) && this.perform_send_again) {
send_again = (function(_this) {
return function(sv) {
return function() {
var l, len3;
hb = _this.getHB(sv).hb;
for (l = 0, len3 = hb.length; l < len3; l++) {
o = hb[l];
_hb.push(o);
if (_hb.length > 10) {
_this.send(sender, {
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
return _this.send(sender, {
sync_step: "applyHB",
data: _hb,
sent_again: "true"
});
};
};
})(this)(data.state_vector);
return setTimeout(send_again, 3000);
}
} else if (res.sync_step === "applyHB") {
this.applyHB(res.data, sender === this.current_sync_target);
if ((this.syncMethod === "syncAll" || (res.sent_again != null)) && (!this.is_synced) && ((this.current_sync_target === sender) || (this.current_sync_target == null))) {
this.connections[sender].is_synced = true;
return this.findNewSyncTarget();
}
} else if (res.sync_step === "applyHB_") {
return this.applyHB(res.data, sender === this.current_sync_target);
}
}
},
parseMessageFromXml: function(m) {
var parse_array, parse_object;
parse_array = function(node) {
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));
} else {
results.push(parse_object(n));
}
}
return results;
};
parse_object = function(node) {
var i, int, json, len, n, name, ref, ref1, value;
json = {};
ref = node.attrs;
for (name in ref) {
value = ref[name];
int = parseInt(value);
if (isNaN(int) || ("" + int) !== value) {
json[name] = value;
} else {
json[name] = int;
}
}
ref1 = node.children;
for (i = 0, len = ref1.length; i < len; i++) {
n = ref1[i];
name = n.name;
if (n.getAttribute("isArray") === "true") {
json[name] = parse_array(n);
} else {
json[name] = parse_object(n);
}
}
return json;
};
return parse_object(m);
},
encodeMessageToXml: function(m, json) {
var encode_array, encode_object;
encode_object = function(m, json) {
var name, value;
for (name in json) {
value = json[name];
if (value == null) {
} else if (value.constructor === Object) {
encode_object(m.c(name), value);
} else if (value.constructor === Array) {
encode_array(m.c(name), value);
} else {
m.setAttribute(name, value);
}
}
return m;
};
encode_array = function(m, array) {
var e, i, len;
m.setAttribute("isArray", "true");
for (i = 0, len = array.length; i < len; i++) {
e = array[i];
if (e.constructor === Object) {
encode_object(m.c("array-element"), e);
} else {
encode_array(m.c("array-element"), e);
}
}
return m;
};
if (json.constructor === Object) {
return encode_object(m.c("y", {
xmlns: "http://y.ninja/connector-stanza"
}), json);
} else if (json.constructor === Array) {
return encode_array(m.c("y", {
xmlns: "http://y.ninja/connector-stanza"
}), json);
} else {
throw new Error("I can't encode this json!");
}
},
setIsBoundToY: function() {
if (typeof this.on_bound_to_y === "function") {
this.on_bound_to_y();
}
delete this.when_bound_to_y;
return this.is_bound_to_y = true;
}
};

View File

@@ -42,31 +42,38 @@ 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) {
return this.applyOp(ops_json);
};
Engine.prototype.applyOp = function(op_json_array) {
var o, op_json, _i, _len;
Engine.prototype.applyOp = function(op_json_array, fromHB) {
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";
}
o = this.parseOperation(op_json);
o.parsed_from_json = op_json;
if (op_json.fromHB != null) {
o.fromHB = op_json.fromHB;
}
@@ -83,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(user_id) {
this.user_id = 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 = [];
@@ -16,30 +16,33 @@ HistoryBuffer = (function() {
setTimeout(this.emptyGarbage, this.garbageCollectTimeout);
}
HistoryBuffer.prototype.resetUserId = function(id) {
var o, o_name, own;
own = this.buffer[this.user_id];
if (own != null) {
for (o_name in own) {
o = own[o_name];
o.uid.creator = id;
}
if (this.buffer[id] != null) {
throw new Error("You are re-assigning an old user id - this is not (yet) possible!");
}
this.buffer[id] = own;
delete this.buffer[this.user_id];
HistoryBuffer.prototype.setUserId = function(user_id1, state_vector) {
var base, buff, counter_diff, name, o, o_name, ref;
this.user_id = user_id1;
if ((base = this.buffer)[name = this.user_id] == null) {
base[name] = [];
}
this.operation_counter[id] = this.operation_counter[this.user_id];
delete this.operation_counter[this.user_id];
return this.user_id = id;
buff = this.buffer[this.user_id];
counter_diff = state_vector[this.user_id] || 0;
if (this.buffer._temp != null) {
ref = this.buffer._temp;
for (o_name in ref) {
o = ref[o_name];
o.uid.creator = this.user_id;
o.uid.op_number += counter_diff;
buff[o.uid.op_number] = o;
}
}
this.operation_counter[this.user_id] = (this.operation_counter._temp || 0) + counter_diff;
delete this.operation_counter._temp;
return delete this.buffer._temp;
};
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();
}
@@ -57,18 +60,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,18 +95,17 @@ HistoryBuffer = (function() {
HistoryBuffer.prototype.getReservedUniqueIdentifier = function() {
return {
creator: '_',
op_number: "_" + (this.reserved_identifier_counter++),
doSync: false
op_number: "_" + (this.reserved_identifier_counter++)
};
};
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;
@@ -113,16 +115,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 = {};
}
@@ -133,12 +135,15 @@ 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;
}
for (o_number in user) {
o = user[o_number];
if (o.uid.doSync && unknown(u_name, o_number)) {
if ((o.uid.noOperation == null) && unknown(u_name, o_number)) {
o_json = o._encode();
if (o.next_cl != null) {
o_next = o.next_cl;
@@ -170,19 +175,18 @@ HistoryBuffer = (function() {
}
uid = {
'creator': user_id,
'op_number': this.operation_counter[user_id],
'doSync': true
'op_number': this.operation_counter[user_id]
};
this.operation_counter[user_id]++;
return uid;
};
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 {
@@ -206,8 +210,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) {
@@ -217,30 +221,31 @@ 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])) {
_results.push(this.operation_counter[user] = state_vector[user]);
if (((this.operation_counter[user] == null) || (this.operation_counter[user] < state_vector[user])) && (state_vector[user] != null)) {
results.push(this.operation_counter[user] = state_vector[user]);
} else {
_results.push(void 0);
results.push(void 0);
}
}
return _results;
return results;
};
HistoryBuffer.prototype.addToCounter = function(o) {
if (this.operation_counter[o.uid.creator] == null) {
this.operation_counter[o.uid.creator] = 0;
var base, name;
if ((base = this.operation_counter)[name = o.uid.creator] == null) {
base[name] = 0;
}
if (typeof o.uid.op_number === 'number' && o.uid.creator !== this.getUserId()) {
if (o.uid.op_number === this.operation_counter[o.uid.creator]) {
return this.operation_counter[o.uid.creator]++;
} else {
return this.invokeSync(o.uid.creator);
}
if (o.uid.op_number === this.operation_counter[o.uid.creator]) {
this.operation_counter[o.uid.creator]++;
}
while (this.buffer[o.uid.creator][this.operation_counter[o.uid.creator]] != null) {
this.operation_counter[o.uid.creator]++;
}
return void 0;
};
return HistoryBuffer;

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,579 @@
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() {
var composition_ref;
if (this.validateSavedOperations()) {
this.getCustomType()._setCompositionValue(this._composition_value);
delete this._composition_value;
if (this.tmp_composition_ref) {
composition_ref = this.HB.getOperation(this.tmp_composition_ref);
if (composition_ref != null) {
delete this.tmp_composition_ref;
this.composition_ref = composition_ref;
}
}
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,487 +0,0 @@
var __slice = [].slice,
__hasProp = {}.hasOwnProperty,
__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; };
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(parent) {
this.parent = parent;
};
Operation.prototype.getParent = function() {
return this.parent;
};
Operation.prototype.getUid = function() {
if (this.uid.noOperation == null) {
return this.uid;
} else {
return this.uid.alt;
}
};
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.dontSync = function() {
return this.uid.doSync = false;
};
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 ? op.execute : void 0) != null) {
return this[name] = op;
} else if (op != null) {
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(uid, prev_cl, next_cl, origin, parent) {
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.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) {
return this.prev_cl.applyDelete();
}
};
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.parent != null) {
if (this.prev_cl == null) {
this.prev_cl = this.parent.beginning;
}
if (this.origin == null) {
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;
};
return Insert;
})(types.Operation);
types.ImmutableObject = (function(_super) {
__extends(ImmutableObject, _super);
function ImmutableObject(uid, content) {
this.content = 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,
__hasProp = {}.hasOwnProperty,
__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; };
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.Array) {
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,354 +0,0 @@
var basic_types_uninitialized,
__hasProp = {}.hasOwnProperty,
__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; };
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, map_uid, rm, rm_uid;
if (this.map[property_name] == null) {
event_properties = {
name: property_name
};
event_this = this;
map_uid = this.cloneUid();
map_uid.sub = property_name;
rm_uid = {
noOperation: true,
alt: map_uid
};
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.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) {
result.push(o);
o = o.next_cl;
}
return result;
};
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 instanceof types.Delimiter)) {
o = o.prev_cl;
}
break;
}
if (position <= 0 && !o.isDeleted()) {
break;
}
o = o.next_cl;
if (!o.isDeleted()) {
position -= 1;
}
}
return o;
};
return ListManager;
})(types.Operation);
types.ReplaceManager = (function(_super) {
__extends(ReplaceManager, _super);
function ReplaceManager(event_properties, event_this, uid, beginning, end) {
this.event_properties = event_properties;
this.event_this = 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) {
if ((content != null) && (content.creator != null)) {
this.saveOperation('content', content);
} else {
this.content = content;
}
this.saveOperation('parent', parent);
Replaceable.__super__.constructor.call(this, 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(),
'origin': this.origin.getUid(),
'uid': this.getUid(),
'is_deleted': this.is_deleted
};
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,558 +0,0 @@
var structured_types_uninitialized,
__hasProp = {}.hasOwnProperty,
__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; };
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.TextInsert = (function(_super) {
__extends(TextInsert, _super);
function TextInsert(content, uid, prev, next, origin, parent) {
if (content != null ? content.creator : void 0) {
this.saveOperation('content', content);
} else {
this.content = content;
}
TextInsert.__super__.constructor.call(this, uid, prev, next, origin, parent);
}
TextInsert.prototype.type = "TextInsert";
TextInsert.prototype.getLength = function() {
if (this.isDeleted()) {
return 0;
} else {
return this.content.length;
}
};
TextInsert.prototype.applyDelete = function() {
TextInsert.__super__.applyDelete.apply(this, arguments);
if (this.content instanceof types.Operation) {
this.content.applyDelete();
}
return this.content = null;
};
TextInsert.prototype.execute = function() {
if (!this.validateSavedOperations()) {
return false;
} else {
if (this.content instanceof types.Operation) {
this.content.insert_parent = this;
}
return TextInsert.__super__.execute.call(this);
}
};
TextInsert.prototype.val = function(current_position) {
if (this.isDeleted() || (this.content == null)) {
return "";
} else {
return this.content;
}
};
TextInsert.prototype._encode = function() {
var json, _ref;
json = {
'type': this.type,
'uid': this.getUid(),
'prev': this.prev_cl.getUid(),
'next': this.next_cl.getUid(),
'origin': this.origin.getUid(),
'parent': this.parent.getUid()
};
if (((_ref = this.content) != null ? _ref.getUid : void 0) != null) {
json['content'] = this.content.getUid();
} else {
json['content'] = this.content;
}
return json;
};
return TextInsert;
})(types.Insert);
types.TextInsert.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'];
return new types.TextInsert(content, uid, prev, next, origin, parent);
};
types.Array = (function(_super) {
__extends(Array, _super);
function Array() {
return Array.__super__.constructor.apply(this, arguments);
}
Array.prototype.type = "Array";
Array.prototype.applyDelete = function() {
var o;
o = this.end;
while (o != null) {
o.applyDelete();
o = o.prev_cl;
}
return Array.__super__.applyDelete.call(this);
};
Array.prototype.cleanup = function() {
return Array.__super__.cleanup.call(this);
};
Array.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.Array) {
_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;
};
Array.prototype.val = function(pos) {
var o, result;
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 {
o = this.beginning.next_cl;
result = [];
while (o !== this.end) {
if (!o.isDeleted()) {
result.push(o.val());
}
o = o.next_cl;
}
return result;
}
};
Array.prototype.push = function(content) {
return this.insertAfter(this.end.prev_cl, content);
};
Array.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.TextInsert(content, void 0, left, right)).execute();
} else {
for (_i = 0, _len = content.length; _i < _len; _i++) {
c = content[_i];
tmp = (new types.TextInsert(createContent(c, options), void 0, left, right)).execute();
left = tmp;
}
}
return this;
};
Array.prototype.insert = function(position, content, options) {
var ith;
ith = this.getOperationByPosition(position);
return this.insertAfter(ith, [content], options);
};
Array.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;
};
Array.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid()
};
return json;
};
return Array;
})(types.ListManager);
types.Array.parse = function(json) {
var uid;
uid = json['uid'];
return new this(uid);
};
types.Array.create = function(content, mutable) {
var ith, list;
if (mutable === "mutable") {
list = new types.Array().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.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() {
var c, o;
c = (function() {
var _i, _len, _ref, _results;
_ref = this.toArray();
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
o = _ref[_i];
if (o.val != null) {
_results.push(o.val());
} else {
_results.push("");
}
}
return _results;
}).call(this);
return c.join('');
};
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, left, right, s;
s = dom_root.getSelection();
clength = textfield.textContent.length;
left = Math.min(s.anchorOffset, clength);
right = Math.min(s.focusOffset, clength);
if (fix != null) {
left = fix(left);
right = fix(right);
}
return {
left: left,
right: right,
isReal: true
};
};
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 append;
append = "";
if (content[content.length - 1] === " ") {
content = content.slice(0, content.length - 1);
append = '&nbsp;';
}
textfield.textContent = content;
return textfield.innerHTML += append;
};
}
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.key != null) {
if (event.charCode === 32) {
char = " ";
} else if (event.keyCode === 13) {
char = '\n';
} 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.Array);
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

@@ -1,44 +1,42 @@
var Y, bindToChildren;
Y = require('./y');
var bindToChildren;
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;
});
};
@@ -52,7 +50,7 @@ Polymer("y-object", {
}
},
valChanged: function() {
if ((this.val != null) && this.val.type === "Object") {
if ((this.val != null) && this.val._name === "Object") {
return bindToChildren(this);
}
},
@@ -68,23 +66,23 @@ Polymer("y-property", {
ready: function() {
if ((this.val != null) && (this.name != null)) {
if (this.val.constructor === Object) {
this.val = this.parentElement.val(this.name, this.val).val(this.name);
this.val = this.parentElement.val(this.name, new Y.Object(this.val)).val(this.name);
} else if (typeof this.val === "string") {
this.parentElement.val(this.name, this.val);
}
if (this.val.type === "Object") {
if (this.val._name === "Object") {
return bindToChildren(this);
}
}
},
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 this.val = this.parentElement.val.val(this.name, new Y.Object(this.val)).val(this.name);
} else if (this.val._name === "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,
__hasProp = {}.hasOwnProperty,
__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; };
var Engine, HistoryBuffer, adaptConnector, createY, structured_ops_uninitialized;
json_types_uninitialized = require("./Types/JsonTypes");
structured_ops_uninitialized = require("./Operations/Structured");
HistoryBuffer = require("./HistoryBuffer");
@@ -11,44 +9,37 @@ Engine = require("./Engine");
adaptConnector = require("./ConnectorAdapter");
createY = function(connector) {
var HB, Y, type_manager, types, user_id;
user_id = null;
if (connector.id != null) {
user_id = connector.id;
var HB, ct, engine, model, ops, ops_manager, user_id;
if (connector.user_id != null) {
user_id = connector.user_id;
} else {
user_id = "_temp";
connector.whenUserIdSet(function(id) {
user_id = id;
return HB.resetUserId(id);
});
connector.when_received_state_vector_listeners = [
function(state_vector) {
return HB.setUserId(this.user_id, state_vector);
}
];
}
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

@@ -1,21 +1,21 @@
<html>
<head>
<meta charset="utf-8">
<title>Test Yatta!</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
<title>Test Yjs!</title>
<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="TextYatta_test.js"></script>
<script src="JsonYatta_test.js"></script>
<!--script src="XmlYatta_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();

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
# Examples
Here you find some (hopefully) usefull examples on how to use Yatta!
Here you find some (hopefully) usefull examples on how to use Yjs!
Please note, that the XMPP Connector is the best supported Connector at the moment.
Feel free to use the code of the examples in your own project. They include basic examples how to use Yjs.

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,8 @@
<meta charset=utf-8 />
<title>Y Example</title>
<script src="../../build/browser/y.js"></script>
<script src="../../../y-connectors/y-xmpp/y-xmpp.js"></script>
<script src="../../../y-text/build/browser/y-text.js"></script>
<script src="../../../y-xmpp/y-xmpp.js"></script>
<script src="./index.js"></script>
</head>
<body>
@@ -14,7 +15,7 @@ and XMPP Connector. </p>
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
<p> <a href="https://github.com/rwth-acis/yjs/">yjs</a> is a Framework for Real-Time collaboration on arbitrary data types.
<p> <a href="https://github.com/y-js/yjs/">yjs</a> is a Framework for Real-Time collaboration on arbitrary data types.
</p>
</body>
</html>

View File

@@ -1,6 +1,6 @@
connector = new Y.XMPP("testy-xmpp-json2");
connector = new Y.XMPP().join("testy-xmpp-json3", {syncMode: "syncAll"});
connector.debug = true
y = new Y(connector);
@@ -18,9 +18,9 @@ window.onload = function(){
});
connector.whenSynced(function(){
if(y.val("textfield") == null){
y.val("headline","headline", "mutable");
y.val("textfield","stuff", "mutable")
y.val("headline", new Y.Text("headline"));
y.val("textfield",new Y.Text("stuff"))
}
})
};
};

View File

@@ -15,20 +15,20 @@ 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'
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
@@ -43,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 }
@@ -66,6 +66,7 @@ gulp.task 'build_browser', ->
debug: true
.pipe rename
extname: ".js"
dirname: "./"
.pipe gulp.dest './build/test/'
gulp.task 'build_node', ->
@@ -76,14 +77,13 @@ gulp.task 'build_node', ->
gulp.task 'build', ['build_node', 'build_browser'], ->
gulp.task 'watch', ['build_browser'], ->
gulp.watch files.all, ['build_browser']
gulp.task 'watch', ['build'], ->
gulp.watch files.all, ['build']
gulp.task 'mocha', ->
gulp.src files.test, { read: false }
.pipe plumber()
.pipe mocha {reporter : 'list'}
.pipe exit()
gulp.task 'lint', ->
gulp.src files.all
@@ -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

@@ -1,13 +1,21 @@
ConnectorClass = require "./ConnectorClass"
#
# @param {Engine} engine The transformation engine
# @param {HistoryBuffer} HB
# @param {Array<Function>} execution_listener You must ensure that whenever an operation is executed, every function in this Array is called.
#
adaptConnector = (connector, engine, HB, execution_listener)->
for name, f of ConnectorClass
connector[name] = f
connector.setIsBoundToY()
send_ = (o)->
if o.uid.creator is HB.getUserId() and (typeof o.uid.op_number isnt "string")
if (o.uid.creator is HB.getUserId()) and
(typeof o.uid.op_number isnt "string") and # TODO: i don't think that we need this anymore..
(HB.getUserId() isnt "_temp")
connector.broadcast o
if connector.invokeSync?
@@ -32,25 +40,22 @@ adaptConnector = (connector, engine, HB, execution_listener)->
getHB = (v)->
state_vector = parse_state_vector v
hb = HB._encode state_vector
for o in hb
o.fromHB = "true" # execute immediately
json =
hb: hb
state_vector: encode_state_vector HB.getOperationCounter()
json
applyHB = (hb)->
engine.applyOp hb
applyHB = (hb, fromHB)->
engine.applyOp hb, fromHB
connector.getStateVector = getStateVector
connector.getHB = getHB
connector.applyHB = applyHB
connector.whenReceiving (sender, op)->
connector.receive_handlers ?= []
connector.receive_handlers.push (sender, op)->
if op.uid.creator isnt HB.getUserId()
engine.applyOp op
if connector._whenBoundToY?
connector._whenBoundToY()
module.exports = adaptConnector
module.exports = adaptConnector

355
lib/ConnectorClass.coffee Normal file
View File

@@ -0,0 +1,355 @@
module.exports =
#
# @params new Connector(options)
# @param options.syncMethod {String} is either "syncAll" or "master-slave".
# @param options.role {String} The role of this client
# (slave or master (only used when syncMethod is master-slave))
# @param options.perform_send_again {Boolean} Whetehr to whether to resend the HB after some time period. This reduces sync errors, but has some overhead (optional)
#
init: (options)->
req = (name, choices)=>
if options[name]?
if (not choices?) or choices.some((c)->c is options[name])
@[name] = options[name]
else
throw new Error "You can set the '"+name+"' option to one of the following choices: "+JSON.encode(choices)
else
throw new Error "You must specify "+name+", when initializing the Connector!"
req "syncMethod", ["syncAll", "master-slave"]
req "role", ["master", "slave"]
req "user_id"
@on_user_id_set?(@user_id)
# whether to resend the HB after some time period. This reduces sync errors.
# But this is not necessary in the test-connector
if options.perform_send_again?
@perform_send_again = options.perform_send_again
else
@perform_send_again = true
# A Master should sync with everyone! TODO: really? - for now its safer this way!
if @role is "master"
@syncMethod = "syncAll"
# is set to true when this is synced with all other connections
@is_synced = false
# Peerjs Connections: key: conn-id, value: object
@connections = {}
# List of functions that shall process incoming data
@receive_handlers ?= []
# whether this instance is bound to any y instance
@connections = {}
@current_sync_target = null
@sent_hb_to_all_users = false
@is_initialized = true
onUserEvent: (f)->
@connections_listeners ?= []
@connections_listeners.push f
isRoleMaster: ->
@role is "master"
isRoleSlave: ->
@role is "slave"
findNewSyncTarget: ()->
@current_sync_target = null
if @syncMethod is "syncAll"
for user, c of @connections
if not c.is_synced
@performSync user
break
if not @current_sync_target?
@setStateSynced()
null
userLeft: (user)->
delete @connections[user]
@findNewSyncTarget()
if @connections_listeners?
for f in @connections_listeners
f {
action: "userLeft"
user: user
}
userJoined: (user, role)->
if not role?
throw new Error "Internal: You must specify the role of the joined user! E.g. userJoined('uid:3939','slave')"
# a user joined the room
@connections[user] ?= {}
@connections[user].is_synced = false
if (not @is_synced) or @syncMethod is "syncAll"
if @syncMethod is "syncAll"
@performSync user
else if role is "master"
# TODO: What if there are two masters? Prevent sending everything two times!
@performSyncWithMaster user
if @connections_listeners?
for f in @connections_listeners
f {
action: "userJoined"
user: user
role: role
}
#
# Execute a function _when_ we are connected. If not connected, wait until connected.
# @param f {Function} Will be executed on the Connector context.
#
whenSynced: (args)->
if args.constructor is Function
args = [args]
if @is_synced
args[0].apply this, args[1..]
else
@compute_when_synced ?= []
@compute_when_synced.push args
#
# Execute an function when a message is received.
# @param f {Function} Will be executed on the PeerJs-Connector context. f will be called with (sender_id, broadcast {true|false}, message).
#
onReceive: (f)->
@receive_handlers.push f
###
# Broadcast a message to all connected peers.
# @param message {Object} The message to broadcast.
#
broadcast: (message)->
throw new Error "You must implement broadcast!"
#
# Send a message to a peer, or set of peers
#
send: (peer_s, message)->
throw new Error "You must implement send!"
###
#
# perform a sync with a specific user.
#
performSync: (user)->
if not @current_sync_target?
@current_sync_target = user
@send user,
sync_step: "getHB"
send_again: "true"
data: @getStateVector()
if not @sent_hb_to_all_users
@sent_hb_to_all_users = true
hb = @getHB([]).hb
_hb = []
for o in hb
_hb.push o
if _hb.length > 10
@broadcast
sync_step: "applyHB_"
data: _hb
_hb = []
@broadcast
sync_step: "applyHB"
data: _hb
#
# When a master node joined the room, perform this sync with him. It will ask the master for the HB,
# and will broadcast his own HB
#
performSyncWithMaster: (user)->
@current_sync_target = user
@send user,
sync_step: "getHB"
send_again: "true"
data: @getStateVector()
hb = @getHB([]).hb
_hb = []
for o in hb
_hb.push o
if _hb.length > 10
@broadcast
sync_step: "applyHB_"
data: _hb
_hb = []
@broadcast
sync_step: "applyHB"
data: _hb
#
# You are sure that all clients are synced, call this function.
#
setStateSynced: ()->
if not @is_synced
@is_synced = true
if @compute_when_synced?
for el in @compute_when_synced
f = el[0]
args = el[1..]
f.apply(args)
delete @compute_when_synced
null
# executed when the a state_vector is received. listener will be called only once!
whenReceivedStateVector: (f)->
@when_received_state_vector_listeners ?= []
@when_received_state_vector_listeners.push f
#
# You received a raw message, and you know that it is intended for to Yjs. Then call this function.
#
receiveMessage: (sender, res)->
if not res.sync_step?
for f in @receive_handlers
f sender, res
else
if sender is @user_id
return
if res.sync_step is "getHB"
# call listeners
if @when_received_state_vector_listeners?
for f in @when_received_state_vector_listeners
f.call this, res.data
delete @when_received_state_vector_listeners
data = @getHB(res.data)
hb = data.hb
_hb = []
# always broadcast, when not synced.
# This reduces errors, when the clients goes offline prematurely.
# When this client only syncs to one other clients, but looses connectors,
# before syncing to the other clients, the online clients have different states.
# Since we do not want to perform regular syncs, this is a good alternative
if @is_synced
sendApplyHB = (m)=>
@send sender, m
else
sendApplyHB = (m)=>
@broadcast m
for o in hb
_hb.push o
if _hb.length > 10
sendApplyHB
sync_step: "applyHB_"
data: _hb
_hb = []
sendApplyHB
sync_step : "applyHB"
data: _hb
if res.send_again? and @perform_send_again
send_again = do (sv = data.state_vector)=>
()=>
hb = @getHB(sv).hb
for o in hb
_hb.push o
if _hb.length > 10
@send sender,
sync_step: "applyHB_"
data: _hb
_hb = []
@send sender,
sync_step: "applyHB",
data: _hb
sent_again: "true"
setTimeout send_again, 3000
else if res.sync_step is "applyHB"
@applyHB(res.data, sender is @current_sync_target)
if (@syncMethod is "syncAll" or res.sent_again?) and (not @is_synced) and ((@current_sync_target is sender) or (not @current_sync_target?))
@connections[sender].is_synced = true
@findNewSyncTarget()
else if res.sync_step is "applyHB_"
@applyHB(res.data, sender is @current_sync_target)
# Currently, the HB encodes operations as JSON. For the moment I want to keep it
# that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
# too much overhead. Y is very likely to get changed a lot in the future
#
# Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
# we encode the JSON as XML.
#
# When the HB support encoding as XML, the format should look pretty much like this.
# does not support primitive values as array elements
# expects an ltx (less than xml) object
parseMessageFromXml: (m)->
parse_array = (node)->
for n in node.children
if n.getAttribute("isArray") is "true"
parse_array n
else
parse_object n
parse_object = (node)->
json = {}
for name, value of node.attrs
int = parseInt(value)
if isNaN(int) or (""+int) isnt value
json[name] = value
else
json[name] = int
for n in node.children
name = n.name
if n.getAttribute("isArray") is "true"
json[name] = parse_array n
else
json[name] = parse_object n
json
parse_object m
# encode message in xml
# we use string because Strophe only accepts an "xml-string"..
# So {a:4,b:{c:5}} will look like
# <y a="4">
# <b c="5"></b>
# </y>
# m - ltx element
# json - guess it ;)
#
encodeMessageToXml: (m, json)->
# attributes is optional
encode_object = (m, json)->
for name,value of json
if not value?
# nop
else if value.constructor is Object
encode_object m.c(name), value
else if value.constructor is Array
encode_array m.c(name), value
else
m.setAttribute(name,value)
m
encode_array = (m, array)->
m.setAttribute("isArray","true")
for e in array
if e.constructor is Object
encode_object m.c("array-element"), e
else
encode_array m.c("array-element"), e
m
if json.constructor is Object
encode_object m.c("y",{xmlns:"http://y.ninja/connector-stanza"}), json
else if json.constructor is Array
encode_array m.c("y",{xmlns:"http://y.ninja/connector-stanza"}), json
else
throw new Error "I can't encode this json!"
setIsBoundToY: ()->
@on_bound_to_y?()
delete @when_bound_to_y
@is_bound_to_y = true

View File

@@ -61,12 +61,15 @@ class Engine
# TODO: make this more efficient!!
# - operations may only executed in order by creator, order them in object of arrays (key by creator)
# - you can probably make something like dependencies (creator1 waits for creator2)
applyOp: (op_json_array)->
applyOp: (op_json_array, fromHB = false)->
if op_json_array.constructor isnt Array
op_json_array = [op_json_array]
for op_json in op_json_array
if fromHB
op_json.fromHB = "true" # execute immediately, if
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
o = @parseOperation op_json
o.parsed_from_json = op_json
if op_json.fromHB?
o.fromHB = op_json.fromHB
# @HB.addOperation o

View File

@@ -22,19 +22,32 @@ class HistoryBuffer
@reserved_identifier_counter = 0
setTimeout @emptyGarbage, @garbageCollectTimeout
resetUserId: (id)->
own = @buffer[@user_id]
if own?
for o_name,o of own
o.uid.creator = id
if @buffer[id]?
throw new Error "You are re-assigning an old user id - this is not (yet) possible!"
@buffer[id] = own
delete @buffer[@user_id]
# At the beginning (when the user id was not assigned yet),
# the operations are added to buffer._temp. When you finally get your user id,
# the operations are copies from buffer._temp to buffer[id]. Furthermore, when buffer[id] does already contain operations
# (because of a previous session), the uid.op_numbers of the operations have to be reassigned.
# This is what this function does. It adds them to buffer[id],
# and assigns them the correct uid.op_number and uid.creator
setUserId: (@user_id, state_vector)->
@buffer[@user_id] ?= []
buff = @buffer[@user_id]
# we assumed that we started with counter = 0.
# when we receive tha state_vector, and actually have
# counter = 10. Then we have to add 10 to every op_counter
counter_diff = state_vector[@user_id] or 0
if @buffer._temp?
for o_name,o of @buffer._temp
o.uid.creator = @user_id
o.uid.op_number += counter_diff
buff[o.uid.op_number] = o
@operation_counter[@user_id] = (@operation_counter._temp or 0) + counter_diff
delete @operation_counter._temp
delete @buffer._temp
@operation_counter[id] = @operation_counter[@user_id]
delete @operation_counter[@user_id]
@user_id = id
emptyGarbage: ()=>
for o in @garbage
@@ -81,7 +94,6 @@ class HistoryBuffer
{
creator : '_'
op_number : "_#{@reserved_identifier_counter++}"
doSync: false
}
#
@@ -113,8 +125,10 @@ class HistoryBuffer
for u_name,user of @buffer
# TODO next, if @state_vector[user] <= state_vector[user]
if u_name is "_"
continue
for o_number,o of user
if o.uid.doSync and unknown(u_name, o_number)
if (not o.uid.noOperation?) and unknown(u_name, o_number)
# its necessary to send it, and not known in state_vector
o_json = o._encode()
if o.next_cl? # applies for all ops but the most right delimiter!
@@ -146,7 +160,6 @@ class HistoryBuffer
uid =
'creator' : user_id
'op_number' : @operation_counter[user_id]
'doSync' : true
@operation_counter[user_id]++
uid
@@ -196,25 +209,19 @@ class HistoryBuffer
# you renew your own state_vector to the state_vector of the other user
renewStateVector: (state_vector)->
for user,state of state_vector
if (not @operation_counter[user]?) or (@operation_counter[user] < state_vector[user])
if ((not @operation_counter[user]?) or (@operation_counter[user] < state_vector[user])) and state_vector[user]?
@operation_counter[user] = state_vector[user]
#
# Increment the operation_counter that defines the current state of the Engine.
#
addToCounter: (o)->
if not @operation_counter[o.uid.creator]?
@operation_counter[o.uid.creator] = 0
if typeof o.uid.op_number is 'number' and o.uid.creator isnt @getUserId()
# TODO: check if operations are send in order
if o.uid.op_number is @operation_counter[o.uid.creator]
@operation_counter[o.uid.creator]++
else
@invokeSync o.uid.creator
#if @operation_counter[o.uid.creator] isnt (o.uid.op_number + 1)
#console.log (@operation_counter[o.uid.creator] - (o.uid.op_number + 1))
#console.log o
#throw new Error "You don't receive operations in the proper order. Try counting like this 0,1,2,3,4,.. ;)"
@operation_counter[o.uid.creator] ?= 0
# TODO: check if operations are send in order
if o.uid.op_number is @operation_counter[o.uid.creator]
@operation_counter[o.uid.creator]++
while @buffer[o.uid.creator][@operation_counter[o.uid.creator]]?
@operation_counter[o.uid.creator]++
undefined
module.exports = HistoryBuffer

74
lib/ObjectType.coffee Normal file
View File

@@ -0,0 +1,74 @@
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 [*] Depends on the value of the property.
#
# @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()
#
@@ -111,7 +153,12 @@ module.exports = (HB)->
if not @uid.noOperation?
@uid
else
@uid.alt # could be (safely) undefined
if @uid.alt? # could be (safely) undefined
map_uid = @uid.alt.cloneUid()
map_uid.sub = @uid.sub
map_uid
else
undefined
cloneUid: ()->
uid = {}
@@ -119,9 +166,6 @@ module.exports = (HB)->
uid[n] = v
uid
dontSync: ()->
@uid.doSync = false
#
# @private
# If not already done, set the uid
@@ -129,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
@@ -159,20 +206,33 @@ 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
# could exist multiple classes of $Operation
#
if op?.execute?
# is instantiated
@[name] = op
else if op?
if not op?
# nop
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..)
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
@@ -183,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"
@@ -240,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
@@ -254,17 +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: (uid, prev_cl, next_cl, origin, parent)->
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
@@ -272,10 +384,34 @@ module.exports = (HB)->
@saveOperation 'origin', origin
else
@saveOperation 'origin', prev_cl
super uid
super custom_type, uid, content, content_operations
type: "Insert"
val: ()->
@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
# @private
@@ -283,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?
@@ -293,8 +429,8 @@ 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()
@@ -314,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.
@@ -340,10 +488,16 @@ module.exports = (HB)->
if not @validateSavedOperations()
return false
else
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
if not @origin?
@origin = @prev_cl
else if @origin is "Delimiter"
@origin = @parent.beginning
if not @next_cl?
@next_cl = @parent.end
@@ -397,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.
#
@@ -425,51 +561,42 @@ 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++
prev = prev.prev_cl
position
#
# @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
#
# Convert all relevant information of this operation to the json-format.
# This result can be send to other clients.
#
_encode: (json = {})->
json.prev = @prev_cl.getUid()
json.next = @next_cl.getUid()
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} content
#
constructor: (uid, @content)->
super uid
if @origin.type is "Delimiter"
json.origin = "Delimiter"
else if @origin isnt @prev_cl
json.origin = @origin.getUid()
type: "ImmutableObject"
# if not (json.prev? and json.next?)
json.parent = @parent.getUid()
#
# @return [String] The content of this operation.
#
val : ()->
@content
super json
#
# 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)->
ops.Insert.parse = (json)->
{
'uid' : uid
'content' : content
'content_operations' : content_operations
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'parent' : parent
} = json
new this(uid, content)
new this null, content, content_operations, parent, uid, prev, next, origin
#
# @nodoc
@@ -477,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)
@@ -487,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"
@@ -536,7 +663,7 @@ module.exports = (HB)->
'next' : @next_cl?.getUid()
}
types.Delimiter.parse = (json)->
ops.Delimiter.parse = (json)->
{
'uid' : uid
'prev' : prev
@@ -546,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,533 @@
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
# check if tmp_composition_ref already exists
if @tmp_composition_ref
composition_ref = @HB.getOperation @tmp_composition_ref
if composition_ref?
delete @tmp_composition_ref
@composition_ref = composition_ref
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,148 +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.Array
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,357 +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 = @
map_uid = @cloneUid()
map_uid.sub = property_name
rm_uid =
noOperation: true
alt: map_uid
rm = new types.ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB
@map[property_name] = rm
rm.setParent @, property_name
rm.execute()
@map[property_name]
#
# @nodoc
# Manages a list of Insert-type operations.
#
class 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"
#
# @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
result.push o
o = o.next_cl
result
#
# 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() or not (o instanceof types.Delimiter)
o = o.prev_cl
break
if position <= 0 and not o.isDeleted()
break
o = o.next_cl
if not o.isDeleted()
position -= 1
o
#
# @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)->
# see encode to see, why we are doing it this way
if content? and content.creator?
@saveOperation 'content', content
else
@content = content
@saveOperation 'parent', parent
super 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()
'origin' : @origin.getUid()
'uid' : @getUid()
'is_deleted': @is_deleted
}
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,522 +0,0 @@
structured_types_uninitialized = require "./StructuredTypes"
module.exports = (HB)->
structured_types = structured_types_uninitialized HB
types = structured_types.types
parser = structured_types.parser
#
# @nodoc
# Extends the basic Insert type to an operation that holds a text value
#
class types.TextInsert extends types.Insert
#
# @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (content, uid, prev, next, origin, parent)->
if content?.creator
@saveOperation 'content', content
else
@content = content
super uid, prev, next, origin, parent
type: "TextInsert"
#
# Retrieve the effective length of the $content of this operation.
#
getLength: ()->
if @isDeleted()
0
else
@content.length
applyDelete: ()->
super # no braces indeed!
if @content instanceof types.Operation
@content.applyDelete()
@content = null
execute: ()->
if not @validateSavedOperations()
return false
else
if @content instanceof types.Operation
@content.insert_parent = @
super()
#
# The result will be concatenated with the results from the other insert operations
# in order to retrieve the content of the engine.
# @see HistoryBuffer.toExecutedArray
#
val: (current_position)->
if @isDeleted() or not @content?
""
else
@content
#
# 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()
'origin': @origin.getUid()
'parent': @parent.getUid()
}
if @content?.getUid?
json['content'] = @content.getUid()
else
json['content'] = @content
json
types.TextInsert.parse = (json)->
{
'content' : content
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'parent' : parent
} = json
new types.TextInsert content, uid, prev, next, origin, parent
class types.Array extends types.ListManager
type: "Array"
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.Array
o.toJson(transform_to_value)
else if transform_to_value and o instanceof types.Operation
o.val()
else
o
val: (pos)->
if pos?
o = @getOperationByPosition(pos+1)
if not (o instanceof types.Delimiter)
o.val()
else
throw new Error "this position does not exist"
else
o = @beginning.next_cl
result = []
while o isnt @end
if not o.isDeleted()
result.push o.val()
o = o.next_cl
result
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.TextInsert content, undefined, left, right).execute()
else
for c in content
tmp = (new types.TextInsert createContent(c, options), undefined, left, right).execute()
left = tmp
@
#
# Inserts a string into the word.
#
# @return {Array Type} This String object.
#
insert: (position, content, options)->
ith = @getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@insertAfter ith, [content], options
#
# Deletes a part of the word.
#
# @return {Array 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.Array.parse = (json)->
{
'uid' : uid
} = json
new this(uid)
types.Array.create = (content, mutable)->
if (mutable is "mutable")
list = new types.Array().execute()
ith = list.getOperationByPosition 0
list.insertAfter ith, content
list
else if (not mutable?) or (mutable is "immutable")
content
else
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
#
# Handles a String-like data structures with support for insert/delete at a word-position.
# @note Currently, only Text is supported!
#
class types.String extends types.Array
#
# @private
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (uid)->
@textfields = []
super uid
#
# Identifies this class.
# Use it to check whether this is a word-type or something else.
#
# @example
# var x = 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: ()->
c = for o in @toArray()
if o.val?
o.val()
else
""
c.join('')
#
# Same as String.val
# @see String.val
#
toString: ()->
@val()
#
# Inserts a string into the word.
#
# @return {Array Type} This String object.
#
insert: (position, content, options)->
ith = @getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@insertAfter ith, content, options
#
# Bind this String to a textfield or input field.
#
# @example
# var textbox = document.getElementById("textfield");
# 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)->
s = dom_root.getSelection()
clength = textfield.textContent.length
left = Math.min s.anchorOffset, clength
right = Math.min s.focusOffset, clength
if fix?
left = fix left
right = fix right
{
left: left
right: right
isReal: true
}
writeRange = (range)->
writeContent word.val()
textnode = textfield.childNodes[0]
if range.isReal and textnode?
if range.left < 0
range.left = 0
range.right = Math.max range.left, range.right
if range.right > textnode.length
range.right = textnode.length
range.left = Math.min range.left, range.right
r = document.createRange()
r.setStart(textnode, range.left)
r.setEnd(textnode, range.right)
s = window.getSelection()
s.removeAllRanges()
s.addRange(r)
writeContent = (content)->
append = ""
if content[content.length - 1] is " "
content = content.slice(0,content.length-1)
append = '&nbsp;'
textfield.textContent = content
textfield.innerHTML += append
writeContent this.val()
@observe (events)->
for event in events
if 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.key?
if event.charCode is 32
char = " "
else if event.keyCode is 13
char = '\n'
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,6 +1,4 @@
Y = require './y'
bindToChildren = (that)->
for i in [0...that.children.length]
attr = that.children.item(i)
@@ -25,7 +23,7 @@ Polymer "y-object",
bindToChildren @
valChanged: ()->
if @val? and @val.type is "Object"
if @val? and @val._name is "Object"
bindToChildren @
connectorChanged: ()->
@@ -37,23 +35,21 @@ Polymer "y-property",
ready: ()->
if @val? and @name?
if @val.constructor is Object
@val = @parentElement.val(@name,@val).val(@name)
# TODO: please use instanceof instead of .type,
@val = @parentElement.val(@name,new Y.Object(@val)).val(@name)
# TODO: please use instanceof instead of ._name,
# since it is more safe (consider someone putting a custom Object type here)
else if typeof @val is "string"
@parentElement.val(@name,@val)
if @val.type is "Object"
if @val._name is "Object"
bindToChildren @
valChanged: ()->
if @val? and @name?
if @val.constructor is Object
@val = @parentElement.val.val(@name,@val).val(@name)
# TODO: please use instanceof instead of .type,
@val = @parentElement.val.val(@name, new Y.Object(@val)).val(@name)
# TODO: please use instanceof instead of ._name,
# since it is more safe (consider someone putting a custom Object type here)
else if @val.type is "Object"
else if @val._name is "Object"
bindToChildren @
else if @parentElement.val?.val? and @val isnt @parentElement.val.val(@name)
@parentElement.val.val @name, @val

View File

@@ -1,48 +1,38 @@
json_types_uninitialized = require "./Types/JsonTypes"
structured_ops_uninitialized = require "./Operations/Structured"
HistoryBuffer = require "./HistoryBuffer"
Engine = require "./Engine"
adaptConnector = require "./ConnectorAdapter"
createY = (connector)->
user_id = null
if connector.id?
user_id = connector.id # TODO: change to getUniqueId()
if connector.user_id?
user_id = connector.user_id # TODO: change to getUniqueId()
else
user_id = "_temp"
connector.whenUserIdSet (id)->
user_id = id
HB.resetUserId id
connector.when_received_state_vector_listeners = [(state_vector)->
HB.setUserId this.user_id, state_vector
]
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

@@ -1,10 +1,11 @@
{
"name": "yjs",
"version": "0.3.0",
"version": "0.5.2",
"description": "A Framework that enables Real-Time Collaboration on arbitrary data structures.",
"main": "./build/node/y.js",
"scripts": {
"test": "./node_modules/.bin/gulp test"
"prepublish": "./node_modules/gulp/bin/gulp.js build_node",
"test": "./node_modules/gulp/bin/gulp.js mocha"
},
"repository": {
"type": "git",
@@ -13,7 +14,6 @@
"keywords": [
"OT",
"collaboration",
"Yata",
"synchronization",
"ShareJS",
"Coweb",
@@ -29,9 +29,7 @@
"dependencies": {
},
"devDependencies": {
"codo": "^2.0.9",
"underscore": "^1.6.0",
"chai": "^1.9.1",
"chai": "^2.2.0",
"codo": "^2.0.9",
"coffee-errors": "~0.8.6",
"coffee-script": "^1.7.1",
@@ -45,12 +43,12 @@
"gulp-concat": "^2.3.4",
"gulp-copy": "0.0.2",
"gulp-debug": "^1.0.0",
"gulp-exit": "0.0.2",
"gulp-git": "^0.5.0",
"gulp-if": "^1.2.4",
"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",
@@ -59,8 +57,9 @@
"gulp-uglify": "^0.3.1",
"gulp-watch": "^3.0.0",
"jquery": "^2.1.1",
"mocha": "^1.21.4",
"sinon": "^1.10.2",
"sinon-chai": "^2.5.0"
"underscore": "^1.6.0",
"mocha": "^2.1.0",
"sinon": "^1.12.2",
"sinon-chai": "^2.7.0"
}
}

View File

@@ -1,42 +1,50 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
chai.use(sinonChai)
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
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 = 3 * @number_of_test_cases_multiplier
@repeat_this = 1 * @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.setManualGarbageCollect()
user._model.HB.stopGarbageCollection()
user
getSomeUser: ()->
@@ -70,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)
@@ -107,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)
@@ -120,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)
@@ -136,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)
@@ -147,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)
@@ -173,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
@@ -184,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()
@@ -197,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,119 +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-connectors/lib/y-test/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", ->
beforeEach (done)->
@timeout 50000
@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()

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 "../../Yatta-Connectors/lib/test-connector/test-connector.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,89 +7,128 @@ _ = require("underscore")
chai.use(sinonChai)
Connector = require "../../y-connectors/lib/y-test/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.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]
]
module.exports = ObjectTest
describe "Object Test", ->
@timeout 500000
describe "JsonFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new JsonTest()
@yTest = new ObjectTest()
@users = @yTest.users
@test_user = @yTest.makeNewUser "test_user"
@@ -103,22 +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.HB.renewStateVector u2.HB.getOperationCounter()
u2.HB.renewStateVector u1.HB.getOperationCounter()
u1.engine.applyOps ops2
u2.engine.applyOps ops1
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')
@@ -126,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')
@@ -138,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"
@@ -159,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"
@@ -182,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
@@ -202,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)->
@@ -237,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)->
@@ -270,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()

View File

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

File diff suppressed because one or more lines are too long

3
y.js

File diff suppressed because one or more lines are too long