diff --git a/src/Database.js b/src/Database.js index d63476d9..b0bce0b1 100644 --- a/src/Database.js +++ b/src/Database.js @@ -275,5 +275,28 @@ class AbstractDatabase { yield* t._changed(transaction, Y.utils.copyObject(op)) } } + getNextRequest () { + if (this.waitingTransactions.length === 0) { + this.transactionInProgress = false + return null + } else { + return this.waitingTransactions.shift() + } + } + requestTransaction (makeGen, callImmediately) { + if (!this.transactionInProgress) { + this.transactionInProgress = true + if (callImmediately) { + this.transact(makeGen) + } else { + var self = this + setTimeout(function () { + self.transact(makeGen) + }, 0) + } + } else { + this.waitingTransactions.push(makeGen) + } + } } Y.AbstractDatabase = AbstractDatabase diff --git a/src/Database.spec.js b/src/Database.spec.js index dd559e31..5abf9ede 100644 --- a/src/Database.spec.js +++ b/src/Database.spec.js @@ -156,48 +156,48 @@ for (var database of databases) { }) it('debug #1', function (done) { store.requestTransaction(function * () { - yield this.os.put({id: [2]}) - yield this.os.put({id: [0]}) - yield this.os.delete([2]) - yield this.os.put({id: [1]}) - expect(yield this.os.find([0])).not.toBeNull() - expect(yield this.os.find([1])).not.toBeNull() - expect(yield this.os.find([2])).toBeNull() + yield* this.os.put({id: [2]}) + yield* this.os.put({id: [0]}) + yield* this.os.delete([2]) + yield* this.os.put({id: [1]}) + expect(yield* this.os.find([0])).not.toBeNull() + expect(yield* this.os.find([1])).not.toBeNull() + expect(yield* this.os.find([2])).toBeNull() done() }) }) it('can add&retrieve 5 elements', function (done) { store.requestTransaction(function * () { - yield this.os.put({val: 'four', id: [4]}) - yield this.os.put({val: 'one', id: [1]}) - yield this.os.put({val: 'three', id: [3]}) - yield this.os.put({val: 'two', id: [2]}) - yield this.os.put({val: 'five', id: [5]}) - expect((yield this.os.find([1])).val).toEqual('one') - expect((yield this.os.find([2])).val).toEqual('two') - expect((yield this.os.find([3])).val).toEqual('three') - expect((yield this.os.find([4])).val).toEqual('four') - expect((yield this.os.find([5])).val).toEqual('five') + yield* this.os.put({val: 'four', id: [4]}) + yield* this.os.put({val: 'one', id: [1]}) + yield* this.os.put({val: 'three', id: [3]}) + yield* this.os.put({val: 'two', id: [2]}) + yield* this.os.put({val: 'five', id: [5]}) + expect((yield* this.os.find([1])).val).toEqual('one') + expect((yield* this.os.find([2])).val).toEqual('two') + expect((yield* this.os.find([3])).val).toEqual('three') + expect((yield* this.os.find([4])).val).toEqual('four') + expect((yield* this.os.find([5])).val).toEqual('five') done() }) }) it('5 elements do not exist anymore after deleting them', function (done) { store.requestTransaction(function * () { - yield this.os.put({val: 'four', id: [4]}) - yield this.os.put({val: 'one', id: [1]}) - yield this.os.put({val: 'three', id: [3]}) - yield this.os.put({val: 'two', id: [2]}) - yield this.os.put({val: 'five', id: [5]}) - yield this.os.delete([4]) - expect(yield this.os.find([4])).not.toBeTruthy() - yield this.os.delete([3]) - expect(yield this.os.find([3])).not.toBeTruthy() - yield this.os.delete([2]) - expect(yield this.os.find([2])).not.toBeTruthy() - yield this.os.delete([1]) - expect(yield this.os.find([1])).not.toBeTruthy() - yield this.os.delete([5]) - expect(yield this.os.find([5])).not.toBeTruthy() + yield* this.os.put({val: 'four', id: [4]}) + yield* this.os.put({val: 'one', id: [1]}) + yield* this.os.put({val: 'three', id: [3]}) + yield* this.os.put({val: 'two', id: [2]}) + yield* this.os.put({val: 'five', id: [5]}) + yield* this.os.delete([4]) + expect(yield* this.os.find([4])).not.toBeTruthy() + yield* this.os.delete([3]) + expect(yield* this.os.find([3])).not.toBeTruthy() + yield* this.os.delete([2]) + expect(yield* this.os.find([2])).not.toBeTruthy() + yield* this.os.delete([1]) + expect(yield* this.os.find([1])).not.toBeTruthy() + yield* this.os.delete([5]) + expect(yield* this.os.find([5])).not.toBeTruthy() done() }) }) @@ -214,9 +214,9 @@ for (var database of databases) { var r = Math.random() if (r < 0.8) { var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)] - if (!(yield this.os.findNode(obj))) { + if (!(this.os.findNode(obj))) { elements.push(obj) - yield this.os.put({id: obj}) + yield* this.os.put({id: obj}) } } else if (elements.length > 0) { var elemid = Math.floor(Math.random() * elements.length) @@ -224,7 +224,7 @@ for (var database of databases) { elements = elements.filter(function (e) { return !Y.utils.compareIds(e, elem) }) - yield this.os.delete(elem) + yield* this.os.delete(elem) } } done() @@ -233,7 +233,7 @@ for (var database of databases) { it('can find every object', function (done) { store.requestTransaction(function * () { for (var id of elements) { - expect((yield this.os.find(id)).id).toEqual(id) + expect((yield* this.os.find(id)).id).toEqual(id) } done() }) @@ -242,7 +242,8 @@ for (var database of databases) { it('can find every object with lower bound search', function (done) { store.requestTransaction(function * () { for (var id of elements) { - expect((yield this.os.findNodeWithLowerBound(id)).val.id).toEqual(id) + var e = yield* this.os.findNodeWithLowerBound(id) + expect(e.val.id).toEqual(id) } done() }) diff --git a/src/Databases/IndexedDB.js b/src/Databases/IndexedDB.js index 8981a767..c3570ac3 100644 --- a/src/Databases/IndexedDB.js +++ b/src/Databases/IndexedDB.js @@ -3,43 +3,51 @@ 'use strict' Y.IndexedDB = (function () { + class Store { + constructor (transaction, name) { + this.store = transaction.objectStore(name) + } + find (id) { + return this.store.get(id) + } + put (v) { + return this.store.put(v) + } + delete (id) { + return this.store.delete(id) + } + * findNodeWithLowerBound (start) { + var cursorResult = this.store.openCursor(window.IDBKeyRange.lowerBound(start)) + var cursor + while ((cursor = yield cursorResult) != null) { + // yield* gen.call(t, cursor.value) + cursor.continue() + } + } + * iterate (t, start, end, gen) { + var range = null + if (start != null && end != null) { + range = window.IDBKeyRange.bound(start, end) + } else if (start != null) { + range = window.IDBKeyRange.lowerBound(start) + } else if (end != null) { + range = window.IDBKeyRange.upperBound(end) + } + var cursorResult = this.store.openCursor(range) + var cursor + while ((cursor = yield cursorResult) != null) { + yield* gen.call(t, cursor.value) + cursor.continue() + } + } + + } class Transaction { constructor (store) { - this.transaction = store.db.transaction(['OperationStore', 'StateVector'], 'readwrite') - this.sv = this.transaction.objectStore('StateVector') - this.os = this.transaction.objectStore('OperationStore') - this.buffer = {} - } - * setOperation (op) { - yield this.os.put(op) - this.buffer[JSON.stringify(op.id)] = op - return op - } - * getOperation (id) { - var op = this.buffer[JSON.stringify(id)] - if (op == null) { - op = yield this.os.get(id) - this.buffer[JSON.stringify(id)] = op - } - return op - } - * removeOperation (id) { - this.buffer[JSON.stringify(id)] = null - return yield this.os.delete(id) - } - * setState (state) { - return yield this.sv.put(state) - } - * getState (user) { - var state - if ((state = yield this.sv.get(user)) != null) { - return state - } else { - return { - user: user, - clock: 0 - } - } + var transaction = store.db.transaction(['OperationStore', 'StateStore', 'DeleteStore'], 'readwrite') + this.ss = new Store(transaction, 'StateStore') + this.os = new Store(transaction, 'OperationStore') + this.ds = new Store(transaction, 'DeleteStore') } * getStateVector () { var stateVector = [] @@ -155,7 +163,8 @@ Y.IndexedDB = (function () { var db = event.target.result try { db.createObjectStore('OperationStore', {keyPath: 'id'}) - db.createObjectStore('StateVector', {keyPath: 'user'}) + db.createObjectStore('DeleteStore', {keyPath: 'id'}) + db.createObjectStore('StateStore', {keyPath: 'id'}) } catch (e) { // console.log("Store already exists!") } @@ -172,7 +181,19 @@ Y.IndexedDB = (function () { this.transactionQueue.onRequest() } } - * removeDatabase () { + transact (makeGen) { + var t = new Y.Transaction(this) + while (makeGen !== null) { + var gen = makeGen.call(t) + var res = gen.next() + while (!res.done) { + res = gen.next(res.value) + } + makeGen = this.getNextRequest() + } + } + // TODO: implement "free".. + * destroy () { this.db.close() yield window.indexedDB.deleteDatabase(this.namespace) } diff --git a/src/Databases/Memory.js b/src/Databases/Memory.js index b15ba204..7b2de91e 100644 --- a/src/Databases/Memory.js +++ b/src/Databases/Memory.js @@ -2,6 +2,15 @@ 'use strict' Y.Memory = (function () { + class Transaction extends Y.Transaction { + constructor (store) { + super(store) + this.store = store + this.ss = store.ss + this.os = store.os + this.ds = store.ds + } + } class Database extends Y.AbstractDatabase { constructor (y, opts) { super(y, opts) @@ -25,29 +34,15 @@ Y.Memory = (function () { }, true) }) } - requestTransaction (_makeGen, callImmediately) { - if (!this.transactionInProgress) { - this.transactionInProgress = true - var transact = () => { - var makeGen = _makeGen - while (makeGen != null) { - var t = new Y.Transaction(this) - var gen = makeGen.call(t) - var res = gen.next() - while (!res.done) { - res = gen.next(res.value) - } - makeGen = this.waitingTransactions.shift() - } - this.transactionInProgress = false + transact (makeGen) { + var t = new Transaction(this) + while (makeGen !== null) { + var gen = makeGen.call(t) + var res = gen.next() + while (!res.done) { + res = gen.next(res.value) } - if (callImmediately) { - transact() - } else { - setTimeout(transact, 0) - } - } else { - this.waitingTransactions.push(_makeGen) + makeGen = this.getNextRequest() } } * destroy () { diff --git a/src/Databases/RedBlackTree.js b/src/Databases/RedBlackTree.js index 9586eb77..3aebe4b8 100644 --- a/src/Databases/RedBlackTree.js +++ b/src/Databases/RedBlackTree.js @@ -130,7 +130,13 @@ class RBTree { this.root = null this.length = 0 } - findNodeWithLowerBound (from) { + * findNext (id) { + return yield* this.findNodeWithLowerBound([id[0], id[1] + 1]) + } + * findPrev (id) { + return yield* this.findNodeWithUpperBound([id[0], id[1] - 1]) + } + * findNodeWithLowerBound (from) { if (from === void 0) { throw new Error('You must define from!') } @@ -158,7 +164,7 @@ class RBTree { } } } - findNodeWithUpperBound (to) { + * findNodeWithUpperBound (to) { if (to === void 0) { throw new Error('You must define from!') } @@ -187,7 +193,7 @@ class RBTree { } } * iterate (t, from, to, f) { - var o = this.findNodeWithLowerBound(from) + var o = yield* this.findNodeWithLowerBound(from) while (o !== null && (to === null || Y.utils.smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) { yield* f.call(t, o.val) o = o.next() @@ -220,7 +226,7 @@ class RBTree { console.table(os) } } - find (id) { + * find (id) { var n return (n = this.findNode(id)) ? n.val : null } @@ -246,7 +252,7 @@ class RBTree { } } } - delete (id) { + * delete (id) { if (id == null || id.constructor !== Array) { throw new Error('id is expected to be an Array!') } @@ -388,7 +394,7 @@ class RBTree { } } } - put (v) { + * put (v) { if (v == null || v.id == null || v.id.constructor !== Array) { throw new Error('v is expected to have an id property which is an Array!') } diff --git a/src/Databases/RedBlackTree.spec.js b/src/Databases/RedBlackTree.spec.js index 3e47d1fd..9039f9cb 100644 --- a/src/Databases/RedBlackTree.spec.js +++ b/src/Databases/RedBlackTree.spec.js @@ -3,7 +3,7 @@ var numberOfRBTreeTests = 1000 -function itRedNodesDoNotHaveBlackChildren (tree) { +function itRedNodesDoNotHaveBlackChildren () { it('Red nodes do not have black children', function () { function traverse (n) { if (n == null) { @@ -20,11 +20,11 @@ function itRedNodesDoNotHaveBlackChildren (tree) { traverse(n.left) traverse(n.right) } - traverse(tree.root) + traverse(this.tree.root) }) } -function itBlackHeightOfSubTreesAreEqual (tree) { +function itBlackHeightOfSubTreesAreEqual () { it('Black-height of sub-trees are equal', function () { function traverse (n) { if (n == null) { @@ -39,76 +39,99 @@ function itBlackHeightOfSubTreesAreEqual (tree) { return sub1 + 1 } } - traverse(tree.root) + traverse(this.tree.root) }) } -function itRootNodeIsBlack (tree) { +function itRootNodeIsBlack () { it('root node is black', function () { - expect(tree.root == null || tree.root.isBlack()).toBeTruthy() + expect(this.tree.root == null || this.tree.root.isBlack()).toBeTruthy() }) } describe('RedBlack Tree', function () { - beforeEach(function () { - this.memory = new Y.Memory(null, { - name: 'Memory', - gcTimeout: -1 - }) - this.tree = this.memory.os - }) + var tree, memory describe('debug #2', function () { - var tree = new Y.utils.RBTree() - tree.put({id: [8433]}) - tree.put({id: [12844]}) - tree.put({id: [1795]}) - tree.put({id: [30302]}) - tree.put({id: [64287]}) - tree.delete([8433]) - tree.put({id: [28996]}) - tree.delete([64287]) - tree.put({id: [22721]}) + beforeAll(function (done) { + this.memory = new Y.Memory(null, { + name: 'Memory', + gcTimeout: -1 + }) + this.tree = this.memory.os + tree = this.tree + memory = this.memory + memory.requestTransaction(function * () { + yield* tree.put({id: [8433]}) + yield* tree.put({id: [12844]}) + yield* tree.put({id: [1795]}) + yield* tree.put({id: [30302]}) + yield* tree.put({id: [64287]}) + yield* tree.delete([8433]) + yield* tree.put({id: [28996]}) + yield* tree.delete([64287]) + yield* tree.put({id: [22721]}) + done() + }) + }) - itRootNodeIsBlack(tree, []) - itBlackHeightOfSubTreesAreEqual(tree, []) + itRootNodeIsBlack() + itBlackHeightOfSubTreesAreEqual([]) }) describe(`After adding&deleting (0.8/0.2) ${numberOfRBTreeTests} times`, function () { var elements = [] - var tree = new Y.utils.RBTree() - for (var i = 0; i < numberOfRBTreeTests; i++) { - var r = Math.random() - if (r < 0.8) { - var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)] - if (!tree.findNode(obj)) { - elements.push(obj) - tree.put({id: obj}) + beforeAll(function (done) { + this.memory = new Y.Memory(null, { + name: 'Memory', + gcTimeout: -1 + }) + this.tree = this.memory.os + tree = this.tree + memory = this.memory + memory.requestTransaction(function * () { + for (var i = 0; i < numberOfRBTreeTests; i++) { + var r = Math.random() + if (r < 0.8) { + var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)] + if (!tree.findNode(obj)) { + elements.push(obj) + yield* tree.put({id: obj}) + } + } else if (elements.length > 0) { + var elemid = Math.floor(Math.random() * elements.length) + var elem = elements[elemid] + elements = elements.filter(function (e) { + return !Y.utils.compareIds(e, elem) + }) + yield* tree.delete(elem) + } } - } else if (elements.length > 0) { - var elemid = Math.floor(Math.random() * elements.length) - var elem = elements[elemid] - elements = elements.filter(function (e) { - return !Y.utils.compareIds(e, elem) - }) - tree.delete(elem) - } - } - itRootNodeIsBlack(tree) - - it('can find every object', function () { - for (var id of elements) { - expect(tree.find(id).id).toEqual(id) - } + done() + }) }) - it('can find every object with lower bound search', function () { - for (var id of elements) { - expect(tree.findNodeWithLowerBound(id).val.id).toEqual(id) - } - }) - itRedNodesDoNotHaveBlackChildren(tree) + itRootNodeIsBlack() - itBlackHeightOfSubTreesAreEqual(tree) + it('can find every object', function (done) { + memory.requestTransaction(function * () { + for (var id of elements) { + expect((yield* tree.find(id)).id).toEqual(id) + } + done() + }) + }) + + it('can find every object with lower bound search', function (done) { + this.memory.requestTransaction(function * () { + for (var id of elements) { + expect((yield* tree.findNodeWithLowerBound(id)).val.id).toEqual(id) + } + done() + }) + }) + itRedNodesDoNotHaveBlackChildren() + + itBlackHeightOfSubTreesAreEqual() it('iterating over a tree with lower bound yields the right amount of results', function (done) { var lowerBound = elements[Math.floor(Math.random() * elements.length)] diff --git a/src/Helper.spec.js b/src/Helper.spec.js index 60643ecb..b350975c 100644 --- a/src/Helper.spec.js +++ b/src/Helper.spec.js @@ -18,7 +18,7 @@ g.g = g g.YConcurrency_TestingMode = true -jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000 +jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 g.describeManyTimes = function describeManyTimes (times, name, f) { for (var i = 0; i < times; i++) { @@ -36,7 +36,7 @@ function wait (t) { return new Promise(function (resolve) { setTimeout(function () { resolve() - }, t) + }, t * 2) }) } g.wait = wait diff --git a/src/Transaction.js b/src/Transaction.js index 7b725ce9..3e37f449 100644 --- a/src/Transaction.js +++ b/src/Transaction.js @@ -75,12 +75,6 @@ applyable on a given SS. */ class Transaction { - constructor (store) { - this.store = store - this.ss = store.ss - this.os = store.os - this.ds = store.ds - } /* Get a type based on the id of its model. If it does not exist yes, create it. @@ -206,14 +200,14 @@ class Transaction { // un-extend left var newlen = n.val.len - (id[1] - n.val.id[1]) n.val.len -= newlen - n = yield this.ds.put({id: id, len: newlen, gc: false}) + n = yield* this.ds.put({id: id, len: newlen, gc: false}) } // get prev&next before adding a new operation var prev = n.prev() var next = n.next() if (id[1] < n.val.id[1] + n.val.len - 1) { // un-extend right - yield this.ds.put({id: [id[0], id[1] + 1], len: n.val.len - 1, gc: false}) + yield* this.ds.put({id: [id[0], id[1] + 1], len: n.val.len - 1, gc: false}) n.val.len = 1 } // set gc'd @@ -225,7 +219,7 @@ class Transaction { Y.utils.compareIds([prev.val.id[0], prev.val.id[1] + prev.val.len], n.val.id) ) { prev.val.len += n.val.len - yield this.ds.delete(n.val.id) + yield* this.ds.delete(n.val.id) n = prev } // can extend right? @@ -235,7 +229,7 @@ class Transaction { Y.utils.compareIds([n.val.id[0], n.val.id[1] + n.val.len], next.val.id) ) { n.val.len += next.val.len - yield this.ds.delete(next.val.id) + yield* this.ds.delete(next.val.id) } } } @@ -246,7 +240,7 @@ class Transaction { */ * markDeleted (id) { // this.mem.push(["del", id]); - var n = yield this.ds.findNodeWithUpperBound(id) + var n = yield* this.ds.findNodeWithUpperBound(id) if (n != null && n.val.id[0] === id[0]) { if (n.val.id[1] <= id[1] && id[1] < n.val.id[1] + n.val.len) { // already deleted @@ -256,11 +250,11 @@ class Transaction { n.val.len++ } else { // cannot extend left - n = yield this.ds.put({id: id, len: 1, gc: false}) + n = yield* this.ds.put({id: id, len: 1, gc: false}) } } else { // cannot extend left - n = yield this.ds.put({id: id, len: 1, gc: false}) + n = yield* this.ds.put({id: id, len: 1, gc: false}) } // can extend right? var next = n.next() @@ -270,8 +264,8 @@ class Transaction { !next.val.gc ) { n.val.len = n.val.len + next.val.len - yield this.ds.delete(next.val.id) - return yield this.ds.findNode(n.val.id) + yield* this.ds.delete(next.val.id) + return this.ds.findNode(n.val.id) } else { return n } @@ -389,7 +383,7 @@ class Transaction { } } * checkDeleteStoreForState (state) { - var n = yield this.ds.findNodeWithUpperBound([state.user, state.clock]) + var n = yield* this.ds.findNodeWithUpperBound([state.user, state.clock]) if (n !== null && n.val.id[0] === state.user && n.val.gc) { state.clock = Math.max(state.clock, n.val.id[1] + n.val.len) } @@ -468,7 +462,7 @@ class Transaction { } } * isGarbageCollected (id) { - var n = yield this.ds.findNodeWithUpperBound(id) + var n = yield* this.ds.findNodeWithUpperBound(id) return n !== null && n.val.id[0] === id[0] && id[1] < n.val.id[1] + n.val.len && n.val.gc } /* @@ -491,15 +485,15 @@ class Transaction { return ds } * isDeleted (id) { - var n = yield this.ds.findNodeWithUpperBound(id) + var n = yield* this.ds.findNodeWithUpperBound(id) return n !== null && n.val.id[0] === id[0] && id[1] < n.val.id[1] + n.val.len } * setOperation (op) { - yield this.os.put(op) + yield* this.os.put(op) return op } * addOperation (op) { - var n = yield this.os.put(op) + var n = yield* this.os.put(op) return function () { if (n != null) { n = n.next() @@ -510,10 +504,10 @@ class Transaction { } } * getOperation (id) { - return yield this.os.find(id) + return yield* this.os.find(id) } * removeOperation (id) { - yield this.os.delete(id) + yield* this.os.delete(id) } * setState (state) { var val = { @@ -521,15 +515,15 @@ class Transaction { clock: state.clock } // TODO: find a way to skip this step.. (after implementing some dbs..) - if (yield this.ss.find([state.user])) { - yield this.ss.put(val) + if (yield* this.ss.find([state.user])) { + yield* this.ss.put(val) } else { - yield this.ss.put(val) + yield* this.ss.put(val) } } * getState (user) { var n - var clock = (n = this.ss.find([user])) == null ? null : n.clock + var clock = (n = yield* this.ss.find([user])) == null ? null : n.clock if (clock == null) { clock = 0 } diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js index 4de9d258..7f909565 100644 --- a/src/Types/Array.spec.js +++ b/src/Types/Array.spec.js @@ -1,7 +1,7 @@ /* global createUsers, wait, Y, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, garbageCollectAllUsers, describeManyTimes */ /* eslint-env browser,jasmine */ -var numberOfYArrayTests = 10 +var numberOfYArrayTests = 200 var repeatArrayTests = 1 describe('Array Type', function () {