diff --git a/.travis.yml b/.travis.yml index 45b66d72ad320b823262d910d36d8af081724788..7a150b62901a166ecd1551937b0d141c62d03a03 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 c618007beef4b8f73a1a3478a2d8555b404bb293..34b41376b98509aad02ca5e0a451204e5ee31004 100644 Binary files a/README.md and b/README.md differ diff --git a/gulpfile.js b/gulpfile.js index 317ac0a81959b679f5edb12867f9fdc1c05725e6..5e02ceab203dfa11fd6a15d1d0a8f12190c3d05a 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 017b1156a72b4ebbc6781afa8cd248ac11d07d02..88ca708de5cc6803406e72c38afef62468406761 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 - // <y a="4"> - // <b c="5"></b> - // </y> - // 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 + <y a="4"> + <b c="5"></b> + </y> + 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 0000000000000000000000000000000000000000..f9d2236e0435d106ed94efa6b8d03acb638fc6b8 --- /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 6bbba9a3f45ec56fde35ba0c078bc51a1b5b1ec8..37031f6acd9adae5b6195e461433de3d23bdb770 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 f219779a8590153083157fe23acfd142b8188b82..ca36f678ec746a2a1c434736bdd4728e0e9fb0c3 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 f34e75a519ade9a8169cae3b66b24f6246c2255c..f20593433a2a93e78fe7333fdfd773ad747547f6 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 e2f73092893a7c325d8eb47ab537e5c2f967b6d8..cb769b111d1d38f2c13d65c92f6e4e6df5477aeb 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 77262bf0d6f4b97566673b8e82976f6c6b392c55..378061774f3fe4610cc97eff991a248064fbfbbd 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 81a3089b4f96c2ec10f358c1a7eb1241c97608fb..0008f941fd0fdb6336a0753bae174e2c4cb945a3 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()