From b1d70ef25e665fdc6d4aa302994ef0749154345c Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 17 Sep 2015 00:21:01 +0200 Subject: [PATCH] added comments to most of the classes. --- .gitignore | 5 +- package.json | 18 ++-- spec/support/jasmine.json | 22 ---- src/Connector.js | 22 ++-- src/Connectors/Test.js | 36 +++---- src/Helper.spec.js | 71 ++++++++----- src/OperationStore.js | 153 +++++++++++++++++++++------- src/OperationStores/Memory.js | 34 ++----- src/OperationStores/RedBlackTree.js | 27 +++-- src/Struct.js | 113 ++++++++++---------- src/Types/Array.js | 8 +- src/Types/Array.spec.js | 3 +- src/Types/Map.js | 70 ++++++------- src/Types/Map.spec.js | 1 - src/Utils.js | 130 +++++++++++++++++++---- src/y.js | 17 ++-- 16 files changed, 448 insertions(+), 282 deletions(-) delete mode 100644 spec/support/jasmine.json diff --git a/.gitignore b/.gitignore index 1feb9b5e..568da994 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,10 @@ bower_components build build_test .directory -.c9 .codio .settings .jshintignore .jshintrc .validate.json -y.js -y.js.map +./y.js +./y.js.map diff --git a/package.json b/package.json index c2e83993..2f95ffb8 100644 --- a/package.json +++ b/package.json @@ -41,18 +41,18 @@ }, "homepage": "http://y-js.org", "devDependencies": { - "babel-eslint": "^4.1.1", + "babel-eslint": "^4.1.2", "gulp": "^3.9.0", - "gulp-babel": "^5.1.0", - "gulp-concat": "^2.5.2", + "gulp-babel": "^5.2.1", + "gulp-concat": "^2.6.0", "gulp-jasmine": "^2.0.1", - "gulp-jasmine-browser": "^0.1.3", + "gulp-jasmine-browser": "^0.2.3", "gulp-sourcemaps": "^1.5.2", - "gulp-uglify": "^1.2.0", - "gulp-util": "^3.0.5", - "gulp-watch": "^4.2.4", - "minimist": "^1.1.1", - "pre-commit": "^1.0.10", + "gulp-uglify": "^1.4.1", + "gulp-util": "^3.0.6", + "gulp-watch": "^4.3.5", + "minimist": "^1.2.0", + "pre-commit": "^1.1.1", "standard": "^5.2.2" } } diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json deleted file mode 100644 index 4c904ce0..00000000 --- a/spec/support/jasmine.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "spec_dir": "build", - "spec_files": [ - "**/**.spec.js" - ], - "helpers": [ - "Helper.spec.js", - "y.js", - "Connector.js", - "OperationStore.js", - "Struct.js", - "Utils.js", - "OperationStores/RedBlackTree.js", - "OperationStores/Memory.js", - "OperationStores/IndexedDB.js", - "Connectors/Test.js", - "Connectors/WebRTC.js", - "Types/Array.js", - "Types/Map.js", - "Types/TextBind.js" - ] -} diff --git a/src/Connector.js b/src/Connector.js index 02d46199..912f6845 100644 --- a/src/Connector.js +++ b/src/Connector.js @@ -1,11 +1,12 @@ /* globals Y */ 'use strict' -class AbstractConnector { // eslint-disable-line no-unused-vars +class AbstractConnector { /* - opts - .role : String Role of this client ("master" or "slave") - .userId : String that uniquely defines the user. + opts contains the following information: + role : String Role of this client ("master" or "slave") + userId : String Uniquely defines the user. + debug: Boolean Whether to print debug messages (optional) */ constructor (y, opts) { this.y = y @@ -90,8 +91,11 @@ class AbstractConnector { // eslint-disable-line no-unused-vars this.whenSyncedListeners.push(f) } } - // returns false, if there is no sync target - // true otherwise + /* + + returns false, if there is no sync target + true otherwise + */ findNextSyncTarget () { if (this.currentSyncTarget != null) { return // "The current sync has not finished!" @@ -115,7 +119,7 @@ class AbstractConnector { // eslint-disable-line no-unused-vars }) }) } - // set the state to synced! + // This user synced with at least one user, set the state to synced (TODO: does this suffice?) if (!this.isSynced) { this.isSynced = true for (var f of this.whenSyncedListeners) { @@ -129,7 +133,9 @@ class AbstractConnector { // eslint-disable-line no-unused-vars console.log(`me -> ${uid}: ${message.type}`, m);// eslint-disable-line } } - // You received a raw message, and you know that it is intended for to Yjs. Then call this function. + /* + You received a raw message, and you know that it is intended for Yjs. Then call this function. + */ receiveMessage (sender, m) { if (sender === this.userId) { return diff --git a/src/Connectors/Test.js b/src/Connectors/Test.js index a6b39b66..b40cbbe4 100644 --- a/src/Connectors/Test.js +++ b/src/Connectors/Test.js @@ -83,26 +83,26 @@ class Test extends Y.AbstractConnector { } } flushAll () { - var def = Promise.defer() - // flushes may result in more created operations, - // flush until there is nothing more to flush - function nextFlush () { - var c = flushOne() - if (c) { - while (flushOne()) { - // nop + return new Promise(function (resolve) { + // flushes may result in more created operations, + // flush until there is nothing more to flush + function nextFlush () { + var c = flushOne() + if (c) { + while (flushOne()) { + // nop + } + wait().then(nextFlush) + } else { + wait().then(function () { + resolve() + }) } - wait().then(nextFlush) - } else { - wait().then(function () { - def.resolve() - }) } - } - // in the case that there are - // still actions that want to be performed - wait(0).then(nextFlush) - return def.promise + // in the case that there are + // still actions that want to be performed + wait(0).then(nextFlush) + }) } flushOne () { flushOne() diff --git a/src/Helper.spec.js b/src/Helper.spec.js index d60f0bb8..565b3a8c 100644 --- a/src/Helper.spec.js +++ b/src/Helper.spec.js @@ -3,8 +3,9 @@ /* This is just a compilation of functions that help to test this library! -***/ +*/ +// When testing, you store everything on the global object. We call it g var g if (typeof global !== 'undefined') { g = global @@ -15,20 +16,29 @@ if (typeof global !== 'undefined') { } g.g = g +g.YConcurrency_TestingMode = true + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000 + +/* + Wait for a specified amount of time (in ms). defaults to 5ms +*/ function wait (t) { if (t == null) { t = 5 } - var def = Promise.defer() - setTimeout(function () { - def.resolve() - }, t) - return def.promise + return new Promise(function (resolve) { + setTimeout(function () { + resolve() + }, t) + }) } g.wait = wait -// returns a random element of o -// works on Object, and Array +/* + returns a random element of o. + works on Object, and Array +*/ function getRandom (o) { if (o instanceof Array) { return o[Math.floor(Math.random() * o.length)] @@ -42,7 +52,7 @@ function getRandom (o) { } g.getRandom = getRandom -function getRandomNumber(n) {//eslint-disable-line +function getRandomNumber (n) { if (n == null) { n = 9999 } @@ -50,7 +60,7 @@ function getRandomNumber(n) {//eslint-disable-line } g.getRandomNumber = getRandomNumber -g.applyRandomTransactions = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) { //eslint-disable-line +g.applyRandomTransactions = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) { function randomTransaction (root) { var f = getRandom(transactions) f(root) @@ -86,8 +96,12 @@ g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) { }) g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-line - var s1, s2, ds1, ds2, allDels1, allDels2 - var db1 = [] + var s1, s2 // state sets + var ds1, ds2 // delete sets + var allDels1, allDels2 // all deletions + var db1 = [] // operation store of user1 + + // t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2] function * t1 () { s1 = yield* this.getStateSet() ds1 = yield* this.getDeleteSet() @@ -105,6 +119,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable- }) } yield users[0].connector.flushAll() + // gc two times because of the two gc phases (really collect everything) yield g.garbageCollectAllUsers(users) yield wait(50) yield g.garbageCollectAllUsers(users) @@ -137,7 +152,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable- if (s1 == null) { u.db.requestTransaction(t1) yield wait() - u.db.os.iterate(null, null, function(o){//eslint-disable-line + u.db.os.iterate(null, null, function (o) { db1.push(o) }) } else { @@ -147,22 +162,22 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable- expect(allDels1).toEqual(allDels2) // inner structure expect(ds1).toEqual(ds2) // exported structure var count = 0 - u.db.os.iterate(null, null, function(o){//eslint-disable-line + u.db.os.iterate(null, null, function (o) { expect(db1[count++]).toEqual(o) }) } } }) -g.createUsers = async(function * createUsers (self, numberOfUsers) { //eslint-disable-line - if (Y.utils.globalRoom.users[0] != null) {//eslint-disable-line - yield Y.utils.globalRoom.users[0].flushAll()//eslint-disable-line +g.createUsers = async(function * createUsers (self, numberOfUsers) { + if (Y.utils.globalRoom.users[0] != null) { + yield Y.utils.globalRoom.users[0].flushAll() } // destroy old users - for (var u in Y.utils.globalRoom.users) {//eslint-disable-line - Y.utils.globalRoom.users[u].y.destroy()//eslint-disable-line + for (var u in Y.utils.globalRoom.users) { + Y.utils.globalRoom.users[u].y.destroy() } - self.users = [] + self.users = null var promises = [] for (var i = 0; i < numberOfUsers; i++) { @@ -178,14 +193,18 @@ g.createUsers = async(function * createUsers (self, numberOfUsers) { //eslint-di })) } self.users = yield Promise.all(promises) + return self.users }) +/* + Until async/await arrives in js, we use this function to wait for promises + by yielding them. +*/ function async (makeGenerator) { return function (arg) { var generator = makeGenerator.apply(this, arguments) function handle (result) { - // result => { done: [Boolean], value: [Object] } if (result.done) return Promise.resolve(result.value) return Promise.resolve(result.value).then(function (res) { @@ -194,12 +213,14 @@ function async (makeGenerator) { return handle(generator.throw(err)) }) } - - try { + // this may throw errors here, but its ok since this is used only for debugging + return handle(generator.next()) + /* try { return handle(generator.next()) } catch (ex) { - return Promise.reject(ex) - } + generator.throw(ex) // TODO: check this out + // return Promise.reject(ex) + }*/ } } g.async = async diff --git a/src/OperationStore.js b/src/OperationStore.js index a872f1d1..2b2b3c1b 100644 --- a/src/OperationStore.js +++ b/src/OperationStore.js @@ -1,7 +1,76 @@ /* global Y */ - 'use strict' +/* + Partial definition of a transaction + By convention, a transaction has the following properties: + * ss for StateSet + * os for OperationStore + * ds for DeleteStore + + A transaction must also define the following methods: + * checkDeleteStoreForState(state) + - When increasing the state of a user, an operation with an higher id + may already be garbage collected, and therefore it will never be received. + update the state to reflect this knowledge. This won't call a method to save the state! + * getDeleteSet(id) + - Get the delete set in a readable format: + { + "userX": [ + [5,1], // starting from position 5, one operations is deleted + [9,4] // starting from position 9, four operations are deleted + ], + "userY": ... + } + * isDeleted(id) + * getOpsFromDeleteSet(ds) -- TODO: just call Struct.Delete.delete(id) here + - get a set of deletions that need to be applied in order to get to + achieve the state of the supplied ds + * setOperation(op) + - write `op` to the database. + Note: this is allowed to return an in-memory object. + E.g. the Memory adapter returns the object that it has in-memory. + Changing values on this object will be stored directly in the database + without calling this function. Therefore, + setOperation may have no functionality in some adapters. This also has + implications on the way we use operations that were served from the database. + We try not to call copyObject, if not necessary. + * addOperation(op) + - add an operation to the database. + This may only be called once for every op.id + * getOperation(id) + * removeOperation(id) + - remove an operation from the database. This is called when an operation + is garbage collected. + * setState(state) + - `state` is of the form + { + user: "1", + clock: 4 + } <- meaning that we have four operations from user "1" + (with these id's respectively: 0, 1, 2, and 3) + * getState(user) + * getStateVector() + - Get the state of the OS in the form + [{ + user: "userX", + clock: 11 + }, + .. + ] + * getStateSet() + - Get the state of the OS in the form + { + "userX": 11, + "userY": 22 + } + * getOperations(startSS) + - Get the all the operations that are necessary in order to achive the + stateSet of this user, starting from a stateSet supplied by another user + * makeOperationReady(ss, op) + - this is called only by `getOperations(startSS)`. It makes an operation + applyable on a given SS. +*/ class AbstractTransaction { constructor (store) { this.store = store @@ -41,6 +110,18 @@ class AbstractTransaction { } Y.AbstractTransaction = AbstractTransaction +/* + Partial definition of an OperationStore. + TODO: name it Database, operation store only holds operations. + + A database definition must alse define the following methods: + * logTable() (optional) + - show relevant information information in a table + * requestTransaction(makeGen) + - request a transaction + * destroy() + - destroy the database +*/ class AbstractOperationStore { // eslint-disable-line no-unused-vars constructor (y, opts) { this.y = y @@ -71,44 +152,44 @@ class AbstractOperationStore { // eslint-disable-line no-unused-vars this.gcTimeout = opts.gcTimeout || 5000 var os = this function garbageCollect () { - var def = Promise.defer() - os.requestTransaction(function * () { - for (var i in os.gc2) { - var oid = os.gc2[i] - var o = yield* this.getOperation(oid) - if (o.left != null) { - var left = yield* this.getOperation(o.left) - left.right = o.right - yield* this.setOperation(left) + return new Promise((resolve) => { + os.requestTransaction(function * () { + for (var i in os.gc2) { + var oid = os.gc2[i] + var o = yield* this.getOperation(oid) + if (o.left != null) { + var left = yield* this.getOperation(o.left) + left.right = o.right + yield* this.setOperation(left) + } + if (o.right != null) { + var right = yield* this.getOperation(o.right) + right.left = o.left + yield* this.setOperation(right) + } + var parent = yield* this.getOperation(o.parent) + var setParent = false + if (Y.utils.compareIds(parent.start, o.id)) { + setParent = true + parent.start = o.right + } + if (Y.utils.compareIds(parent.end, o.id)) { + setParent = true + parent.end = o.left + } + if (setParent) { + yield* this.setOperation(parent) + } + yield* this.removeOperation(o.id) } - if (o.right != null) { - var right = yield* this.getOperation(o.right) - right.left = o.left - yield* this.setOperation(right) + os.gc2 = os.gc1 + os.gc1 = [] + if (os.gcTimeout > 0) { + os.gcInterval = setTimeout(garbageCollect, os.gcTimeout) } - var parent = yield* this.getOperation(o.parent) - var setParent = false - if (Y.utils.compareIds(parent.start, o.id)) { - setParent = true - parent.start = o.right - } - if (Y.utils.compareIds(parent.end, o.id)) { - setParent = true - parent.end = o.left - } - if (setParent) { - yield* this.setOperation(parent) - } - yield* this.removeOperation(o.id) - } - os.gc2 = os.gc1 - os.gc1 = [] - if (os.gcTimeout > 0) { - os.gcInterval = setTimeout(garbageCollect, os.gcTimeout) - } - def.resolve() + resolve() + }) }) - return def.promise } this.garbageCollect = garbageCollect if (this.gcTimeout > 0) { diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js index 2ccf1285..f2ad5a9a 100644 --- a/src/OperationStores/Memory.js +++ b/src/OperationStores/Memory.js @@ -1,15 +1,6 @@ /* global Y */ 'use strict' -function copyObject (o) { - var c = {} - for (var key in o) { - c[key] = o[key] - } - return c -} -Y.utils.copyObject = copyObject - class DeleteStore extends Y.utils.RBTree { constructor () { super() @@ -120,8 +111,8 @@ class DeleteStore extends Y.utils.RBTree { Y.utils.DeleteStore = DeleteStore -Y.Memory = (function () { // eslint-disable-line no-unused-vars - class Transaction extends Y.AbstractTransaction { // eslint-disable-line +Y.Memory = (function () { + class Transaction extends Y.AbstractTransaction { constructor (store) { super(store) @@ -144,28 +135,25 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars * getOpsFromDeleteSet (ds) { return this.ds.getDeletions(ds) } - * setOperation (op) { // eslint-disable-line + * setOperation (op) { // TODO: you can remove this step! probs.. var n = this.os.findNode(op.id) n.val = op return op } - * addOperation (op) { // eslint-disable-line + * addOperation (op) { this.os.add(op) } - * getOperation (id) { // eslint-disable-line - if (id == null) { - throw new Error('You must define id!') - } + * getOperation (id) { return this.os.find(id) } - * removeOperation (id) { // eslint-disable-line + * removeOperation (id) { this.os.delete(id) } - * setState (state) { // eslint-disable-line + * setState (state) { this.ss[state.user] = state.clock } - * getState (user) { // eslint-disable-line + * getState (user) { var clock = this.ss[user] if (clock == null) { clock = 0 @@ -175,7 +163,7 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars clock: clock } } - * getStateVector () { // eslint-disable-line + * getStateVector () { var stateVector = [] for (var user in this.ss) { var clock = this.ss[user] @@ -186,7 +174,7 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars } return stateVector } - * getStateSet () { // eslint-disable-line + * getStateSet () { return this.ss } * getOperations (startSS) { @@ -225,7 +213,7 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars } * makeOperationReady (ss, op) { // instead of ss, you could use currSS (a ss that increments when you add an operation) - op = copyObject(op) + op = Y.utils.copyObject(op) var o = op var clock while (o.right != null) { diff --git a/src/OperationStores/RedBlackTree.js b/src/OperationStores/RedBlackTree.js index 2a679f0b..4af6f043 100644 --- a/src/OperationStores/RedBlackTree.js +++ b/src/OperationStores/RedBlackTree.js @@ -1,10 +1,9 @@ /* global Y */ 'use strict' -function smaller (a, b) { - return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]) -} -Y.utils.smaller = smaller +/* + This file contains a not so fancy implemantion of a Red Black Tree. +*/ class N { // A created node is always red! @@ -126,7 +125,7 @@ class N { } } -class RBTree { // eslint-disable-line no-unused-vars +class RBTree { constructor () { this.root = null this.length = 0 @@ -140,11 +139,11 @@ class RBTree { // eslint-disable-line no-unused-vars return null } else { while (true) { - if ((from === null || smaller(from, o.val.id)) && o.left !== null) { + if ((from === null || Y.utils.smaller(from, o.val.id)) && o.left !== null) { // o is included in the bound // try to find an element that is closer to the bound o = o.left - } else if (from !== null && smaller(o.val.id, from)) { + } else if (from !== null && Y.utils.smaller(o.val.id, from)) { // o is not within the bound, maybe one of the right elements is.. if (o.right !== null) { o = o.right @@ -168,11 +167,11 @@ class RBTree { // eslint-disable-line no-unused-vars return null } else { while (true) { - if ((to === null || smaller(o.val.id, to)) && o.right !== null) { + if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) { // o is included in the bound // try to find an element that is closer to the bound o = o.right - } else if (to !== null && smaller(to, o.val.id)) { + } else if (to !== null && Y.utils.smaller(to, o.val.id)) { // o is not within the bound, maybe one of the left elements is.. if (o.left !== null) { o = o.left @@ -189,7 +188,7 @@ class RBTree { // eslint-disable-line no-unused-vars } iterate (from, to, f) { var o = this.findNodeWithLowerBound(from) - while (o !== null && (to === null || smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) { + while (o !== null && (to === null || Y.utils.smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) { f(o.val) o = o.next() } @@ -226,9 +225,9 @@ class RBTree { // eslint-disable-line no-unused-vars if (o === null) { return false } - if (smaller(id, o.val.id)) { + if (Y.utils.smaller(id, o.val.id)) { o = o.left - } else if (smaller(o.val.id, id)) { + } else if (Y.utils.smaller(o.val.id, id)) { o = o.right } else { return o @@ -386,14 +385,14 @@ class RBTree { // eslint-disable-line no-unused-vars if (this.root !== null) { var p = this.root // p abbrev. parent while (true) { - if (smaller(node.val.id, p.val.id)) { + if (Y.utils.smaller(node.val.id, p.val.id)) { if (p.left === null) { p.left = node break } else { p = p.left } - } else if (smaller(p.val.id, node.val.id)) { + } else if (Y.utils.smaller(p.val.id, node.val.id)) { if (p.right === null) { p.right = node break diff --git a/src/Struct.js b/src/Struct.js index c76f83ec..f0646b9d 100644 --- a/src/Struct.js +++ b/src/Struct.js @@ -1,25 +1,31 @@ /* global Y */ 'use strict' -function compareIds (id1, id2) { - if (id1 == null || id2 == null) { - if (id1 == null && id2 == null) { - return true - } - return false - } - if (id1[0] === id2[0] && id1[1] === id2[1]) { - return true - } else { - return false - } -} -Y.utils.compareIds = compareIds +/* + An operation also defines the structure of a type. This is why operation and + structure are used interchangeably here. + + It must be of the type Object. I hope to achieve some performance + improvements when working on databases that support the json format. + + An operation must have the following properties: + + * encode + - Encode the structure in a readable format (preferably string- todo) + * decode (todo) + - decode structure to json + * execute + - Execute the semantics of an operation. + * requiredOps + - Operations that are required to execute this operation. +*/ var Struct = { - /* This Operations does _not_ have an id! - { - target: Id + /* This is the only operation that is actually not a structure, because + it is not stored in the OS. This is why it _does not_ have an id + + op = { + target: Id } */ Delete: { @@ -29,16 +35,18 @@ var Struct = { requiredOps: function (op) { return [op.target] }, - execute: function * (op) { - // console.log('Delete', op, console.trace()) - var target = yield* this.getOperation(op.target) + /* + Delete an operation from the OS, and add it to the GC, if necessary. + */ + delete: function * (targetId) { + var target = yield* this.getOperation(targetId) if (target != null && !target.deleted) { target.deleted = true - if (target.left !== null && (yield* this.getOperation(target.left)).deleted) { + if (target.left != null && (yield* this.getOperation(target.left)).deleted) { this.store.addToGarbageCollector(target.id) target.gc = true } - if (target.right !== null) { + if (target.right != null) { var right = yield* this.getOperation(target.right) if (right.deleted && right.gc == null) { this.store.addToGarbageCollector(right.id) @@ -49,15 +57,21 @@ var Struct = { yield* this.setOperation(target) var t = this.store.initializedTypes[JSON.stringify(target.parent)] if (t != null) { - yield* t._changed(this, Y.utils.copyObject(op)) + yield* t._changed(this, { + struct: 'Delete', + target: targetId + }) } } - this.ds.delete(op.target) - var state = yield* this.getState(op.target[0]) - if (state.clock === op.target[1]) { + this.ds.delete(targetId) + var state = yield* this.getState(targetId[0]) + if (state.clock === targetId[1]) { yield* this.checkDeleteStoreForState(state) yield* this.setState(state) } + }, + execute: function * (op) { + yield* Struct.Delete.delete.call(this, op.target) } }, Insert: { @@ -65,27 +79,15 @@ var Struct = { content: any, left: Id, right: Id, - origin: id, + origin: Id, parent: Id, - parentSub: string (optional), - id: this.os.getNextOpId() + parentSub: string (optional), // child of Map type + id: Id } */ encode: function (op) { - /* bad idea, right? - var e = { - id: op.id, - left: op.left, - right: op.right, - origin: op.origin, - parent: op.parent, - content: op.content, - struct: "Insert" - } - if (op.parentSub != null){ - e.parentSub = op.parentSub - } - return e;*/ + // TODO: you could not send the "left" property, then you also have to + // "op.left = null" in $execute or $decode return op }, requiredOps: function (op) { @@ -96,7 +98,7 @@ var Struct = { if (op.right != null) { ids.push(op.right) } - // if(op.right == null && op.left == null) {} + // if (op.right == null && op.left == null) { ids.push(op.parent) if (op.opContent != null) { @@ -104,13 +106,13 @@ var Struct = { } return ids }, - getDistanceToOrigin: function *(op) { + getDistanceToOrigin: function * (op) { if (op.left == null) { return 0 } else { var d = 0 var o = yield* this.getOperation(op.left) - while (!compareIds(op.origin, (o ? o.id : null))) { + while (!Y.utils.compareIds(op.origin, (o ? o.id : null))) { d++ if (o.left == null) { break @@ -156,7 +158,7 @@ var Struct = { // handle conflicts while (true) { - if (o != null && !compareIds(o.id, op.right)) { + if (o != null && !Y.utils.compareIds(o.id, op.right)) { var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o) if (oOriginDistance === i) { // case 1 @@ -250,7 +252,7 @@ var Struct = { */ return [] }, - execute: function * (op) { // eslint-disable-line + execute: function * (op) { op.start = null op.end = null }, @@ -277,7 +279,7 @@ var Struct = { map: function * (o, f) { o = o.start var res = [] - while (o !== null) { + while (o !== null) { // TODO: change to != (at least some convention) var operation = yield* this.getOperation(o) if (!operation.deleted) { res.push(f(operation)) @@ -305,16 +307,12 @@ var Struct = { } }, requiredOps: function () { - /* - var ids = [] - for (var end in op.map) { - ids.push(op.map[end]) - } - return ids - */ return [] }, execute: function * () {}, + /* + Get a property by name + */ get: function * (op, name) { var oid = op.map[name] if (oid != null) { @@ -323,6 +321,9 @@ var Struct = { ? res.content : yield* this.getType(res.opContent)) } }, + /* + Delete a property by name + */ delete: function * (op, name) { var v = op.map[name] || null if (v != null) { diff --git a/src/Types/Array.js b/src/Types/Array.js index 18dcb268..10d4399d 100644 --- a/src/Types/Array.js +++ b/src/Types/Array.js @@ -49,7 +49,7 @@ throw new Error('Unexpected struct!') } } - this.eventHandler.callUserEventListeners(userEvents) + this.eventHandler.callEventListeners(userEvents) }) } get length () { @@ -110,7 +110,7 @@ ops[j].right = mostRight } yield* this.applyCreatedOperations(ops) - eventHandler.awaitedLastInserts(ops.length) + eventHandler.awaitedInserts(ops.length) }) } delete (pos, length) { @@ -139,11 +139,11 @@ eventHandler.awaitAndPrematurelyCall(dels) this.os.requestTransaction(function *() { yield* this.applyCreatedOperations(dels) - eventHandler.awaitedLastDeletes(dels.length, newLeft) + eventHandler.awaitedDeletes(dels.length, newLeft) }) } observe (f) { - this.eventHandler.addUserEventListener(f) + this.eventHandler.addEventListener(f) } * _changed (transaction, op) { if (!op.deleted) { diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js index 9a0c3fa6..ded6d1a7 100644 --- a/src/Types/Array.spec.js +++ b/src/Types/Array.spec.js @@ -6,7 +6,6 @@ var numberOfYArrayTests = 10 describe('Array Type', function () { var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll - jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 beforeEach(async(function * (done) { yield createUsers(this, 3) y1 = (yconfig1 = this.users[0]).root @@ -59,7 +58,7 @@ describe('Array Type', function () { expect(l2.toArray()).toEqual(l3.toArray()) expect(l2.toArray()).toEqual([0, 2, 'y']) done() - })) + }), 100) it('Handles getOperations ascending ids bug in late sync', async(function * (done) { var l1, l2 l1 = yield y1.set('Array', Y.Array) diff --git a/src/Types/Map.js b/src/Types/Map.js index cbcbb1b1..e2f73092 100644 --- a/src/Types/Map.js +++ b/src/Types/Map.js @@ -21,11 +21,11 @@ if (this.opContents[key] != null) { let prevType = this.opContents[key] oldValue = () => {// eslint-disable-line - let def = Promise.defer() - this.os.requestTransaction(function *() {// eslint-disable-line - def.resolve(yield* this.getType(prevType)) + return new Promise((resolve) => { + this.os.requestTransaction(function *() {// eslint-disable-line + resolve(yield* this.getType(prevType)) + }) }) - return def.promise } } else { oldValue = this.contents[key] @@ -77,7 +77,7 @@ throw new Error('Unexpected Operation!') } } - this.eventHandler.callUserEventListeners(userEvents) + this.eventHandler.callEventListeners(userEvents) }) } get (key) { @@ -91,12 +91,12 @@ return this.contents[key] } } else { - let def = Promise.defer() - var oid = this.opContents[key] - this.os.requestTransaction(function *() { - def.resolve(yield* this.getType(oid)) + return new Promise((resolve) => { + var oid = this.opContents[key] + this.os.requestTransaction(function *() { + resolve(yield* this.getType(oid)) + }) }) - return def.promise } } delete (key) { @@ -112,7 +112,7 @@ eventHandler.awaitAndPrematurelyCall([modDel]) this.os.requestTransaction(function *() { yield* this.applyCreatedOperations([del]) - eventHandler.awaitedLastDeletes(1) + eventHandler.awaitedDeletes(1) }) } } @@ -130,35 +130,35 @@ parentSub: key, struct: 'Insert' } - var def = Promise.defer() - 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 - insert.id = this.store.getNextOpId() - yield* this.applyCreatedOperations([insert]) - def.resolve(type) - }) - } else { - insert.content = value - insert.id = this.os.getNextOpId() - var eventHandler = this.eventHandler - eventHandler.awaitAndPrematurelyCall([insert]) + return new Promise((resolve) => { + 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 + insert.id = this.store.getNextOpId() + yield* this.applyCreatedOperations([insert]) + resolve(type) + }) + } else { + insert.content = value + insert.id = this.os.getNextOpId() + var eventHandler = this.eventHandler + eventHandler.awaitAndPrematurelyCall([insert]) - this.os.requestTransaction(function *() { - yield* this.applyCreatedOperations([insert]) - eventHandler.awaitedLastInserts(1) - }) - def.resolve(value) - } - return def.promise + this.os.requestTransaction(function *() { + yield* this.applyCreatedOperations([insert]) + eventHandler.awaitedInserts(1) + }) + resolve(value) + } + }) } observe (f) { - this.eventHandler.addUserEventListener(f) + this.eventHandler.addEventListener(f) } unobserve (f) { - this.eventHandler.removeUserEventListener(f) + this.eventHandler.removeEventListener(f) } observePath (path, f) { var self = this diff --git a/src/Types/Map.spec.js b/src/Types/Map.spec.js index aa23ba65..56cb2a60 100644 --- a/src/Types/Map.spec.js +++ b/src/Types/Map.spec.js @@ -6,7 +6,6 @@ var numberOfYMapTests = 5 describe('Map Type', function () { var y1, y2, y3, y4, flushAll - jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000 beforeEach(async(function * (done) { yield createUsers(this, 5) y1 = this.users[0].root diff --git a/src/Utils.js b/src/Utils.js index 733c2942..50325a29 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -1,13 +1,44 @@ /* global Y */ 'use strict' -class EventHandler { // eslint-disable-line +/* + EventHandler is an helper class for constructing custom types. + + Why: When constructing custom types, you sometimes want your types to work + synchronous: E.g. + ``` Synchronous + mytype.setSomething("yay") + mytype.getSomething() === "yay" + ``` + ``` Asynchronous + mytype.setSomething("yay") + mytype.getSomething() === undefined + mytype.waitForSomething().then(function(){ + mytype.getSomething() === "yay" + }) + + The structures usually work asynchronously (you have to wait for the + database request to finish). EventHandler will help you to make your type + synchronously. +*/ +class EventHandler { + /* + onevent: is called when the structure changes. + + Note: "awaiting opertations" is used to denote operations that were + prematurely called. Events for received operations can not be executed until + all prematurely called operations were executed ("waiting operations") + */ constructor (onevent) { this.waiting = [] this.awaiting = 0 this.onevent = onevent - this.userEventListeners = [] + this.eventListeners = [] } + /* + Call this when a new operation arrives. It will be executed right away if + there are no waiting operations, that you prematurely executed + */ receivedOp (op) { if (this.awaiting <= 0) { this.onevent([op]) @@ -15,31 +46,43 @@ class EventHandler { // eslint-disable-line this.waiting.push(Y.utils.copyObject(op)) } } + /* + You created some operations, and you want the `onevent` function to be + called right away. Received operations will not be executed untill all + prematurely called operations are executed + */ awaitAndPrematurelyCall (ops) { this.awaiting++ this.onevent(ops) } - addUserEventListener (f) { - this.userEventListeners.push(f) + /* + Basic event listener boilerplate... + TODO: maybe put this in a different type.. + */ + addEventListener (f) { + this.eventListeners.push(f) } - removeUserEventListener (f) { - this.userEventListeners = this.userEventListeners.filter(function (g) { + removeEventListener (f) { + this.eventListeners = this.eventListeners.filter(function (g) { return f !== g }) } - removeAllUserEventListeners () { - this.userEventListeners = [] + removeAllEventListeners () { + this.eventListeners = [] } - callUserEventListeners (event) { - for (var i in this.userEventListeners) { + callEventListeners (event) { + for (var i in this.eventListeners) { try { - this.userEventListeners[i](event) + this.eventListeners[i](event) } catch (e) { - console.log('User events must not throw Errors!');// eslint-disable-line + console.log('User events must not throw Errors!') // eslint-disable-line } } } - awaitedLastInserts (n) { + /* + Call this when you successfully awaited the execution of n Insert operations + */ + awaitedInserts (n) { var ops = this.waiting.splice(this.waiting.length - n) for (var oid = 0; oid < ops.length; oid++) { var op = ops[oid] @@ -57,9 +100,12 @@ class EventHandler { // eslint-disable-line } } } - this.tryCallEvents() + this._tryCallEvents() } - awaitedLastDeletes (n, newLeft) { + /* + Call this when you successfully awaited the execution of n Delete operations + */ + awaitedDeletes (n, newLeft) { var ops = this.waiting.splice(this.waiting.length - n) for (var j in ops) { var del = ops[j] @@ -73,9 +119,12 @@ class EventHandler { // eslint-disable-line } } } - this.tryCallEvents() + this._tryCallEvents() } - tryCallEvents () { + /* (private) + Try to execute the events for the waiting operations + */ + _tryCallEvents () { this.awaiting-- if (this.awaiting <= 0 && this.waiting.length > 0) { var events = this.waiting @@ -86,6 +135,17 @@ class EventHandler { // eslint-disable-line } Y.utils.EventHandler = EventHandler +/* + A wrapper for the definition of a custom type. + Every custom type must have three properties: + + * createType + - Defines the model of a newly created custom type and returns the type + * initType + - Given a model, creates a custom type + * class + - the constructor of the custom type (e.g. in order to inherit from a type) +*/ class CustomType { // eslint-disable-line constructor (def) { if (def.createType == null || @@ -100,3 +160,39 @@ class CustomType { // eslint-disable-line } } Y.utils.CustomType = CustomType + +/* + Make a flat copy of an object + (just copy properties) +*/ +function copyObject (o) { + var c = {} + for (var key in o) { + c[key] = o[key] + } + return c +} +Y.utils.copyObject = copyObject + +/* + Defines a smaller relation on Id's +*/ +function smaller (a, b) { + return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]) +} +Y.utils.smaller = smaller + +function compareIds (id1, id2) { + if (id1 == null || id2 == null) { + if (id1 == null && id2 == null) { + return true + } + return false + } + if (id1[0] === id2[0] && id1[1] === id2[1]) { + return true + } else { + return false + } +} +Y.utils.compareIds = compareIds diff --git a/src/y.js b/src/y.js index 78bc13a2..f5e98f36 100644 --- a/src/y.js +++ b/src/y.js @@ -2,20 +2,19 @@ 'use strict' function Y (opts) { - var def = Promise.defer() - new YConfig(opts, function (yconfig) { // eslint-disable-line - yconfig.db.whenUserIdSet(function () { - def.resolve(yconfig) + return new Promise(function (resolve) { + var yconfig = new YConfig(opts, function () { + yconfig.db.whenUserIdSet(function () { + resolve(yconfig) + }) }) }) - return def.promise } -class YConfig { // eslint-disable-line no-unused-vars +class YConfig { constructor (opts, callback) { this.db = new Y[opts.db.name](this, opts.db) this.connector = new Y[opts.connector.name](this, opts.connector) - var yconfig = this this.db.requestTransaction(function * requestTransaction () { // create initial Map type var model = { @@ -27,7 +26,7 @@ class YConfig { // eslint-disable-line no-unused-vars yield* this.addOperation(model) var root = yield* this.createType(model) this.store.y.root = root - callback(yconfig) + callback() }) } isConnected () { @@ -55,7 +54,7 @@ class YConfig { // eslint-disable-line no-unused-vars } } -if (g) { // eslint-disable-line +if (typeof YConcurrency_TestingMode !== 'undefined') { g.Y = Y //eslint-disable-line // debugger //eslint-disable-line }