diff --git a/gulpfile.js b/gulpfile.js index 10477644..3f8e5c6d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -133,7 +133,7 @@ gulp.task('build_jasmine_browser', function () { .pipe(gulp.dest('build')) }) -gulp.task('develop', ['build_jasmine_browser', 'build'], function () { +gulp.task('develop', ['build_jasmine_browser'], function () { gulp.watch(files.test, ['build_jasmine_browser']) // gulp.watch(files.test, ["test"]) gulp.watch(files.test, ['build']) diff --git a/src/Helper.spec.js b/src/Helper.spec.js index 67dafc82..21bc5918 100644 --- a/src/Helper.spec.js +++ b/src/Helper.spec.js @@ -99,7 +99,9 @@ async function compareAllUsers(users){//eslint-disable-line var d = ds[j] for (var i = 0; i < d.len; i++) { var o = yield* this.getOperation([d.id[0], d.id[1] + i]) - expect(o.deleted).toBeTruthy() + if (o != null) { + expect(o.deleted).toBeTruthy() + } } } }) @@ -139,7 +141,8 @@ async function createUsers(self, numberOfUsers) {//eslint-disable-line for (var i = 0; i < numberOfUsers; i++) { promises.push(Y({ db: { - name: 'Memory' + name: 'Memory', + gcTimeout: -1 }, connector: { name: 'Test', diff --git a/src/OperationStore.js b/src/OperationStore.js index 3b66e21b..d00cf3a6 100644 --- a/src/OperationStore.js +++ b/src/OperationStore.js @@ -39,7 +39,7 @@ class AbstractTransaction { // eslint-disable-line no-unused-vars } class AbstractOperationStore { // eslint-disable-line no-unused-vars - constructor (y) { + constructor (y, opts) { this.y = y // E.g. this.listenersById[id] : Array this.listenersById = {} @@ -62,6 +62,44 @@ class AbstractOperationStore { // eslint-disable-line no-unused-vars this.initializedTypes = {} this.whenUserIdSetListener = null this.waitingOperations = new RBTree() + + this.gc1 = [] // first stage + this.gc2 = [] // second stage -> after that, kill it + this.gcTimeout = opts.gcTimeout || 5000 + var os = this + function garbageCollect () { + 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 + } + if (o.right != null) { + var right = yield* this.getOperation(o.right) + right.left = o.left + } + yield* this.removeOperation(o.id) + } + os.gc2 = os.gc1 + os.gc1 = [] + if (os.gcTimeout > 0) { + os.gcInterval = setTimeout(garbageCollect, os.gcTimeout) + } + }) + } + this.garbageCollect = garbageCollect + if (this.gcTimeout > 0) { + garbageCollect() + } + } + addToGarbageCollector (op) { + this.gc1.push(op) + } + destroy () { + clearInterval(this.gcInterval) + this.gcInterval = null } setUserId (userId) { this.userId = userId @@ -201,7 +239,7 @@ class AbstractOperationStore { // eslint-disable-line no-unused-vars } // notify parent, if it has been initialized as a custom type var t = this.initializedTypes[JSON.stringify(op.parent)] - if (t != null) { + if (t != null && !op.deleted) { yield* t._changed(transaction, copyObject(op)) } } diff --git a/src/OperationStores/IndexedDB.js b/src/OperationStores/IndexedDB.js index b8e24cbc..d742fe08 100644 --- a/src/OperationStores/IndexedDB.js +++ b/src/OperationStores/IndexedDB.js @@ -81,7 +81,7 @@ Y.IndexedDB = (function () { // eslint-disable-line } class OperationStore extends AbstractOperationStore { // eslint-disable-line no-undef constructor (y, opts) { - super(y) + super(y, opts) if (opts == null) { opts = {} } diff --git a/src/OperationStores/IndexedDB.spec.js b/src/OperationStores/IndexedDB.spec.js index 159d4ede..b065f773 100644 --- a/src/OperationStores/IndexedDB.spec.js +++ b/src/OperationStores/IndexedDB.spec.js @@ -6,7 +6,7 @@ if (typeof window !== 'undefined') { describe('IndexedDB', function () { var ob beforeAll(function () { - ob = new Y.IndexedDB(null, {namespace: 'Test'}) + ob = new Y.IndexedDB(null, {namespace: 'Test', gcTimeout: -1}) }) it('can add and get operation', function (done) { diff --git a/src/OperationStores/Memory.js b/src/OperationStores/Memory.js index 385923c0..1127aa87 100644 --- a/src/OperationStores/Memory.js +++ b/src/OperationStores/Memory.js @@ -238,8 +238,8 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars } } class OperationStore extends AbstractOperationStore { // eslint-disable-line no-undef - constructor (y) { - super(y) + constructor (y, opts) { + super(y, opts) this.os = new RBTree() this.ss = {} this.waitingTransactions = [] @@ -249,10 +249,10 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars logTable () { this.os.logTable() } - requestTransaction (_makeGen) { + requestTransaction (_makeGen, requestNow = false) { if (!this.transactionInProgress) { this.transactionInProgress = true - setTimeout(() => { + var transact = () => { var makeGen = _makeGen while (makeGen != null) { var t = new Transaction(this) @@ -268,12 +268,18 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars makeGen = this.waitingTransactions.shift() } this.transactionInProgress = false - }, 0) + } + if (!requestNow) { + setTimeout(transact, 0) + } else { + transact() + } } else { this.waitingTransactions.push(_makeGen) } } - * removeDatabase () { // eslint-disable-line + * destroy () { // eslint-disable-line + super.destroy() delete this.os } } diff --git a/src/Struct.js b/src/Struct.js index a21a256f..ad5f22bc 100644 --- a/src/Struct.js +++ b/src/Struct.js @@ -31,6 +31,18 @@ var Struct = { var target = yield* this.getOperation(op.target) if (!target.deleted) { target.deleted = true + if (target.left !== null && (yield* this.getOperation(target.left)).deleted) { + this.store.addToGarbageCollector(target.id) + target.gc = true + } + if (target.right !== null) { + var right = yield* this.getOperation(target.right) + if (right.deleted && right.gc == null) { + this.store.addToGarbageCollector(right.id) + right.gc = true + yield* this.setOperation(right) + } + } yield* this.setOperation(target) this.ds.delete(target.id) var t = this.store.initializedTypes[JSON.stringify(target.parent)] diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js index 9b0fbb49..7444d248 100644 --- a/src/Types/Array.spec.js +++ b/src/Types/Array.spec.js @@ -179,6 +179,27 @@ describe('Array Type', function () { await wait(50) done() }) + it('garbage collects', async function (done) { + var l1, l2, l3 + l1 = await y1.set('Array', Y.Array) + l1.insert(0, ['x', 'y', 'z']) + await flushAll() + yconfig1.disconnect() + l1.delete(0, 3) + l2 = await y2.get('Array') + await wait() + yconfig1.reconnect() + await wait() + l3 = await y3.get('Array') + await flushAll() + yconfig1.db.garbageCollect() + yconfig1.db.garbageCollect() + yconfig1.db.logTable() + expect(l1.toArray()).toEqual(l2.toArray()) + expect(l2.toArray()).toEqual(l3.toArray()) + expect(l2.toArray()).toEqual([]) + done() + }) }) describe(`Random tests`, function () { var randomArrayTransactions = [ diff --git a/src/Types/Map.js b/src/Types/Map.js index e7f0debf..57e4872b 100644 --- a/src/Types/Map.js +++ b/src/Types/Map.js @@ -34,10 +34,18 @@ if (op.left === null) { if (op.opContent != null) { delete this.contents[key] - this.opContents[key] = op.opContent + if (op.deleted) { + delete this.opContents[key] + } else { + this.opContents[key] = op.opContent + } } else { delete this.opContents[key] - this.contents[key] = op.content + if (op.deleted) { + delete this.contents[key] + } else { + this.contents[key] = op.content + } } this.map[key] = op.id var insertEvent = { @@ -54,11 +62,8 @@ } } else if (op.struct === 'Delete') { if (compareIds(this.map[key], op.target)) { - if (this.opContents[key] != null) { - delete this.opContents[key] - } else { - delete this.contents[key] - } + delete this.opContents[key] + delete this.contents[key] var deleteEvent = { name: key, object: this, diff --git a/src/y.js b/src/y.js index dc86d354..9ff86034 100644 --- a/src/y.js +++ b/src/y.js @@ -40,7 +40,7 @@ class YConfig { // eslint-disable-line no-unused-vars } destroy () { this.connector.disconnect() - this.db.removeDatabase() + this.db.destroy() this.connector = null this.db = null this.transact = function () {