diff --git a/dist b/dist index 3f60690880908e7f9a92642e37253f1ac84db3f6..e2f93af86e9dd207cb57d313c6ac305cd69e34d1 160000 --- a/dist +++ b/dist @@ -1 +1 @@ -Subproject commit 3f60690880908e7f9a92642e37253f1ac84db3f6 +Subproject commit e2f93af86e9dd207cb57d313c6ac305cd69e34d1 diff --git a/src/Database.js b/src/Database.js index 8246e3fc90fe3cb76c4386c48854d85bb11cc1ae..7810c1784c0bfe8320f16d5c0d559df07057abab 100644 --- a/src/Database.js +++ b/src/Database.js @@ -413,7 +413,7 @@ module.exports = function (Y /* :any */) { // notify parent, if it was instanciated as a custom type if (t != null) { - let o = Y.utils.copyObject(op) + let o = Y.utils.copyOperation(op) yield* t._changed(transaction, o) } if (!op.deleted) { diff --git a/src/SpecHelper.js b/src/SpecHelper.js index 5683962d2e4517b8e322fd5226f8b0b898e13fb4..47ce153f0f8f12e4d445dc3672037a6dd72cb78c 100644 --- a/src/SpecHelper.js +++ b/src/SpecHelper.js @@ -86,10 +86,6 @@ function getRandomString () { g.getRandomString = getRandomString function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions, noReconnect) { - function randomTransaction (root) { - var f = getRandom(transactions) - f(root) - } for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) { var r = Math.random() if (r >= 0.5) { @@ -97,44 +93,76 @@ function * applyTransactions (relAmount, numberOfTransactions, objects, users, t yield Y.utils.globalRoom.flushOne() // flushes for some user.. (not necessarily 0) } else if (noReconnect || r >= 0.05) { // 45% chance to create operation - randomTransaction(getRandom(objects)) + var done = getRandom(transactions)(getRandom(objects)) + if (done != null) { + yield done + } else { + yield wait() + } yield Y.utils.globalRoom.whenTransactionsFinished() } else { // 5% chance to disconnect/reconnect var u = getRandom(users) + yield Promise.all(objects.map(fixAwaitingInType)) if (u.connector.isDisconnected()) { yield u.reconnect() } else { yield u.disconnect() } + yield Promise.all(objects.map(fixAwaitingInType)) } } } +function fixAwaitingInType (type) { + return new Promise(function (resolve) { + type.os.whenTransactionsFinished().then(function () { + // _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once + type.os.requestTransaction(function * () { + if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) { + type.eventHandler._debuggingAwaiting = false + yield* type.eventHandler.awaitedOps(this, 0) + } + wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve) + }) + }) + }) +} +g.fixAwaitingInType = fixAwaitingInType + g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) { yield* applyTransactions(1, numberOfTransactions, objects, users, transactions, true) yield Y.utils.globalRoom.flushAll() + yield Promise.all(objects.map(fixAwaitingInType)) }) g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) { yield* applyTransactions(1, numberOfTransactions, objects, users, transactions) + yield Promise.all(objects.map(fixAwaitingInType)) yield Y.utils.globalRoom.flushAll() + yield Promise.all(objects.map(fixAwaitingInType)) for (var u in users) { + yield Promise.all(objects.map(fixAwaitingInType)) yield users[u].reconnect() + yield Promise.all(objects.map(fixAwaitingInType)) } + yield Promise.all(objects.map(fixAwaitingInType)) yield Y.utils.globalRoom.flushAll() + yield Promise.all(objects.map(fixAwaitingInType)) yield g.garbageCollectAllUsers(users) }) g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) { yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions) yield Y.utils.globalRoom.flushAll() + yield Promise.all(objects.map(fixAwaitingInType)) for (var u in users) { // TODO: here, we enforce that two users never sync at the same time with u[0] // enforce that in the connector itself! yield users[u].reconnect() } yield Y.utils.globalRoom.flushAll() + yield Promise.all(objects.map(fixAwaitingInType)) yield g.garbageCollectAllUsers(users) }) @@ -222,7 +250,6 @@ g.compareAllUsers = async(function * compareAllUsers (users) { }) }) } else { - // TODO: make requestTransaction return a promise.. u.db.requestTransaction(function * () { yield* t2.call(this) var db2 = [] diff --git a/src/Transaction.js b/src/Transaction.js index 7f0275bf09e811815a00780a00f9018a9618c66c..d32112b3f6a6c4d82f42d8e673291c74444e013a 100644 --- a/src/Transaction.js +++ b/src/Transaction.js @@ -1068,7 +1068,7 @@ module.exports = function (Y/* :any */) { /* this is what we used before.. use this as a reference.. * makeOperationReady (startSS, op) { op = Y.Struct[op.struct].encode(op) - op = Y.utils.copyObject(op) + op = Y.utils.copyObject(op) -- use copyoperation instead now! var o = op var ids = [op.id] // search for the new op.right diff --git a/src/Utils.js b/src/Utils.js index 759ccd15858e7d25dc55fd8315daa73cf97cb80b..6214fcb59fe1eaa9de786e5e269c385d44765103 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -102,9 +102,41 @@ module.exports = function (Y /* : any*/) { prematurely called operations are executed */ awaitAndPrematurelyCall (ops) { - this.awaiting += ops.length - ops.forEach(this.onevent) + this.awaiting++ + ops.map(Y.utils.copyOperation).forEach(this.onevent) } + * awaitedOps (transaction, n) { + // remove awaited ops + this.waiting.splice(this.waiting.length - n) + // update all waiting ops + for (let i = 0; i < this.waiting.length; i++) { + var o = this.waiting[i] + if (o.struct === 'Insert') { + var _o = yield* transaction.getInsertion(o.id) + if (!Y.utils.compareIds(_o.id, o.id)) { + // o got extended + o.left = [o.id[0], o.id[1] - 1] + } else if (_o.left == null) { + o.left = null + } else { + // find next undeleted op + var left = yield* transaction.getInsertion(_o.left) + while (left.deleted != null) { + if (left.left != null) { + left = yield* transaction.getInsertion(left.left) + } else { + left = null + break + } + } + o.left = left != null ? Y.utils.getLastId(left) : null + } + } + } + this._tryCallEvents() + } + // TODO: Remove awaitedInserts and awaitedDeletes in favor of awaitedOps, as they are deprecated and do not always work + // Do this in one of the coming releases that are breaking anyway /* Call this when you successfully awaited the execution of n Insert operations */ @@ -162,12 +194,42 @@ module.exports = function (Y /* : any*/) { /* (private) Try to execute the events for the waiting operations */ - _tryCallEvents (n) { - this.awaiting -= n + _tryCallEvents () { + function notSoSmartSort (array) { + var result = [] + while (array.length > 0) { + for (var i = 0; i < array.length; i++) { + var independent = true + for (var j = 0; j < array.length; j++) { + if (Y.utils.matchesId(array[j], array[i].left)) { + // array[i] depends on array[j] + independent = false + break + } + } + if (independent) { + result.push(array.splice(i, 1)[0]) + i-- + } + } + } + return result + } + if (this.awaiting > 0) this.awaiting-- if (this.awaiting === 0 && this.waiting.length > 0) { - var ops = this.waiting + var ins = [] + var dels = [] + this.waiting.forEach(function (o) { + if (o.struct === 'Delete') { + dels.push(o) + } else { + ins.push(o) + } + }) + ins = notSoSmartSort(ins) + ins.forEach(this.onevent) + dels.forEach(this.onevent) this.waiting = [] - ops.forEach(this.onevent) } } } @@ -236,6 +298,20 @@ module.exports = function (Y /* : any*/) { } Y.utils.copyObject = copyObject + /* + Copy an operation, so that it can be manipulated. + Note: You must not change subproperties (except o.content)! + */ + function copyOperation (o) { + o = copyObject(o) + if (o.content != null) { + o.content = o.content.map(function (c) { return c }) + } + return o + } + + Y.utils.copyOperation = copyOperation + /* Defines a smaller relation on Id's */ @@ -244,6 +320,11 @@ module.exports = function (Y /* : any*/) { } Y.utils.smaller = smaller + function inDeletionRange (del, ins) { + return del.target[0] === ins[0] && del.target[1] <= ins[1] && ins[1] < del.target[1] + (del.length || 1) + } + Y.utils.inDeletionRange = inDeletionRange + function compareIds (id1, id2) { if (id1 == null || id2 == null) { return id1 === id2