diff --git a/.travis.yml b/.travis.yml
index 45b66d72..7a150b62 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,12 +1,8 @@
language: node_js
before_install:
- "npm install -g bower"
- - "bower install"
node_js:
- "0.12"
- - "0.11"
- - "0.10"
branches:
only:
- master
- - 0.6
diff --git a/README.md b/README.md
index c618007b..34b41376 100644
Binary files a/README.md and b/README.md differ
diff --git a/gulpfile.js b/gulpfile.js
index 317ac0a8..5e02ceab 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -28,8 +28,8 @@
Specify which specs to use!
Commands:
- - build
- Build this library
+ - build:deploy
+ Build this library for deployment (es6->es5, minified)
- dev:browser
Watch the ./src directory.
Builds the library on changes.
diff --git a/src/Connector.js b/src/Connector.js
index 017b1156..88ca708d 100644
--- a/src/Connector.js
+++ b/src/Connector.js
@@ -130,7 +130,7 @@ class AbstractConnector {
}
send (uid, message) {
if (this.debug) {
- console.log(`send ${this.userId} -> ${uid}: ${message.type}`, m);// eslint-disable-line
+ console.log(`send ${this.userId} -> ${uid}: ${message.type}`, m) // eslint-disable-line
}
}
/*
@@ -141,7 +141,7 @@ class AbstractConnector {
return
}
if (this.debug) {
- console.log(`receive ${sender} -> ${this.userId}: ${m.type}`, m);// eslint-disable-line
+ console.log(`receive ${sender} -> ${this.userId}: ${m.type}`, m) // eslint-disable-line
}
if (m.type === 'sync step 1') {
// TODO: make transaction, stream the ops
@@ -212,17 +212,19 @@ class AbstractConnector {
this.y.db.apply(m.ops)
}
}
- // Currently, the HB encodes operations as JSON. For the moment I want to keep it
- // that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
- // too much overhead. Y is very likely to get changed a lot in the future
- //
- // Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
- // we encode the JSON as XML.
- //
- // When the HB support encoding as XML, the format should look pretty much like this.
- //
- // does not support primitive values as array elements
- // expects an ltx (less than xml) object
+ /*
+ Currently, the HB encodes operations as JSON. For the moment I want to keep it
+ that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
+ too much overhead. Y is very likely to get changed a lot in the future
+
+ Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
+ we encode the JSON as XML.
+
+ When the HB support encoding as XML, the format should look pretty much like this.
+
+ does not support primitive values as array elements
+ expects an ltx (less than xml) object
+ */
parseMessageFromXml (m) {
function parseArray (node) {
for (var n of node.children) {
@@ -256,14 +258,16 @@ class AbstractConnector {
}
parseObject(m)
}
- // encode message in xml
- // we use string because Strophe only accepts an "xml-string"..
- // So {a:4,b:{c:5}} will look like
- //
- //
- //
- // m - ltx element
- // json - Object
+ /*
+ encode message in xml
+ we use string because Strophe only accepts an "xml-string"..
+ So {a:4,b:{c:5}} will look like
+
+
+
+ m - ltx element
+ json - Object
+ */
encodeMessageToXml (msg, obj) {
// attributes is optional
function encodeObject (m, json) {
diff --git a/src/Notes.md b/src/Notes.md
new file mode 100644
index 00000000..f9d2236e
--- /dev/null
+++ b/src/Notes.md
@@ -0,0 +1,12 @@
+
+# Notes
+
+### Terminology
+
+* DB: DataBase that holds all the information of the shared object. It is devided into the OS, DS, and SS. This can be a persistent database or an in-memory database. Depending on the type of database, it could make sense to store OS, DS, and SS in different tables, or maybe different databases.
+* OS: OperationStore holds all the operations. An operation is a js object with a fixed number of name fields.
+* DS: DeleteStore holds the information about which operations are deleted and which operations were garbage collected (no longer available in the OS).
+* SS: StateSet holds the current state of the OS. SS.getState(username) refers to the amount of operations that were received by that respective user.
+* Op: Operation defines an action on a shared type. But it is also the format in which we store the model of a type. This is why it is also called a Struct/Structure.
+* Type and Structure: We crearly distinguish between type and structure. Short explanation: A type (e.g. Strings, Numbers) have a number of functions that you can apply on them. (+) is well defined on both of them. They are *modeled* by a structure - the functions really change the structure of a type. Types can be implemented differently but still provide the same functionality. In Yjs, almost all types are realized as a doubly linked list (on which Yjs can provide eventual convergence)
+*
\ No newline at end of file
diff --git a/src/OperationStore.js b/src/OperationStore.js
index 6bbba9a3..37031f6a 100644
--- a/src/OperationStore.js
+++ b/src/OperationStore.js
@@ -3,6 +3,9 @@
/*
Partial definition of a transaction
+
+ A transaction provides all the the async functionality on a database.
+
By convention, a transaction has the following properties:
* ss for StateSet
* os for OperationStore
@@ -75,6 +78,10 @@ class AbstractTransaction {
constructor (store) {
this.store = store
}
+ /*
+ Get a type based on the id of its model.
+ If it does not exist yes, create it.
+ */
* getType (id) {
var sid = JSON.stringify(id)
var t = this.store.initializedTypes[sid]
@@ -87,12 +94,11 @@ class AbstractTransaction {
}
return t
}
- * createType (model) {
- var sid = JSON.stringify(model.id)
- var t = yield* Y[model.type].initType.call(this, this.store, model)
- this.store.initializedTypes[sid] = t
- return t
- }
+ /*
+ Apply operations that this user created (no remote ones!)
+ * does not check for Struct.*.requiredOps()
+ * also broadcasts it through the connector
+ */
* applyCreatedOperations (ops) {
var send = []
for (var i = 0; i < ops.length; i++) {
@@ -108,24 +114,13 @@ class AbstractTransaction {
}
}
/*
- Delete an operation from the OS, and add it to the GC, if necessary.
-
- Rulez:
- * The most left element in a list must not be deleted.
- => There is at least one element in the list
- * When an operation o is deleted, then it checks if its right operation
- can be gc'd (iff it's deleted)
+ Mark an operation as deleted, and add it to the GC, if possible.
*/
* deleteOperation (targetId) {
var target = yield* this.getOperation(targetId)
if (target == null || !target.deleted) {
this.ds.markDeleted(targetId)
- var state = yield* this.getState(targetId[0])
- if (state.clock === targetId[1]) {
- yield* this.checkDeleteStoreForState(state)
- yield* this.setState(state)
- }
}
if (target != null && target.gc == null) {
@@ -143,23 +138,16 @@ class AbstractTransaction {
var left = target.left != null ? yield* this.getOperation(target.left) : null
var right = target.right != null ? yield* this.getOperation(target.right) : null
- this.store.addToGarbageCollector(target, left, right)
+ this.store.addToGarbageCollector(target, left)
// set here because it was deleted and/or gc'd
yield* this.setOperation(target)
- if (
- left != null &&
- left.left != null &&
- this.store.addToGarbageCollector(left, yield* this.getOperation(left.left), target)
- ) {
- yield* this.setOperation(left)
- }
-
+ // check if it is possible to add right to the gc (this delete can't be responsible for left being gc'd)
if (
right != null &&
right.right != null &&
- this.store.addToGarbageCollector(right, target, yield* this.getOperation(right.right))
+ this.store.addToGarbageCollector(right, target)
) {
yield* this.setOperation(right)
}
@@ -176,31 +164,45 @@ class AbstractTransaction {
yield* this.deleteOperation(id)
o = yield* this.getOperation(id)
}
+
+ // check to increase the state of the respective user
+ var state = yield* this.getState(id[0])
+ if (state.clock === id[1]) {
+ // also check if more expected operations were gc'd
+ yield* this.checkDeleteStoreForState(state)
+ // then set the state
+ yield* this.setState(state)
+ }
+ // remove gc'd op from the left op, if it exists
if (o.left != null) {
var left = yield* this.getOperation(o.left)
left.right = o.right
yield* this.setOperation(left)
}
+ // remove gc'd op from the right op, if it exists
if (o.right != null) {
var right = yield* this.getOperation(o.right)
right.left = o.left
yield* this.setOperation(right)
}
+ // remove gc'd op from parent, if it exists
var parent = yield* this.getOperation(o.parent)
- var setParent = false
+ var setParent = false // whether to save parent to the os
if (Y.utils.compareIds(parent.start, o.id)) {
+ // gc'd op is the start
setParent = true
parent.start = o.right
}
if (Y.utils.compareIds(parent.end, o.id)) {
+ // gc'd op is the end
setParent = true
parent.end = o.left
}
if (setParent) {
yield* this.setOperation(parent)
}
- yield* this.removeOperation(o.id)
+ yield* this.removeOperation(o.id) // actually remove it from the os
yield* this.ds.markGarbageCollected(o.id)
}
}
@@ -272,10 +274,9 @@ class AbstractOperationStore {
var os = this.os
var self = this
os.iterate(null, null, function (op) {
- if (op.deleted && op.left != null && op.right != null) {
+ if (op.deleted && op.left != null) {
var left = os.find(op.left)
- var right = os.find(op.right)
- self.addToGarbageCollector(op, left, right)
+ self.addToGarbageCollector(op, left)
}
})
}
@@ -283,25 +284,21 @@ class AbstractOperationStore {
Try to add to GC.
TODO: rename this function
-
- Only gc when
- * creator of op is online
- * left & right defined and both are from the same creator as op
-
+
+ Rulez:
+ * Only gc if this user is online
+ * The most left element in a list must not be gc'd.
+ => There is at least one element in the list
+
returns true iff op was added to GC
*/
- addToGarbageCollector (op, left, right) {
+ addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true &&
this.y.connector.isSynced &&
- // (this.y.connector.connections[op.id[0]] != null || op.id[0] === this.y.connector.userId) &&
left != null &&
- right != null &&
left.deleted &&
- right.deleted &&
- left.id[0] === op.id[0] &&
- right.id[0] === op.id[0]
) {
op.gc = true
this.gc1.push(op.id)
@@ -343,23 +340,25 @@ class AbstractOperationStore {
}
return [this.userId, this.opClock++]
}
+ /*
+ Apply a list of operations.
+
+ * get a transaction
+ * check whether all Struct.*.requiredOps are in the OS
+ * check if it is an expected op (otherwise wait for it)
+ * check if was deleted, apply a delete operation after op was applied
+ */
apply (ops) {
for (var key in ops) {
var o = ops[key]
- if (o.gc == null) { // TODO: why do i get the same op twice?
- if (o.deleted == null) {
- var required = Y.Struct[o.struct].requiredOps(o)
- this.whenOperationsExist(required, o)
- } else {
- throw new Error('Ops must not contain deleted field!')
- }
- } else {
- throw new Error("Must not receive gc'd ops!")
- }
+ var required = Y.Struct[o.struct].requiredOps(o)
+ this.whenOperationsExist(required, o)
}
}
- // op is executed as soon as every operation requested is available.
- // Note that Transaction can (and should) buffer requests.
+ /*
+ op is executed as soon as every operation requested is available.
+ Note that Transaction can (and should) buffer requests.
+ */
whenOperationsExist (ids, op) {
if (ids.length > 0) {
let listener = {
@@ -390,7 +389,7 @@ class AbstractOperationStore {
this.listenersByIdRequestPending = true
var store = this
- this.requestTransaction(function *() {
+ this.requestTransaction(function * () {
var exeNow = store.listenersByIdExecuteNow
store.listenersByIdExecuteNow = []
@@ -421,6 +420,13 @@ class AbstractOperationStore {
}
})
}
+ /*
+ Actually execute an operation, when all expected operations are available.
+ If op is not yet expected, add it to the list of waiting operations.
+
+ This will also try to execute waiting operations
+ (ops that were not expected yet), after it was applied
+ */
* tryExecute (op) {
if (op.struct === 'Delete') {
yield* Y.Struct.Delete.execute.call(this, op)
@@ -439,6 +445,7 @@ class AbstractOperationStore {
yield* this.addOperation(op)
yield* this.store.operationAdded(this, op)
+ // Delete if DS says this is actually deleted
if (this.store.ds.isDeleted(op.id)) {
yield* Y.Struct['Delete'].execute.call(this, {struct: 'Delete', target: op.id})
}
@@ -479,21 +486,5 @@ class AbstractOperationStore {
yield* t._changed(transaction, Y.utils.copyObject(op))
}
}
- removeParentListener (id, f) {
- var ls = this.parentListeners[id]
- if (ls != null) {
- this.parentListeners[id] = ls.filter(function (g) {
- return (f !== g)
- })
- }
- }
- addParentListener (id, f) {
- var ls = this.parentListeners[JSON.stringify(id)]
- if (ls == null) {
- ls = []
- this.parentListeners[JSON.stringify(id)] = ls
- }
- ls.push(f)
- }
}
Y.AbstractOperationStore = AbstractOperationStore
diff --git a/src/OperationStores/IndexedDB.js b/src/OperationStores/IndexedDB.js
index f219779a..ca36f678 100644
--- a/src/OperationStores/IndexedDB.js
+++ b/src/OperationStores/IndexedDB.js
@@ -122,7 +122,7 @@ Y.IndexedDB = (function () { // eslint-disable-line
}
})()
- function handleTransactions (t) { // eslint-disable-line no-unused-vars
+ function handleTransactions (t) {
var request = t.value
if (t.done) {
return
diff --git a/src/Types/Array.js b/src/Types/Array.js
index f34e75a5..f2059343 100644
--- a/src/Types/Array.js
+++ b/src/Types/Array.js
@@ -167,15 +167,16 @@
Y.Array = new Y.utils.CustomType({
class: YArray,
createType: function * YArrayCreator () {
+ var modelid = this.store.getNextOpId()
var model = {
struct: 'List',
type: 'Array',
start: null,
end: null,
- id: this.store.getNextOpId()
+ id: modelid
}
yield* this.applyCreatedOperations([model])
- return yield* this.createType(model)
+ return modelid
},
initType: function * YArrayInitializer (os, model) {
var valArray = []
diff --git a/src/Types/Map.js b/src/Types/Map.js
index e2f73092..cb769b11 100644
--- a/src/Types/Map.js
+++ b/src/Types/Map.js
@@ -134,8 +134,9 @@
if (value instanceof Y.utils.CustomType) {
// construct a new type
this.os.requestTransaction(function *() {
- var type = yield* value.createType.call(this)
- insert.opContent = type._model
+ var typeid = yield* value.createType.call(this)
+ var type = yield* this.getType(typeid)
+ insert.opContent = typeid
insert.id = this.store.getNextOpId()
yield* this.applyCreatedOperations([insert])
resolve(type)
@@ -212,14 +213,15 @@
Y.Map = new Y.utils.CustomType({
class: YMap,
createType: function * YMapCreator () {
+ var modelid = this.store.getNextOpId()
var model = {
map: {},
struct: 'Map',
type: 'Map',
- id: this.store.getNextOpId()
+ id: modelid
}
yield* this.applyCreatedOperations([model])
- return yield* this.createType(model)
+ return modelid
},
initType: function * YMapInitializer (os, model) { // eslint-disable-line
return new YMap(os, model)
diff --git a/src/Types/TextBind.js b/src/Types/TextBind.js
index 77262bf0..37806177 100644
--- a/src/Types/TextBind.js
+++ b/src/Types/TextBind.js
@@ -267,15 +267,16 @@
Y.TextBind = new Y.utils.CustomType({
class: YTextBind,
createType: function * YTextBindCreator () {
+ var modelid = this.store.getNextOpId()
var model = {
start: null,
end: null,
struct: 'List',
type: 'TextBind',
- id: this.store.getNextOpId()
+ id: modelid
}
yield* this.applyCreatedOperations([model])
- return yield* this.createType(model)
+ return modelid
},
initType: function * YTextBindInitializer (os, model) {
var valArray = []
diff --git a/src/y.js b/src/y.js
index 81a3089b..0008f941 100644
--- a/src/y.js
+++ b/src/y.js
@@ -37,14 +37,6 @@ class YConfig {
}
reconnect () {
this.connector.reconnect()
- /* TODO: maybe do this..
- Promise.all([
- this.db.garbageCollect(),
- this.db.garbageCollect()
- ]).then(() => {
- this.connector.reconnect()
- })
- */
}
destroy () {
this.connector.disconnect()