diff --git a/examples/yjs-dist.esm b/examples/yjs-dist.esm index 2399227a..61d3d836 100644 --- a/examples/yjs-dist.esm +++ b/examples/yjs-dist.esm @@ -1,7 +1,7 @@ import Y from '../src/y.js' import yArray from '../../y-array/src/y-array.js' -import yMap from '../../y-map/src/Map.js' +import yMap from '../../y-map/src/y-map.js' import yText from '../../y-text/src/Text.js' import yXml from '../../y-xml/src/y-xml.js' import yMemory from '../../y-memory/src/y-memory.js' diff --git a/rollup.browser.js b/rollup.browser.js index 6c3f1777..f43af28e 100644 --- a/rollup.browser.js +++ b/rollup.browser.js @@ -1,4 +1,3 @@ -import inject from 'rollup-plugin-inject' import babel from 'rollup-plugin-babel' import uglify from 'rollup-plugin-uglify' import nodeResolve from 'rollup-plugin-node-resolve' @@ -16,12 +15,7 @@ export default { browser: true }), commonjs(), - babel({ - runtimeHelpers: true - }), - inject({ - regeneratorRuntime: 'regenerator-runtime' - }), + babel(), uglify({ output: { comments: function (node, comment) { diff --git a/rollup.test.js b/rollup.test.js index c74129e6..e0dbe22d 100644 --- a/rollup.test.js +++ b/rollup.test.js @@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs' import multiEntry from 'rollup-plugin-multi-entry' export default { - entry: 'test/y-xml.tests.js', + entry: 'test/*.js', moduleName: 'y-tests', format: 'umd', plugins: [ diff --git a/src/Connector.js b/src/Connector.js index 46fc263f..38b08231 100644 --- a/src/Connector.js +++ b/src/Connector.js @@ -105,6 +105,7 @@ export default function extendConnector (Y/* :any */) { } } } + userJoined (user, role, auth) { if (role == null) { throw new Error('You must specify the role of the joined user!') @@ -133,6 +134,7 @@ export default function extendConnector (Y/* :any */) { } this._syncWithUser(user) } + // Execute a function _when_ we are connected. // If not connected, wait until connected whenSynced (f) { @@ -142,18 +144,20 @@ export default function extendConnector (Y/* :any */) { this.whenSyncedListeners.push(f) } } + _syncWithUser (userid) { if (this.role === 'slave') { return // "The current sync has not finished or this is controlled by a master!" } sendSyncStep1(this, userid) } + _fireIsSyncedListeners () { this.y.db.whenTransactionsFinished().then(() => { if (!this.isSynced) { this.isSynced = true // It is safer to remove this! - // TODO: remove: yield * this.garbageCollectAfterSync() + // TODO: remove: this.garbageCollectAfterSync() // call whensynced listeners for (var f of this.whenSyncedListeners) { f() @@ -162,6 +166,7 @@ export default function extendConnector (Y/* :any */) { } }) } + send (uid, buffer) { if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) { throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages') @@ -169,6 +174,7 @@ export default function extendConnector (Y/* :any */) { this.log('%s: Send \'%y\' to %s', this.userId, buffer, uid) this.logMessage('Message: %Y', buffer) } + broadcast (buffer) { if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) { throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages') @@ -176,6 +182,7 @@ export default function extendConnector (Y/* :any */) { this.log('%s: Broadcast \'%y\'', this.userId, buffer) this.logMessage('Message: %Y', buffer) } + /* Buffer operations, and broadcast them when ready. */ @@ -207,6 +214,7 @@ export default function extendConnector (Y/* :any */) { this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops) } } + /* You received a raw message, and you know that it is intended for Yjs. Then call this function. */ @@ -224,14 +232,11 @@ export default function extendConnector (Y/* :any */) { encoder.writeVarString(roomname) let messageType = decoder.readVarString() let senderConn = this.connections.get(sender) - this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender) this.logMessage('Message: %Y', buffer) - if (senderConn == null && !skipAuth) { throw new Error('Received message from unknown peer!') } - if (messageType === 'sync step 1' || messageType === 'sync step 2') { let auth = decoder.readVarUint() if (senderConn.auth == null) { @@ -284,97 +289,6 @@ export default function extendConnector (Y/* :any */) { this._fireIsSyncedListeners() } } - - /* - 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/* :any */) { - function parseArray (node) { - for (var n of node.children) { - if (n.getAttribute('isArray') === 'true') { - return parseArray(n) - } else { - return parseObject(n) - } - } - } - function parseObject (node/* :any */) { - var json = {} - for (var attrName in node.attrs) { - var value = node.attrs[attrName] - var int = parseInt(value, 10) - if (isNaN(int) || ('' + int) !== value) { - json[attrName] = value - } else { - json[attrName] = int - } - } - for (var n/* :any */ in node.children) { - var name = n.name - if (n.getAttribute('isArray') === 'true') { - json[name] = parseArray(n) - } else { - json[name] = parseObject(n) - } - } - return json - } - 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 - - - - m - ltx element - json - Object - */ - encodeMessageToXml (msg, obj) { - // attributes is optional - function encodeObject (m, json) { - for (var name in json) { - var value = json[name] - if (name == null) { - // nop - } else if (value.constructor === Object) { - encodeObject(m.c(name), value) - } else if (value.constructor === Array) { - encodeArray(m.c(name), value) - } else { - m.setAttribute(name, value) - } - } - } - function encodeArray (m, array) { - m.setAttribute('isArray', 'true') - for (var e of array) { - if (e.constructor === Object) { - encodeObject(m.c('array-element'), e) - } else { - encodeArray(m.c('array-element'), e) - } - } - } - if (obj.constructor === Object) { - encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj) - } else if (obj.constructor === Array) { - encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj) - } else { - throw new Error("I can't encode this json!") - } - } } Y.AbstractConnector = AbstractConnector } diff --git a/src/Database.js b/src/Database.js index cb2f5601..8cac24f6 100644 --- a/src/Database.js +++ b/src/Database.js @@ -85,11 +85,11 @@ export default function extendDatabase (Y /* :any */) { console.warn('gc should be empty when not synced!') } return new Promise((resolve) => { - os.requestTransaction(function * () { + os.requestTransaction(function () { if (os.y.connector != null && os.y.connector.isSynced) { for (var i = 0; i < os.gc2.length; i++) { var oid = os.gc2[i] - yield * this.garbageCollectOperation(oid) + this.garbageCollectOperation(oid) } os.gc2 = os.gc1 os.gc1 = [] @@ -197,15 +197,15 @@ export default function extendDatabase (Y /* :any */) { this.gc = false this.gcTimeout = -1 return new Promise(function (resolve) { - self.requestTransaction(function * () { + self.requestTransaction(function () { var ungc /* :Array */ = self.gc1.concat(self.gc2) self.gc1 = [] self.gc2 = [] for (var i = 0; i < ungc.length; i++) { - var op = yield * this.getOperation(ungc[i]) + var op = this.getOperation(ungc[i]) if (op != null) { delete op.gc - yield * this.setOperation(op) + this.setOperation(op) } } resolve() @@ -224,7 +224,7 @@ export default function extendDatabase (Y /* :any */) { returns true iff op was added to GC */ - * addToGarbageCollector (op, left) { + addToGarbageCollector (op, left) { if ( op.gc == null && op.deleted === true && @@ -235,12 +235,12 @@ export default function extendDatabase (Y /* :any */) { if (left != null && left.deleted === true) { gc = true } else if (op.content != null && op.content.length > 1) { - op = yield * this.getInsertionCleanStart([op.id[0], op.id[1] + 1]) + op = this.getInsertionCleanStart([op.id[0], op.id[1] + 1]) gc = true } if (gc) { op.gc = true - yield * this.setOperation(op) + this.setOperation(op) this.store.queueGarbageCollector(op.id) return true } @@ -265,7 +265,7 @@ export default function extendDatabase (Y /* :any */) { } } } - * destroy () { + destroy () { clearTimeout(this.gcInterval) this.gcInterval = null this.stopRepairCheck() @@ -274,9 +274,9 @@ export default function extendDatabase (Y /* :any */) { if (!this.userIdPromise.inProgress) { this.userIdPromise.inProgress = true var self = this - self.requestTransaction(function * () { + self.requestTransaction(function () { self.userId = userId - var state = yield * this.getState(userId) + var state = this.getState(userId) self.opClock = state.clock self.userIdPromise.resolve(userId) }) @@ -355,7 +355,7 @@ export default function extendDatabase (Y /* :any */) { this.listenersByIdRequestPending = true var store = this - this.requestTransaction(function * () { + this.requestTransaction(function () { var exeNow = store.listenersByIdExecuteNow store.listenersByIdExecuteNow = [] @@ -366,7 +366,7 @@ export default function extendDatabase (Y /* :any */) { for (let key = 0; key < exeNow.length; key++) { let o = exeNow[key].op - yield * store.tryExecute.call(this, o) + store.tryExecute.call(this, o) } for (var sid in ls) { @@ -374,9 +374,9 @@ export default function extendDatabase (Y /* :any */) { var id = JSON.parse(sid) var op if (typeof id[1] === 'string') { - op = yield * this.getOperation(id) + op = this.getOperation(id) } else { - op = yield * this.getInsertion(id) + op = this.getInsertion(id) } if (op == null) { store.listenersById[sid] = l @@ -385,7 +385,7 @@ export default function extendDatabase (Y /* :any */) { let listener = l[i] let o = listener.op if (--listener.missing === 0) { - yield * store.tryExecute.call(this, o) + store.tryExecute.call(this, o) } } } @@ -402,15 +402,15 @@ export default function extendDatabase (Y /* :any */) { addOperation: any; whenOperationsExist: any; */ - * tryExecute (op) { - this.store.addToDebug('yield * this.store.tryExecute.call(this, ', JSON.stringify(op), ')') + tryExecute (op) { + this.store.addToDebug('this.store.tryExecute.call(this, ', JSON.stringify(op), ')') if (op.struct === 'Delete') { - yield * Y.Struct.Delete.execute.call(this, op) + Y.Struct.Delete.execute.call(this, op) // this is now called in Transaction.deleteOperation! - // yield * this.store.operationAdded(this, op) + // this.store.operationAdded(this, op) } else { // check if this op was defined - var defined = yield * this.getInsertion(op.id) + var defined = this.getInsertion(op.id) while (defined != null && defined.content != null) { // check if this op has a longer content in the case it is defined if (defined.id[1] + defined.content.length < op.id[1] + op.content.length) { @@ -419,23 +419,23 @@ export default function extendDatabase (Y /* :any */) { op.id = [op.id[0], op.id[1] + overlapSize] op.left = Y.utils.getLastId(defined) op.origin = op.left - defined = yield * this.getOperation(op.id) // getOperation suffices here + defined = this.getOperation(op.id) // getOperation suffices here } else { break } } if (defined == null) { var opid = op.id - var isGarbageCollected = yield * this.isGarbageCollected(opid) + var isGarbageCollected = this.isGarbageCollected(opid) if (!isGarbageCollected) { // TODO: reduce number of get / put calls for op .. - yield * Y.Struct[op.struct].execute.call(this, op) - yield * this.addOperation(op) - yield * this.store.operationAdded(this, op) + Y.Struct[op.struct].execute.call(this, op) + this.addOperation(op) + this.store.operationAdded(this, op) // operationAdded can change op.. - op = yield * this.getOperation(opid) + op = this.getOperation(opid) // if insertion, try to combine with left - yield * this.tryCombineWithLeft(op) + this.tryCombineWithLeft(op) } } } @@ -452,15 +452,15 @@ export default function extendDatabase (Y /* :any */) { * Always: * * Call type */ - * operationAdded (transaction, op) { + operationAdded (transaction, op) { if (op.struct === 'Delete') { var type = this.initializedTypes[JSON.stringify(op.targetParent)] if (type != null) { - yield * type._changed(transaction, op) + type._changed(transaction, op) } } else { // increase SS - yield * transaction.updateState(op.id[0]) + transaction.updateState(op.id[0]) var opLen = op.content != null ? op.content.length : 1 for (let i = 0; i < opLen; i++) { // notify whenOperation listeners (by id) @@ -480,9 +480,9 @@ export default function extendDatabase (Y /* :any */) { // if parent is deleted, mark as gc'd and return if (op.parent != null) { - var parentIsDeleted = yield * transaction.isDeleted(op.parent) + var parentIsDeleted = transaction.isDeleted(op.parent) if (parentIsDeleted) { - yield * transaction.deleteList(op.id) + transaction.deleteList(op.id) return } } @@ -490,7 +490,7 @@ export default function extendDatabase (Y /* :any */) { // notify parent, if it was instanciated as a custom type if (t != null) { let o = Y.utils.copyOperation(op) - yield * t._changed(transaction, o) + t._changed(transaction, o) } if (!op.deleted) { // Delete if DS says this is actually deleted @@ -499,13 +499,13 @@ export default function extendDatabase (Y /* :any */) { // TODO: !! console.log('TODO: change this before commiting') for (let i = 0; i < len; i++) { var id = [startId[0], startId[1] + i] - var opIsDeleted = yield * transaction.isDeleted(id) + var opIsDeleted = transaction.isDeleted(id) if (opIsDeleted) { var delop = { struct: 'Delete', target: id } - yield * this.tryExecute.call(transaction, delop) + this.tryExecute.call(transaction, delop) } } } @@ -528,6 +528,7 @@ export default function extendDatabase (Y /* :any */) { return Promise.resolve() } } + // Check if there is another transaction request. // * the last transaction is always a flush :) getNextRequest () { @@ -542,8 +543,8 @@ export default function extendDatabase (Y /* :any */) { return null } else { this.transactionIsFlushed = true - return function * () { - yield * this.flush() + return function () { + this.flush() } } } else { @@ -570,13 +571,13 @@ export default function extendDatabase (Y /* :any */) { Init type. This is called when a remote operation is retrieved, and transformed to a type TODO: delete type from store.initializedTypes[id] when corresponding id was deleted! */ - * initType (id, args) { + initType (id, args) { var sid = JSON.stringify(id) var t = this.store.initializedTypes[sid] if (t == null) { - var op/* :MapStruct | ListStruct */ = yield * this.getOperation(id) + var op/* :MapStruct | ListStruct */ = this.getOperation(id) if (op != null) { - t = yield * Y[op.type].typeDefinition.initType.call(this, this.store, op, args) + t = Y[op.type].typeDefinition.initType.call(this, this.store, op, args) this.store.initializedTypes[sid] = t } } @@ -591,11 +592,11 @@ export default function extendDatabase (Y /* :any */) { var op = Y.Struct[structname].create(id, typedefinition[1]) op.type = typedefinition[0].name - this.requestTransaction(function * () { + this.requestTransaction(function () { if (op.id[0] === 0xFFFFFF) { - yield * this.setOperation(op) + this.setOperation(op) } else { - yield * this.applyCreatedOperations([op]) + this.applyCreatedOperations([op]) } }) var t = Y[op.type].typeDefinition.createType(this, op, typedefinition[1]) diff --git a/src/Database.spec.js b/src/Database.spec.js deleted file mode 100644 index 353b2880..00000000 --- a/src/Database.spec.js +++ /dev/null @@ -1,354 +0,0 @@ -/* global async, databases, describe, beforeEach, afterEach */ -/* eslint-env browser,jasmine,console */ -'use strict' - -var Y = require('./SpecHelper.js') - -for (let database of databases) { - describe(`Database (${database})`, function () { - var store - describe('DeleteStore', function () { - describe('Basic', function () { - beforeEach(function () { - store = new Y[database](null, { - gcTimeout: -1, - namespace: 'testing' - }) - }) - afterEach(function (done) { - store.requestTransaction(function * () { - yield * this.store.destroy() - done() - }) - }) - it('Deleted operation is deleted', async(function * (done) { - store.requestTransaction(function * () { - yield * this.markDeleted(['u1', 10], 1) - expect(yield * this.isDeleted(['u1', 10])).toBeTruthy() - expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]}) - done() - }) - })) - it('Deleted operation extends other deleted operation', async(function * (done) { - store.requestTransaction(function * () { - yield * this.markDeleted(['u1', 10], 1) - yield * this.markDeleted(['u1', 11], 1) - expect(yield * this.isDeleted(['u1', 10])).toBeTruthy() - expect(yield * this.isDeleted(['u1', 11])).toBeTruthy() - expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]}) - done() - }) - })) - it('Deleted operation extends other deleted operation', async(function * (done) { - store.requestTransaction(function * () { - yield * this.markDeleted(['0', 3], 1) - yield * this.markDeleted(['0', 4], 1) - yield * this.markDeleted(['0', 2], 1) - expect(yield * this.getDeleteSet()).toEqual({'0': [[2, 3, false]]}) - done() - }) - })) - it('Debug #1', async(function * (done) { - store.requestTransaction(function * () { - yield * this.markDeleted(['166', 0], 1) - yield * this.markDeleted(['166', 2], 1) - yield * this.markDeleted(['166', 0], 1) - yield * this.markDeleted(['166', 2], 1) - yield * this.markGarbageCollected(['166', 2], 1) - yield * this.markDeleted(['166', 1], 1) - yield * this.markDeleted(['166', 3], 1) - yield * this.markGarbageCollected(['166', 3], 1) - yield * this.markDeleted(['166', 0], 1) - expect(yield * this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]}) - done() - }) - })) - it('Debug #2', async(function * (done) { - store.requestTransaction(function * () { - yield * this.markDeleted(['293', 0], 1) - yield * this.markDeleted(['291', 2], 1) - yield * this.markDeleted(['291', 2], 1) - yield * this.markGarbageCollected(['293', 0], 1) - yield * this.markDeleted(['293', 1], 1) - yield * this.markGarbageCollected(['291', 2], 1) - expect(yield * this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]}) - done() - }) - })) - it('Debug #3', async(function * (done) { - store.requestTransaction(function * () { - yield * this.markDeleted(['581', 0], 1) - yield * this.markDeleted(['581', 1], 1) - yield * this.markDeleted(['580', 0], 1) - yield * this.markDeleted(['580', 0], 1) - yield * this.markGarbageCollected(['581', 0], 1) - yield * this.markDeleted(['581', 2], 1) - yield * this.markDeleted(['580', 1], 1) - yield * this.markDeleted(['580', 2], 1) - yield * this.markDeleted(['580', 1], 1) - yield * this.markDeleted(['580', 2], 1) - yield * this.markGarbageCollected(['581', 2], 1) - yield * this.markGarbageCollected(['581', 1], 1) - yield * this.markGarbageCollected(['580', 1], 1) - expect(yield * this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]}) - done() - }) - })) - it('Debug #4', async(function * (done) { - store.requestTransaction(function * () { - yield * this.markDeleted(['544', 0], 1) - yield * this.markDeleted(['543', 2], 1) - yield * this.markDeleted(['544', 0], 1) - yield * this.markDeleted(['543', 2], 1) - yield * this.markGarbageCollected(['544', 0], 1) - yield * this.markDeleted(['545', 1], 1) - yield * this.markDeleted(['543', 4], 1) - yield * this.markDeleted(['543', 3], 1) - yield * this.markDeleted(['544', 1], 1) - yield * this.markDeleted(['544', 2], 1) - yield * this.markDeleted(['544', 1], 1) - yield * this.markDeleted(['544', 2], 1) - yield * this.markGarbageCollected(['543', 2], 1) - yield * this.markGarbageCollected(['543', 4], 1) - yield * this.markGarbageCollected(['544', 2], 1) - yield * this.markGarbageCollected(['543', 3], 1) - expect(yield * this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]}) - done() - }) - })) - it('Debug #5', async(function * (done) { - store.requestTransaction(function * () { - yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]}) - expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]}) - yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]}) - expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]}) - done() - }) - })) - it('Debug #6', async(function * (done) { - store.requestTransaction(function * () { - yield * this.applyDeleteSet({'40': [[0, 3, false]]}) - expect(yield * this.getDeleteSet()).toEqual({'40': [[0, 3, false]]}) - yield * this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]}) - expect(yield * this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]}) - done() - }) - })) - it('Debug #7', async(function * (done) { - store.requestTransaction(function * () { - yield * this.markDeleted(['9', 2], 1) - yield * this.markDeleted(['11', 2], 1) - yield * this.markDeleted(['11', 4], 1) - yield * this.markDeleted(['11', 1], 1) - yield * this.markDeleted(['9', 4], 1) - yield * this.markDeleted(['10', 0], 1) - yield * this.markGarbageCollected(['11', 2], 1) - yield * this.markDeleted(['11', 2], 1) - yield * this.markGarbageCollected(['11', 3], 1) - yield * this.markDeleted(['11', 3], 1) - yield * this.markDeleted(['11', 3], 1) - yield * this.markDeleted(['9', 4], 1) - yield * this.markDeleted(['10', 0], 1) - yield * this.markGarbageCollected(['11', 1], 1) - yield * this.markDeleted(['11', 1], 1) - expect(yield * this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]}) - done() - }) - })) - }) - }) - describe('OperationStore', function () { - describe('Basic Tests', function () { - beforeEach(function () { - store = new Y[database](null, { - gcTimeout: -1, - namespace: 'testing' - }) - }) - afterEach(function (done) { - store.requestTransaction(function * () { - yield * this.store.destroy() - done() - }) - }) - 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])).toBeTruthy() - expect(yield * this.os.find([1])).toBeTruthy() - expect(yield * this.os.find([2])).toBeFalsy() - 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') - 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() - done() - }) - }) - }) - var numberOfOSTests = 1000 - describe(`Random Tests - after adding&deleting (0.8/0.2) ${numberOfOSTests} times`, function () { - var elements = [] - beforeAll(function (done) { - store = new Y[database](null, { - gcTimeout: -1, - namespace: 'testing' - }) - store.requestTransaction(function * () { - for (var i = 0; i < numberOfOSTests; i++) { - var r = Math.random() - if (r < 0.8) { - var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)] - if (!(yield * this.os.find(obj))) { - elements.push(obj) - yield * this.os.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 * this.os.delete(elem) - } - } - done() - }) - }) - afterAll(function (done) { - store.requestTransaction(function * () { - yield * this.store.destroy() - done() - }) - }) - it('can find every object', function (done) { - store.requestTransaction(function * () { - for (var id of elements) { - expect((yield * this.os.find(id)).id).toEqual(id) - } - done() - }) - }) - - it('can find every object with lower bound search', function (done) { - store.requestTransaction(function * () { - for (var id of elements) { - var e = yield * this.os.findWithLowerBound(id) - expect(e.id).toEqual(id) - } - done() - }) - }) - - 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)] - var expectedResults = elements.filter(function (e, pos) { - return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && elements.indexOf(e) === pos - }).length - - var actualResults = 0 - store.requestTransaction(function * () { - yield * this.os.iterate(this, lowerBound, null, function * (val) { - expect(val).toBeDefined() - actualResults++ - }) - expect(expectedResults).toEqual(actualResults) - done() - }) - }) - - it('iterating over a tree without bounds yield the right amount of results', function (done) { - var lowerBound = null - var expectedResults = elements.filter(function (e, pos) { - return elements.indexOf(e) === pos - }).length - var actualResults = 0 - store.requestTransaction(function * () { - yield * this.os.iterate(this, lowerBound, null, function * (val) { - expect(val).toBeDefined() - actualResults++ - }) - expect(expectedResults).toEqual(actualResults) - done() - }) - }) - - it('iterating over a tree with upper bound yields the right amount of results', function (done) { - var upperBound = elements[Math.floor(Math.random() * elements.length)] - var expectedResults = elements.filter(function (e, pos) { - return (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos - }).length - - var actualResults = 0 - store.requestTransaction(function * () { - yield * this.os.iterate(this, null, upperBound, function * (val) { - expect(val).toBeDefined() - actualResults++ - }) - expect(expectedResults).toEqual(actualResults) - done() - }) - }) - - it('iterating over a tree with upper and lower bounds yield the right amount of results', function (done) { - var b1 = elements[Math.floor(Math.random() * elements.length)] - var b2 = elements[Math.floor(Math.random() * elements.length)] - var upperBound, lowerBound - if (Y.utils.smaller(b1, b2)) { - lowerBound = b1 - upperBound = b2 - } else { - lowerBound = b2 - upperBound = b1 - } - var expectedResults = elements.filter(function (e, pos) { - return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && - (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos - }).length - var actualResults = 0 - store.requestTransaction(function * () { - yield * this.os.iterate(this, lowerBound, upperBound, function * (val) { - expect(val).toBeDefined() - actualResults++ - }) - expect(expectedResults).toEqual(actualResults) - done() - }) - }) - }) - }) - }) -} diff --git a/src/MessageHandler.js b/src/MessageHandler.js index c62280eb..6a01c2e4 100644 --- a/src/MessageHandler.js +++ b/src/MessageHandler.js @@ -58,7 +58,7 @@ export function computeMessageUpdate (decoder, encoder, conn) { } export function sendSyncStep1 (conn, syncUser) { - conn.y.db.requestTransaction(function * () { + conn.y.db.requestTransaction(function () { let encoder = new BinaryEncoder() encoder.writeVarString(conn.opts.room || '') encoder.writeVarString('sync step 1') @@ -66,7 +66,7 @@ export function sendSyncStep1 (conn, syncUser) { encoder.writeVarUint(conn.protocolVersion) let preferUntransformed = conn.preferUntransformed && this.os.length === 0 // TODO: length may not be defined encoder.writeUint8(preferUntransformed ? 1 : 0) - yield * this.writeStateSet(encoder) + this.writeStateSet(encoder) conn.send(syncUser, encoder.createBuffer()) }) } @@ -99,19 +99,19 @@ export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sen return conn.y.db.whenTransactionsFinished().then(() => { // send sync step 2 - conn.y.db.requestTransaction(function * () { + conn.y.db.requestTransaction(function () { encoder.writeVarString('sync step 2') encoder.writeVarString(conn.authInfo || '') if (preferUntransformed) { encoder.writeUint8(1) - yield * this.writeOperationsUntransformed(encoder) + this.writeOperationsUntransformed(encoder) } else { encoder.writeUint8(0) - yield * this.writeOperations(encoder, decoder) + this.writeOperations(encoder, decoder) } - yield * this.writeDeleteSet(encoder) + this.writeDeleteSet(encoder) conn.send(senderConn.uid, encoder.createBuffer()) senderConn.receivedSyncStep2 = true }) @@ -174,17 +174,17 @@ export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sen let defer = senderConn.syncStep2 // apply operations first - db.requestTransaction(function * () { + db.requestTransaction(function () { let osUntransformed = decoder.readUint8() if (osUntransformed === 1) { - yield * this.applyOperationsUntransformed(decoder) + this.applyOperationsUntransformed(decoder) } else { this.store.applyOperations(decoder) } }) // then apply ds - db.requestTransaction(function * () { - yield * this.applyDeleteSet(decoder) + db.requestTransaction(function () { + this.applyDeleteSet(decoder) }) return db.whenTransactionsFinished().then(() => { conn._setSyncedWith(sender) diff --git a/src/Persistence.js b/src/Persistence.js index c5dbd681..7c961ed2 100644 --- a/src/Persistence.js +++ b/src/Persistence.js @@ -8,9 +8,11 @@ export default function extendPersistence (Y) { this.saveOperationsBuffer = [] this.log = Y.debug('y:persistence') } + saveToMessageQueue (binary) { this.log('Room %s: Save message to message queue', this.y.options.connector.room) } + saveOperations (ops) { ops = ops.map(function (op) { return Y.Struct[op.struct].encode(op) @@ -39,5 +41,6 @@ export default function extendPersistence (Y) { } } } + Y.AbstractPersistence = AbstractPersistence } diff --git a/src/RedBlackTree.js b/src/RedBlackTree.js new file mode 100644 index 00000000..40483e99 --- /dev/null +++ b/src/RedBlackTree.js @@ -0,0 +1,506 @@ + +export default function extendRBTree (Y) { + class N { + // A created node is always red! + constructor (val) { + this.val = val + this.color = true + this._left = null + this._right = null + this._parent = null + if (val.id === null) { + throw new Error('You must define id!') + } + } + isRed () { return this.color } + isBlack () { return !this.color } + redden () { this.color = true; return this } + blacken () { this.color = false; return this } + get grandparent () { + return this.parent.parent + } + get parent () { + return this._parent + } + get sibling () { + return (this === this.parent.left) + ? this.parent.right : this.parent.left + } + get left () { + return this._left + } + get right () { + return this._right + } + set left (n) { + if (n !== null) { + n._parent = this + } + this._left = n + } + set right (n) { + if (n !== null) { + n._parent = this + } + this._right = n + } + rotateLeft (tree) { + var parent = this.parent + var newParent = this.right + var newRight = this.right.left + newParent.left = this + this.right = newRight + if (parent === null) { + tree.root = newParent + newParent._parent = null + } else if (parent.left === this) { + parent.left = newParent + } else if (parent.right === this) { + parent.right = newParent + } else { + throw new Error('The elements are wrongly connected!') + } + } + next () { + if (this.right !== null) { + // search the most left node in the right tree + var o = this.right + while (o.left !== null) { + o = o.left + } + return o + } else { + var p = this + while (p.parent !== null && p !== p.parent.left) { + p = p.parent + } + return p.parent + } + } + prev () { + if (this.left !== null) { + // search the most right node in the left tree + var o = this.left + while (o.right !== null) { + o = o.right + } + return o + } else { + var p = this + while (p.parent !== null && p !== p.parent.right) { + p = p.parent + } + return p.parent + } + } + rotateRight (tree) { + var parent = this.parent + var newParent = this.left + var newLeft = this.left.right + newParent.right = this + this.left = newLeft + if (parent === null) { + tree.root = newParent + newParent._parent = null + } else if (parent.left === this) { + parent.left = newParent + } else if (parent.right === this) { + parent.right = newParent + } else { + throw new Error('The elements are wrongly connected!') + } + } + getUncle () { + // we can assume that grandparent exists when this is called! + if (this.parent === this.parent.parent.left) { + return this.parent.parent.right + } else { + return this.parent.parent.left + } + } + } + + class RBTree { + constructor () { + this.root = null + this.length = 0 + } + findNext (id) { + return this.findWithLowerBound([id[0], id[1] + 1]) + } + findPrev (id) { + return this.findWithUpperBound([id[0], id[1] - 1]) + } + findNodeWithLowerBound (from) { + if (from === void 0) { + throw new Error('You must define from!') + } + var o = this.root + if (o === null) { + return null + } else { + while (true) { + 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 && 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 + } else { + // there is no right element. Search for the next bigger element, + // this should be within the bounds + return o.next() + } + } else { + return o + } + } + } + } + findNodeWithUpperBound (to) { + if (to === void 0) { + throw new Error('You must define from!') + } + var o = this.root + if (o === null) { + return null + } else { + while (true) { + 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 && 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 + } else { + // there is no left element. Search for the prev smaller element, + // this should be within the bounds + return o.prev() + } + } else { + return o + } + } + } + } + findSmallestNode () { + var o = this.root + while (o != null && o.left != null) { + o = o.left + } + return o + } + findWithLowerBound (from) { + var n = this.findNodeWithLowerBound(from) + return n == null ? null : n.val + } + findWithUpperBound (to) { + var n = this.findNodeWithUpperBound(to) + return n == null ? null : n.val + } + iterate (t, from, to, f) { + var o + if (from === null) { + o = this.findSmallestNode() + } else { + o = this.findNodeWithLowerBound(from) + } + while ( + o !== null && + ( + to === null || // eslint-disable-line no-unmodified-loop-condition + Y.utils.smaller(o.val.id, to) || + Y.utils.compareIds(o.val.id, to) + ) + ) { + f.call(t, o.val) + o = o.next() + } + return true + } + logTable (from, to, filter) { + if (filter == null) { + filter = function () { + return true + } + } + if (from == null) { from = null } + if (to == null) { to = null } + var os = [] + this.iterate(this, from, to, function (o) { + if (filter(o)) { + var o_ = {} + for (var key in o) { + if (typeof o[key] === 'object') { + o_[key] = JSON.stringify(o[key]) + } else { + o_[key] = o[key] + } + } + os.push(o_) + } + }) + if (console.table != null) { + console.table(os) + } + } + find (id) { + var n + return (n = this.findNode(id)) ? n.val : null + } + findNode (id) { + if (id == null || id.constructor !== Array) { + throw new Error('Expect id to be an array!') + } + var o = this.root + if (o === null) { + return false + } else { + while (true) { + if (o === null) { + return false + } + if (Y.utils.smaller(id, o.val.id)) { + o = o.left + } else if (Y.utils.smaller(o.val.id, id)) { + o = o.right + } else { + return o + } + } + } + } + delete (id) { + if (id == null || id.constructor !== Array) { + throw new Error('id is expected to be an Array!') + } + var d = this.findNode(id) + if (d == null) { + // throw new Error('Element does not exist!') + return + } + this.length-- + if (d.left !== null && d.right !== null) { + // switch d with the greates element in the left subtree. + // o should have at most one child. + var o = d.left + // find + while (o.right !== null) { + o = o.right + } + // switch + d.val = o.val + d = o + } + // d has at most one child + // let n be the node that replaces d + var isFakeChild + var child = d.left || d.right + if (child === null) { + isFakeChild = true + child = new N({id: 0}) + child.blacken() + d.right = child + } else { + isFakeChild = false + } + + if (d.parent === null) { + if (!isFakeChild) { + this.root = child + child.blacken() + child._parent = null + } else { + this.root = null + } + return + } else if (d.parent.left === d) { + d.parent.left = child + } else if (d.parent.right === d) { + d.parent.right = child + } else { + throw new Error('Impossible!') + } + if (d.isBlack()) { + if (child.isRed()) { + child.blacken() + } else { + this._fixDelete(child) + } + } + this.root.blacken() + if (isFakeChild) { + if (child.parent.left === child) { + child.parent.left = null + } else if (child.parent.right === child) { + child.parent.right = null + } else { + throw new Error('Impossible #3') + } + } + } + _fixDelete (n) { + function isBlack (node) { + return node !== null ? node.isBlack() : true + } + function isRed (node) { + return node !== null ? node.isRed() : false + } + if (n.parent === null) { + // this can only be called after the first iteration of fixDelete. + return + } + // d was already replaced by the child + // d is not the root + // d and child are black + var sibling = n.sibling + if (isRed(sibling)) { + // make sibling the grandfather + n.parent.redden() + sibling.blacken() + if (n === n.parent.left) { + n.parent.rotateLeft(this) + } else if (n === n.parent.right) { + n.parent.rotateRight(this) + } else { + throw new Error('Impossible #2') + } + sibling = n.sibling + } + // parent, sibling, and children of n are black + if (n.parent.isBlack() && + sibling.isBlack() && + isBlack(sibling.left) && + isBlack(sibling.right) + ) { + sibling.redden() + this._fixDelete(n.parent) + } else if (n.parent.isRed() && + sibling.isBlack() && + isBlack(sibling.left) && + isBlack(sibling.right) + ) { + sibling.redden() + n.parent.blacken() + } else { + if (n === n.parent.left && + sibling.isBlack() && + isRed(sibling.left) && + isBlack(sibling.right) + ) { + sibling.redden() + sibling.left.blacken() + sibling.rotateRight(this) + sibling = n.sibling + } else if (n === n.parent.right && + sibling.isBlack() && + isRed(sibling.right) && + isBlack(sibling.left) + ) { + sibling.redden() + sibling.right.blacken() + sibling.rotateLeft(this) + sibling = n.sibling + } + sibling.color = n.parent.color + n.parent.blacken() + if (n === n.parent.left) { + sibling.right.blacken() + n.parent.rotateLeft(this) + } else { + sibling.left.blacken() + n.parent.rotateRight(this) + } + } + } + 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!') + } + var node = new N(v) + if (this.root !== null) { + var p = this.root // p abbrev. parent + while (true) { + if (Y.utils.smaller(node.val.id, p.val.id)) { + if (p.left === null) { + p.left = node + break + } else { + p = p.left + } + } else if (Y.utils.smaller(p.val.id, node.val.id)) { + if (p.right === null) { + p.right = node + break + } else { + p = p.right + } + } else { + p.val = node.val + return p + } + } + this._fixInsert(node) + } else { + this.root = node + } + this.length++ + this.root.blacken() + return node + } + _fixInsert (n) { + if (n.parent === null) { + n.blacken() + return + } else if (n.parent.isBlack()) { + return + } + var uncle = n.getUncle() + if (uncle !== null && uncle.isRed()) { + // Note: parent: red, uncle: red + n.parent.blacken() + uncle.blacken() + n.grandparent.redden() + this._fixInsert(n.grandparent) + } else { + // Note: parent: red, uncle: black or null + // Now we transform the tree in such a way that + // either of these holds: + // 1) grandparent.left.isRed + // and grandparent.left.left.isRed + // 2) grandparent.right.isRed + // and grandparent.right.right.isRed + if (n === n.parent.right && n.parent === n.grandparent.left) { + n.parent.rotateLeft(this) + // Since we rotated and want to use the previous + // cases, we need to set n in such a way that + // n.parent.isRed again + n = n.left + } else if (n === n.parent.left && n.parent === n.grandparent.right) { + n.parent.rotateRight(this) + // see above + n = n.right + } + // Case 1) or 2) hold from here on. + // Now traverse grandparent, make parent a black node + // on the highest level which holds two red nodes. + n.parent.blacken() + n.grandparent.redden() + if (n === n.parent.left) { + // Case 1 + n.grandparent.rotateRight(this) + } else { + // Case 2 + n.grandparent.rotateLeft(this) + } + } + } + flush () {} + } + + Y.utils.RBTree = RBTree +} diff --git a/src/Struct.js b/src/Struct.js index 8935a4d9..efda733a 100644 --- a/src/Struct.js +++ b/src/Struct.js @@ -73,8 +73,8 @@ export default function extendStruct (Y) { requiredOps: function (op) { return [] // [op.target] }, - execute: function * (op) { - return yield * this.deleteOperation(op.target, op.length || 1) + execute: function (op) { + return this.deleteOperation(op.target, op.length || 1) } } @@ -216,18 +216,18 @@ export default function extendStruct (Y) { } return ids }, - getDistanceToOrigin: function * (op) { + getDistanceToOrigin: function (op) { if (op.left == null) { return 0 } else { var d = 0 - var o = yield * this.getInsertion(op.left) + var o = this.getInsertion(op.left) while (!Y.utils.matchesId(o, op.origin)) { d++ if (o.left == null) { break } else { - o = yield * this.getInsertion(o.left) + o = this.getInsertion(o.left) } } return d @@ -248,7 +248,7 @@ export default function extendStruct (Y) { # case 3: $origin > $o.origin # $this insert_position is to the left of $o (forever!) */ - execute: function * (op) { + execute: function (op) { var i // loop counter // during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd) @@ -258,17 +258,17 @@ export default function extendStruct (Y) { if (op.origin != null) { // TODO: !== instead of != // we save in origin that op originates in it // we need that later when we eventually garbage collect origin (see transaction) - var origin = yield * this.getInsertionCleanEnd(op.origin) + var origin = this.getInsertionCleanEnd(op.origin) if (origin.originOf == null) { origin.originOf = [] } origin.originOf.push(op.id) - yield * this.setOperation(origin) + this.setOperation(origin) if (origin.right != null) { tryToRemergeLater.push(origin.right) } } - var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0) + var distanceToOrigin = i = Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0) // now we begin to insert op in the list of insertions.. var o @@ -277,29 +277,29 @@ export default function extendStruct (Y) { // find o. o is the first conflicting operation if (op.left != null) { - o = yield * this.getInsertionCleanEnd(op.left) + o = this.getInsertionCleanEnd(op.left) if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) { // only if not added previously tryToRemergeLater.push(o.right) } - o = (o.right == null) ? null : yield * this.getOperation(o.right) + o = (o.right == null) ? null : this.getOperation(o.right) } else { // left == null - parent = yield * this.getOperation(op.parent) + parent = this.getOperation(op.parent) let startId = op.parentSub ? parent.map[op.parentSub] : parent.start - start = startId == null ? null : yield * this.getOperation(startId) + start = startId == null ? null : this.getOperation(startId) o = start } // make sure to split op.right if necessary (also add to tryCombineWithLeft) if (op.right != null) { tryToRemergeLater.push(op.right) - yield * this.getInsertionCleanStart(op.right) + this.getInsertionCleanStart(op.right) } // handle conflicts while (true) { if (o != null && !Y.utils.compareIds(o.id, op.right)) { - var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o) + var oOriginDistance = Struct.Insert.getDistanceToOrigin.call(this, o) if (oOriginDistance === i) { // case 1 if (o.id[0] < op.id[0]) { @@ -317,7 +317,7 @@ export default function extendStruct (Y) { } i++ if (o.right != null) { - o = yield * this.getInsertion(o.right) + o = this.getInsertion(o.right) } else { o = null } @@ -330,17 +330,17 @@ export default function extendStruct (Y) { var left = null var right = null if (parent == null) { - parent = yield * this.getOperation(op.parent) + parent = this.getOperation(op.parent) } // reconnect left and set right of op if (op.left != null) { - left = yield * this.getInsertion(op.left) + left = this.getInsertion(op.left) // link left op.right = left.right left.right = op.id - yield * this.setOperation(left) + this.setOperation(left) } else { // set op.right from parent, if necessary op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start @@ -348,33 +348,33 @@ export default function extendStruct (Y) { // reconnect right if (op.right != null) { // TODO: wanna connect right too? - right = yield * this.getOperation(op.right) + right = this.getOperation(op.right) right.left = Y.utils.getLastId(op) // if right exists, and it is supposed to be gc'd. Remove it from the gc if (right.gc != null) { if (right.content != null && right.content.length > 1) { - right = yield * this.getInsertionCleanEnd(right.id) + right = this.getInsertionCleanEnd(right.id) } this.store.removeFromGarbageCollector(right) } - yield * this.setOperation(right) + this.setOperation(right) } // update parents .map/start/end properties if (op.parentSub != null) { if (left == null) { parent.map[op.parentSub] = op.id - yield * this.setOperation(parent) + this.setOperation(parent) } // is a child of a map struct. // Then also make sure that only the most left element is not deleted // We do not call the type in this case (this is what the third parameter is for) if (op.right != null) { - yield * this.deleteOperation(op.right, 1, true) + this.deleteOperation(op.right, 1, true) } if (op.left != null) { - yield * this.deleteOperation(op.id, 1, true) + this.deleteOperation(op.id, 1, true) } } else { if (right == null || left == null) { @@ -384,14 +384,14 @@ export default function extendStruct (Y) { if (left == null) { parent.start = op.id } - yield * this.setOperation(parent) + this.setOperation(parent) } } // try to merge original op.left and op.origin for (i = 0; i < tryToRemergeLater.length; i++) { - var m = yield * this.getOperation(tryToRemergeLater[i]) - yield * this.tryCombineWithLeft(m) + var m = this.getOperation(tryToRemergeLater[i]) + this.tryCombineWithLeft(m) } } } @@ -451,16 +451,16 @@ export default function extendStruct (Y) { */ return [] }, - execute: function * (op) { + execute: function (op) { op.start = null op.end = null }, - ref: function * (op, pos) { + ref: function (op, pos) { if (op.start == null) { return null } var res = null - var o = yield * this.getOperation(op.start) + var o = this.getOperation(op.start) while (true) { if (!o.deleted) { @@ -468,18 +468,18 @@ export default function extendStruct (Y) { pos-- } if (pos >= 0 && o.right != null) { - o = yield * this.getOperation(o.right) + o = this.getOperation(o.right) } else { break } } return res }, - map: function * (o, f) { + map: function (o, f) { o = o.start var res = [] while (o != null) { // TODO: change to != (at least some convention) - var operation = yield * this.getOperation(o) + var operation = this.getOperation(o) if (!operation.deleted) { res.push(f(operation)) } @@ -532,23 +532,23 @@ export default function extendStruct (Y) { requiredOps: function () { return [] }, - execute: function * (op) { + execute: function (op) { op.start = null op.end = null }, /* Get a property by name */ - get: function * (op, name) { + get: function (op, name) { var oid = op.map[name] if (oid != null) { - var res = yield * this.getOperation(oid) + var res = this.getOperation(oid) if (res == null || res.deleted) { return void 0 } else if (res.opContent == null) { return res.content[0] } else { - return yield * this.getType(res.opContent) + return this.getType(res.opContent) } } } @@ -608,7 +608,7 @@ export default function extendStruct (Y) { requiredOps: function () { return [] }, - execute: function * () {}, + execute: function () {}, ref: Struct.List.ref, map: Struct.List.map, /* diff --git a/src/Transaction.js b/src/Transaction.js index bdc43cac..69fc8fd6 100644 --- a/src/Transaction.js +++ b/src/Transaction.js @@ -86,11 +86,11 @@ export default function extendTransaction (Y) { * does not check for Struct.*.requiredOps() * also broadcasts it through the connector */ - * applyCreatedOperations (ops) { + applyCreatedOperations (ops) { var send = [] for (var i = 0; i < ops.length; i++) { var op = ops[i] - yield * this.store.tryExecute.call(this, op) + this.store.tryExecute.call(this, op) if (op.id == null || typeof op.id[1] !== 'string') { send.push(Y.Struct[op.struct].encode(op)) } @@ -104,17 +104,17 @@ export default function extendTransaction (Y) { } } - * deleteList (start) { + deleteList (start) { while (start != null) { - start = yield * this.getOperation(start) + start = this.getOperation(start) if (!start.gc) { start.gc = true start.deleted = true - yield * this.setOperation(start) + this.setOperation(start) var delLength = start.content != null ? start.content.length : 1 - yield * this.markDeleted(start.id, delLength) + this.markDeleted(start.id, delLength) if (start.opContent != null) { - yield * this.deleteOperation(start.opContent) + this.deleteOperation(start.opContent) } this.store.queueGarbageCollector(start.id) } @@ -125,14 +125,14 @@ export default function extendTransaction (Y) { /* Mark an operation as deleted, and add it to the GC, if possible. */ - * deleteOperation (targetId, length, preventCallType) /* :Generator */ { + deleteOperation (targetId, length, preventCallType) /* :Generator */ { if (length == null) { length = 1 } - yield * this.markDeleted(targetId, length) + this.markDeleted(targetId, length) while (length > 0) { var callType = false - var target = yield * this.os.findWithUpperBound([targetId[0], targetId[1] + length - 1]) + var target = this.os.findWithUpperBound([targetId[0], targetId[1] + length - 1]) var targetLength = target != null && target.content != null ? target.content.length : 1 if (target == null || target.id[0] !== targetId[0] || target.id[1] + targetLength <= targetId[1]) { // does not exist or is not in the range of the deletion @@ -143,12 +143,12 @@ export default function extendTransaction (Y) { if (!target.deleted) { if (target.id[1] < targetId[1]) { // starts to the left of the deletion range - target = yield * this.getInsertionCleanStart(targetId) + target = this.getInsertionCleanStart(targetId) targetLength = target.content.length // must have content property! } if (target.id[1] + targetLength > targetId[1] + length) { // ends to the right of the deletion range - target = yield * this.getInsertionCleanEnd([targetId[0], targetId[1] + length - 1]) + target = this.getInsertionCleanEnd([targetId[0], targetId[1] + length - 1]) targetLength = target.content.length } } @@ -163,35 +163,35 @@ export default function extendTransaction (Y) { // delete containing lists if (target.start != null) { // TODO: don't do it like this .. -.- - yield * this.deleteList(target.start) - // yield * this.deleteList(target.id) -- do not gc itself because this may still get referenced + this.deleteList(target.start) + // this.deleteList(target.id) -- do not gc itself because this may still get referenced } if (target.map != null) { for (var name in target.map) { - yield * this.deleteList(target.map[name]) + this.deleteList(target.map[name]) } // TODO: here to.. (see above) - // yield * this.deleteList(target.id) -- see above + // this.deleteList(target.id) -- see above } if (target.opContent != null) { - yield * this.deleteOperation(target.opContent) + this.deleteOperation(target.opContent) // target.opContent = null } if (target.requires != null) { for (var i = 0; i < target.requires.length; i++) { - yield * this.deleteOperation(target.requires[i]) + this.deleteOperation(target.requires[i]) } } } var left if (target.left != null) { - left = yield * this.getInsertion(target.left) + left = this.getInsertion(target.left) } else { left = null } // set here because it was deleted and/or gc'd - yield * this.setOperation(target) + this.setOperation(target) /* Check if it is possible to add right to the gc. @@ -200,12 +200,12 @@ export default function extendTransaction (Y) { */ var right if (target.right != null) { - right = yield * this.getOperation(target.right) + right = this.getOperation(target.right) } else { right = null } if (callType && !preventCallType) { - yield * this.store.operationAdded(this, { + this.store.operationAdded(this, { struct: 'Delete', target: target.id, length: targetLength, @@ -213,9 +213,9 @@ export default function extendTransaction (Y) { }) } // need to gc in the end! - yield * this.store.addToGarbageCollector.call(this, target, left) + this.store.addToGarbageCollector.call(this, target, left) if (right != null) { - yield * this.store.addToGarbageCollector.call(this, right, target) + this.store.addToGarbageCollector.call(this, right, target) } } } @@ -223,25 +223,25 @@ export default function extendTransaction (Y) { /* Mark an operation as deleted&gc'd */ - * markGarbageCollected (id, len) { + markGarbageCollected (id, len) { // this.mem.push(["gc", id]); - this.store.addToDebug('yield * this.markGarbageCollected(', id, ', ', len, ')') - var n = yield * this.markDeleted(id, len) + this.store.addToDebug('this.markGarbageCollected(', id, ', ', len, ')') + var n = this.markDeleted(id, len) if (n.id[1] < id[1] && !n.gc) { // un-extend left var newlen = n.len - (id[1] - n.id[1]) n.len -= newlen - yield * this.ds.put(n) + this.ds.put(n) n = {id: id, len: newlen, gc: false} - yield * this.ds.put(n) + this.ds.put(n) } // get prev&next before adding a new operation - var prev = yield * this.ds.findPrev(id) - var next = yield * this.ds.findNext(id) + var prev = this.ds.findPrev(id) + var next = this.ds.findNext(id) if (id[1] + len < n.id[1] + n.len && !n.gc) { // un-extend right - yield * this.ds.put({id: [id[0], id[1] + len], len: n.len - len, gc: false}) + this.ds.put({id: [id[0], id[1] + len], len: n.len - len, gc: false}) n.len = len } // set gc'd @@ -253,7 +253,7 @@ export default function extendTransaction (Y) { Y.utils.compareIds([prev.id[0], prev.id[1] + prev.len], n.id) ) { prev.len += n.len - yield * this.ds.delete(n.id) + this.ds.delete(n.id) n = prev // ds.put n here? } @@ -264,22 +264,22 @@ export default function extendTransaction (Y) { Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id) ) { n.len += next.len - yield * this.ds.delete(next.id) + this.ds.delete(next.id) } - yield * this.ds.put(n) - yield * this.updateState(n.id[0]) + this.ds.put(n) + this.updateState(n.id[0]) } /* Mark an operation as deleted. returns the delete node */ - * markDeleted (id, length) { + markDeleted (id, length) { if (length == null) { length = 1 } // this.mem.push(["del", id]); - var n = yield * this.ds.findWithUpperBound(id) + var n = this.ds.findWithUpperBound(id) if (n != null && n.id[0] === id[0]) { if (n.id[1] <= id[1] && id[1] <= n.id[1] + n.len) { // id is in n's range @@ -293,7 +293,7 @@ export default function extendTransaction (Y) { if (diff < length) { // a partial deletion n = {id: [id[0], id[1] + diff], len: length - diff, gc: false} - yield * this.ds.put(n) + this.ds.put(n) } else { // already gc'd throw new Error( @@ -308,15 +308,15 @@ export default function extendTransaction (Y) { } else { // cannot extend left (there is no left!) n = {id: id, len: length, gc: false} - yield * this.ds.put(n) // TODO: you double-put !! + this.ds.put(n) // TODO: you double-put !! } } else { // cannot extend left n = {id: id, len: length, gc: false} - yield * this.ds.put(n) + this.ds.put(n) } // can extend right? - var next = yield * this.ds.findNext(n.id) + var next = this.ds.findNext(n.id) if ( next != null && n.id[0] === next.id[0] && @@ -332,8 +332,8 @@ export default function extendTransaction (Y) { // delete the missing range after next diff = diff - next.len // missing range after next if (diff > 0) { - yield * this.ds.put(n) // unneccessary? TODO! - yield * this.markDeleted([next.id[0], next.id[1] + next.len], diff) + this.ds.put(n) // unneccessary? TODO! + this.markDeleted([next.id[0], next.id[1] + next.len], diff) } } break @@ -342,8 +342,8 @@ export default function extendTransaction (Y) { if (diff > next.len) { // n is even longer than next // get next.next, and try to extend it - var _next = yield * this.ds.findNext(next.id) - yield * this.ds.delete(next.id) + var _next = this.ds.findNext(next.id) + this.ds.delete(next.id) if (_next == null || n.id[0] !== _next.id[0]) { break } else { @@ -354,13 +354,13 @@ export default function extendTransaction (Y) { } else { // n just partially overlaps with next. extend n, delete next, and break this loop n.len += next.len - diff - yield * this.ds.delete(next.id) + this.ds.delete(next.id) break } } } } - yield * this.ds.put(n) + this.ds.put(n) return n } /* @@ -368,7 +368,7 @@ export default function extendTransaction (Y) { other clients (e.g. master). This will query the database for operations that can be gc'd and add them to the garbage collector. */ - * garbageCollectAfterSync () { + garbageCollectAfterSync () { // debugger if (this.store.gc1.length > 0 || this.store.gc2.length > 0) { console.warn('gc should be empty after sync') @@ -376,28 +376,28 @@ export default function extendTransaction (Y) { if (!this.store.gc) { return } - yield * this.os.iterate(this, null, null, function * (op) { + this.os.iterate(this, null, null, function (op) { if (op.gc) { delete op.gc - yield * this.setOperation(op) + this.setOperation(op) } if (op.parent != null) { - var parentDeleted = yield * this.isDeleted(op.parent) + var parentDeleted = this.isDeleted(op.parent) if (parentDeleted) { op.gc = true if (!op.deleted) { - yield * this.markDeleted(op.id, op.content != null ? op.content.length : 1) + this.markDeleted(op.id, op.content != null ? op.content.length : 1) op.deleted = true if (op.opContent != null) { - yield * this.deleteOperation(op.opContent) + this.deleteOperation(op.opContent) } if (op.requires != null) { for (var i = 0; i < op.requires.length; i++) { - yield * this.deleteOperation(op.requires[i]) + this.deleteOperation(op.requires[i]) } } } - yield * this.setOperation(op) + this.setOperation(op) this.store.gc1.push(op.id) // this is ok becaues its shortly before sync (otherwise use queueGarbageCollector!) return } @@ -405,9 +405,9 @@ export default function extendTransaction (Y) { if (op.deleted) { var left = null if (op.left != null) { - left = yield * this.getInsertion(op.left) + left = this.getInsertion(op.left) } - yield * this.store.addToGarbageCollector.call(this, op, left) + this.store.addToGarbageCollector.call(this, op, left) } }) } @@ -420,10 +420,10 @@ export default function extendTransaction (Y) { * reset parent.end * reset origins of all right ops */ - * garbageCollectOperation (id) { - this.store.addToDebug('yield * this.garbageCollectOperation(', id, ')') - var o = yield * this.getOperation(id) - yield * this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd + garbageCollectOperation (id) { + this.store.addToDebug('this.garbageCollectOperation(', id, ')') + var o = this.getOperation(id) + this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd // if op exists, then clean that mess up.. if (o != null) { var deps = [] @@ -434,32 +434,32 @@ export default function extendTransaction (Y) { deps = deps.concat(o.requires) } for (var i = 0; i < deps.length; i++) { - var dep = yield * this.getOperation(deps[i]) + var dep = this.getOperation(deps[i]) if (dep != null) { if (!dep.deleted) { - yield * this.deleteOperation(dep.id) - dep = yield * this.getOperation(dep.id) + this.deleteOperation(dep.id) + dep = this.getOperation(dep.id) } dep.gc = true - yield * this.setOperation(dep) + this.setOperation(dep) this.store.queueGarbageCollector(dep.id) } else { - yield * this.markGarbageCollected(deps[i], 1) + this.markGarbageCollected(deps[i], 1) } } // remove gc'd op from the left op, if it exists if (o.left != null) { - var left = yield * this.getInsertion(o.left) + var left = this.getInsertion(o.left) left.right = o.right - yield * this.setOperation(left) + this.setOperation(left) } // remove gc'd op from the right op, if it exists // also reset origins of right ops if (o.right != null) { - var right = yield * this.getOperation(o.right) + var right = this.getOperation(o.right) right.left = o.left - yield * this.setOperation(right) + this.setOperation(right) if (o.originOf != null && o.originOf.length > 0) { // find new origin of right ops @@ -478,7 +478,7 @@ export default function extendTransaction (Y) { right.origin = neworigin // search until you find origin pointer to the left of o if (right.right != null) { - var i = yield * this.getOperation(right.right) + var i = this.getOperation(right.right) var ids = [o.id, o.right] while (ids.some(function (id) { return Y.utils.compareIds(id, i.origin) @@ -486,14 +486,14 @@ export default function extendTransaction (Y) { if (Y.utils.compareIds(i.origin, o.id)) { // reset origin of i i.origin = neworigin - yield * this.setOperation(i) + this.setOperation(i) } // get next i if (i.right == null) { break } else { ids.push(i.id) - i = yield * this.getOperation(i.right) + i = this.getOperation(i.right) } } } @@ -502,20 +502,20 @@ export default function extendTransaction (Y) { // ** Now the new implementation starts ** // reset neworigin of all originOf[*] for (var _i in o.originOf) { - var originsIn = yield * this.getOperation(o.originOf[_i]) + var originsIn = this.getOperation(o.originOf[_i]) if (originsIn != null) { originsIn.origin = neworigin - yield * this.setOperation(originsIn) + this.setOperation(originsIn) } } if (neworigin != null) { - var neworigin_ = yield * this.getInsertion(neworigin) + var neworigin_ = this.getInsertion(neworigin) if (neworigin_.originOf == null) { neworigin_.originOf = o.originOf } else { neworigin_.originOf = o.originOf.concat(neworigin_.originOf) } - yield * this.setOperation(neworigin_) + this.setOperation(neworigin_) } // we don't need to set right here, because // right should be in o.originOf => it is set it the previous for loop @@ -524,15 +524,15 @@ export default function extendTransaction (Y) { // o may originate in another operation. // Since o is deleted, we have to reset o.origin's `originOf` property if (o.origin != null) { - var origin = yield * this.getInsertion(o.origin) + var origin = this.getInsertion(o.origin) origin.originOf = origin.originOf.filter(function (_id) { return !Y.utils.compareIds(id, _id) }) - yield * this.setOperation(origin) + this.setOperation(origin) } var parent if (o.parent != null) { - parent = yield * this.getOperation(o.parent) + parent = this.getOperation(o.parent) } // remove gc'd op from parent, if it exists if (parent != null) { @@ -559,38 +559,38 @@ export default function extendTransaction (Y) { } } if (setParent) { - yield * this.setOperation(parent) + this.setOperation(parent) } } // finally remove it from the os - yield * this.removeOperation(o.id) + this.removeOperation(o.id) } } - * checkDeleteStoreForState (state) { - var n = yield * this.ds.findWithUpperBound([state.user, state.clock]) + checkDeleteStoreForState (state) { + var n = this.ds.findWithUpperBound([state.user, state.clock]) if (n != null && n.id[0] === state.user && n.gc) { state.clock = Math.max(state.clock, n.id[1] + n.len) } } - * updateState (user) { - var state = yield * this.getState(user) - yield * this.checkDeleteStoreForState(state) - var o = yield * this.getInsertion([user, state.clock]) + updateState (user) { + var state = this.getState(user) + this.checkDeleteStoreForState(state) + var o = this.getInsertion([user, state.clock]) var oLength = (o != null && o.content != null) ? o.content.length : 1 while (o != null && user === o.id[0] && o.id[1] <= state.clock && o.id[1] + oLength > state.clock) { // either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS state.clock += oLength - yield * this.checkDeleteStoreForState(state) - o = yield * this.os.findNext(o.id) + this.checkDeleteStoreForState(state) + o = this.os.findNext(o.id) oLength = (o != null && o.content != null) ? o.content.length : 1 } - yield * this.setState(state) + this.setState(state) } /* apply a delete set in order to get the state of the supplied ds */ - * applyDeleteSet (decoder) { + applyDeleteSet (decoder) { var deletions = [] let dsLength = decoder.readUint32() @@ -606,7 +606,7 @@ export default function extendTransaction (Y) { } var pos = 0 var d = dv[pos] - yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) { + this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function (n) { // cases: // 1. d deletes something to the right of n // => go to next n (break) @@ -655,14 +655,14 @@ export default function extendTransaction (Y) { for (var i = 0; i < deletions.length; i++) { var del = deletions[i] // always try to delete.. - yield * this.deleteOperation([del[0], del[1]], del[2]) + this.deleteOperation([del[0], del[1]], del[2]) if (del[3]) { // gc.. - yield * this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd + this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd // remove operation.. var counter = del[1] + del[2] while (counter >= del[1]) { - var o = yield * this.os.findWithUpperBound([del[0], counter - 1]) + var o = this.os.findWithUpperBound([del[0], counter - 1]) if (o == null) { break } @@ -673,14 +673,14 @@ export default function extendTransaction (Y) { } if (o.id[1] + oLen > del[1] + del[2]) { // overlaps right - o = yield * this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1]) + o = this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1]) } if (o.id[1] < del[1]) { // overlaps left - o = yield * this.getInsertionCleanStart([del[0], del[1]]) + o = this.getInsertionCleanStart([del[0], del[1]]) } counter = o.id[1] - yield * this.garbageCollectOperation(o.id) + this.garbageCollectOperation(o.id) } } if (this.store.forwardAppliedOperations || this.store.y.persistence != null) { @@ -695,16 +695,16 @@ export default function extendTransaction (Y) { } } } - * isGarbageCollected (id) { - var n = yield * this.ds.findWithUpperBound(id) + isGarbageCollected (id) { + var n = this.ds.findWithUpperBound(id) return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len && n.gc } /* A DeleteSet (ds) describes all the deleted ops in the OS */ - * writeDeleteSet (encoder) { + writeDeleteSet (encoder) { var ds = new Map() - yield * this.ds.iterate(this, null, null, function * (n) { + this.ds.iterate(this, null, null, function (n) { var user = n.id[0] var counter = n.id[1] var len = n.len @@ -731,16 +731,16 @@ export default function extendTransaction (Y) { } } } - * isDeleted (id) { - var n = yield * this.ds.findWithUpperBound(id) + isDeleted (id) { + var n = this.ds.findWithUpperBound(id) return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len } - * setOperation (op) { - yield * this.os.put(op) + setOperation (op) { + this.os.put(op) return op } - * addOperation (op) { - yield * this.os.put(op) + addOperation (op) { + this.os.put(op) // case op is created by this user, op is already broadcasted in applyCreatedOperations if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') { if (this.store.forwardAppliedOperations) { @@ -753,7 +753,7 @@ export default function extendTransaction (Y) { } } // if insertion, try to combine with left insertion (if both have content property) - * tryCombineWithLeft (op) { + tryCombineWithLeft (op) { if ( op != null && op.left != null && @@ -761,7 +761,7 @@ export default function extendTransaction (Y) { op.left[0] === op.id[0] && Y.utils.compareIds(op.left, op.origin) ) { - var left = yield * this.getInsertion(op.left) + var left = this.getInsertion(op.left) if (left.content != null && left.id[1] + left.content.length === op.id[1] && left.originOf.length === 1 && @@ -776,13 +776,13 @@ export default function extendTransaction (Y) { } left.content = left.content.concat(op.content) left.right = op.right - yield * this.os.delete(op.id) - yield * this.setOperation(left) + this.os.delete(op.id) + this.setOperation(left) } } } - * getInsertion (id) { - var ins = yield * this.os.findWithUpperBound(id) + getInsertion (id) { + var ins = this.os.findWithUpperBound(id) if (ins == null) { return null } else { @@ -794,14 +794,14 @@ export default function extendTransaction (Y) { } } } - * getInsertionCleanStartEnd (id) { - yield * this.getInsertionCleanStart(id) - return yield * this.getInsertionCleanEnd(id) + getInsertionCleanStartEnd (id) { + this.getInsertionCleanStart(id) + return this.getInsertionCleanEnd(id) } // Return an insertion such that id is the first element of content // This function manipulates an operation, if necessary - * getInsertionCleanStart (id) { - var ins = yield * this.getInsertion(id) + getInsertionCleanStart (id) { + var ins = this.getInsertion(id) if (ins != null) { if (ins.id[1] === id[1]) { return ins @@ -815,8 +815,8 @@ export default function extendTransaction (Y) { left.right = ins.id ins.left = leftLid // debugger // check - yield * this.setOperation(left) - yield * this.setOperation(ins) + this.setOperation(left) + this.setOperation(ins) if (left.gc) { this.store.queueGarbageCollector(ins.id) } @@ -828,8 +828,8 @@ export default function extendTransaction (Y) { } // Return an insertion such that id is the last element of content // This function manipulates an operation, if necessary - * getInsertionCleanEnd (id) { - var ins = yield * this.getInsertion(id) + getInsertionCleanEnd (id) { + var ins = this.getInsertion(id) if (ins != null) { if (ins.content == null || (ins.id[1] + ins.content.length - 1 === id[1])) { return ins @@ -843,8 +843,8 @@ export default function extendTransaction (Y) { ins.right = right.id right.left = insLid // debugger // check - yield * this.setOperation(right) - yield * this.setOperation(ins) + this.setOperation(right) + this.setOperation(ins) if (ins.gc) { this.store.queueGarbageCollector(right.id) } @@ -854,8 +854,8 @@ export default function extendTransaction (Y) { return null } } - * getOperation (id/* :any */)/* :Transaction */ { - var o = yield * this.os.find(id) + getOperation (id/* :any */)/* :Transaction */ { + var o = this.os.find(id) if (id[0] !== 0xFFFFFF || o != null) { return o } else { // type is string @@ -870,7 +870,7 @@ export default function extendTransaction (Y) { } var op = Y.Struct[struct].create(id, args) op.type = comp[1] - yield * this.setOperation(op) + this.setOperation(op) return op } else { throw new Error( @@ -880,18 +880,18 @@ export default function extendTransaction (Y) { } } } - * removeOperation (id) { - yield * this.os.delete(id) + removeOperation (id) { + this.os.delete(id) } - * setState (state) { + setState (state) { var val = { id: [state.user], clock: state.clock } - yield * this.ss.put(val) + this.ss.put(val) } - * getState (user) { - var n = yield * this.ss.find([user]) + getState (user) { + var n = this.ss.find([user]) var clock = n == null ? null : n.clock if (clock == null) { clock = 0 @@ -901,9 +901,9 @@ export default function extendTransaction (Y) { clock: clock } } - * getStateVector () { + getStateVector () { var stateVector = [] - yield * this.ss.iterate(this, null, null, function * (n) { + this.ss.iterate(this, null, null, function (n) { stateVector.push({ user: n.id[0], clock: n.clock @@ -911,18 +911,18 @@ export default function extendTransaction (Y) { }) return stateVector } - * getStateSet () { + getStateSet () { var ss = {} - yield * this.ss.iterate(this, null, null, function * (n) { + this.ss.iterate(this, null, null, function (n) { ss[n.id[0]] = n.clock }) return ss } - * writeStateSet (encoder) { + writeStateSet (encoder) { let lenPosition = encoder.pos let len = 0 encoder.writeUint32(0) - yield * this.ss.iterate(this, null, null, function * (n) { + this.ss.iterate(this, null, null, function (n) { encoder.writeVarUint(n.id[0]) encoder.writeVarUint(n.clock) len++ @@ -976,14 +976,14 @@ export default function extendTransaction (Y) { 3. Found o = op.origin -> set op.left = op.origin, and send it to the user. start again from 1. (set op = o) 4. Found some o -> set o.right = op, o.left = o.origin, send it to the user, continue */ - * getOperations (startSS) { + getOperations (startSS) { // TODO: use bounds here! if (startSS == null) { startSS = new Map() } var send = [] - var endSV = yield * this.getStateVector() + var endSV = this.getStateVector() for (let endState of endSV) { let user = endState.user if (user === 0xFFFFFF) { @@ -993,7 +993,7 @@ export default function extendTransaction (Y) { if (startPos > 0) { // There is a change that [user, startPos] is in a composed Insertion (with a smaller counter) // find out if that is the case - let firstMissing = yield * this.getInsertion([user, startPos]) + let firstMissing = this.getInsertion([user, startPos]) if (firstMissing != null) { // update startPos startPos = firstMissing.id[1] @@ -1007,7 +1007,7 @@ export default function extendTransaction (Y) { if (user === 0xFFFFFF) { continue } - yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) { + this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function (op) { op = Y.Struct[op.struct].encode(op) if (op.struct !== 'Insert') { send.push(op) @@ -1038,7 +1038,7 @@ export default function extendTransaction (Y) { */ break } - o = yield * this.getInsertion(o.left) + o = this.getInsertion(o.left) // we set another o, check if we can reduce $missingOrigins while (missingOrigins.length > 0 && Y.utils.matchesId(o, missingOrigins[missingOrigins.length - 1].origin)) { missingOrigins.pop() @@ -1076,7 +1076,7 @@ export default function extendTransaction (Y) { return send.reverse() } - * writeOperations (encoder, decoder) { + writeOperations (encoder, decoder) { let ss = new Map() let ssLength = decoder.readUint32() for (let i = 0; i < ssLength; i++) { @@ -1084,7 +1084,7 @@ export default function extendTransaction (Y) { let clock = decoder.readVarUint() ss.set(user, clock) } - let ops = yield * this.getOperations(ss) + let ops = this.getOperations(ss) encoder.writeUint32(ops.length) for (let i = 0; i < ops.length; i++) { let op = ops[i] @@ -1092,17 +1092,17 @@ export default function extendTransaction (Y) { } } - * toBinary () { + toBinary () { let encoder = new BinaryEncoder() - yield * this.writeOperationsUntransformed(encoder) - yield * this.writeDeleteSet(encoder) + this.writeOperationsUntransformed(encoder) + this.writeDeleteSet(encoder) return encoder.createBuffer() } - * fromBinary (buffer) { + fromBinary (buffer) { let decoder = new BinaryDecoder(buffer) - yield * this.applyOperationsUntransformed(decoder) - yield * this.applyDeleteSet(decoder) + this.applyOperationsUntransformed(decoder) + this.applyDeleteSet(decoder) } /* @@ -1110,43 +1110,43 @@ export default function extendTransaction (Y) { * You can apply these operations using .applyOperationsUntransformed(ops) * */ - * writeOperationsUntransformed (encoder) { + writeOperationsUntransformed (encoder) { let lenPosition = encoder.pos let len = 0 encoder.writeUint32(0) // placeholder - yield * this.os.iterate(this, null, null, function * (op) { + this.os.iterate(this, null, null, function (op) { if (op.id[0] !== 0xFFFFFF) { len++ Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op)) } }) encoder.setUint32(lenPosition, len) - yield * this.writeStateSet(encoder) + this.writeStateSet(encoder) } - * applyOperationsUntransformed (decoder) { + applyOperationsUntransformed (decoder) { let len = decoder.readUint32() for (let i = 0; i < len; i++) { let op = Y.Struct.binaryDecodeOperation(decoder) - yield * this.os.put(op) + this.os.put(op) } - yield * this.os.iterate(this, null, null, function * (op) { + this.os.iterate(this, null, null, function (op) { if (op.parent != null) { if (op.struct === 'Insert') { // update parents .map/start/end properties if (op.parentSub != null && op.left == null) { // op is child of Map - let parent = yield * this.getOperation(op.parent) + let parent = this.getOperation(op.parent) parent.map[op.parentSub] = op.id - yield * this.setOperation(parent) + this.setOperation(parent) } else if (op.right == null || op.left == null) { - let parent = yield * this.getOperation(op.parent) + let parent = this.getOperation(op.parent) if (op.right == null) { parent.end = Y.utils.getLastId(op) } if (op.left == null) { parent.start = op.id } - yield * this.setOperation(parent) + this.setOperation(parent) } } } @@ -1155,14 +1155,14 @@ export default function extendTransaction (Y) { for (let i = 0; i < stateSetLength; i++) { let user = decoder.readVarUint() let clock = decoder.readVarUint() - yield * this.ss.put({ + this.ss.put({ id: [user], clock: clock }) } } /* this is what we used before.. use this as a reference.. - * makeOperationReady (startSS, op) { + makeOperationReady (startSS, op) { op = Y.Struct[op.struct].encode(op) op = Y.utils.copyObject(op) -- use copyoperation instead now! var o = op @@ -1172,7 +1172,7 @@ export default function extendTransaction (Y) { // or the o that has no origin to the right of op // (this is why we use the ids array) while (o.right != null) { - var right = yield * this.getOperation(o.right) + var right = this.getOperation(o.right) if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) { return Y.utils.compareIds(id, right.origin) })) { @@ -1186,10 +1186,10 @@ export default function extendTransaction (Y) { return op } */ - * flush () { - yield * this.os.flush() - yield * this.ss.flush() - yield * this.ds.flush() + flush () { + this.os.flush() + this.ss.flush() + this.ds.flush() } } Y.Transaction = TransactionInterface diff --git a/src/Utils.js b/src/Utils.js index f5c0298c..03e841a3 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -315,7 +315,7 @@ export default function Utils (Y) { this.awaiting++ ops.map(Y.utils.copyOperation).forEach(this.onevent) } - * awaitOps (transaction, f, args) { + awaitOps (transaction, f, args) { function notSoSmartSort (array) { // this function sorts insertions in a executable order var result = [] @@ -339,7 +339,7 @@ export default function Utils (Y) { } var before = this.waiting.length // somehow create new operations - yield * f.apply(transaction, args) + f.apply(transaction, args) // remove all appended ops / awaited ops this.waiting.splice(before) if (this.awaiting > 0) this.awaiting-- @@ -349,7 +349,7 @@ export default function Utils (Y) { 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) + var _o = transaction.getInsertion(o.id) if (_o.parentSub != null && _o.left != null) { // if o is an insertion of a map struc (parentSub is defined), then it shouldn't be necessary to compute left this.waiting.splice(i, 1) @@ -361,10 +361,10 @@ export default function Utils (Y) { o.left = null } else { // find next undeleted op - var left = yield * transaction.getInsertion(_o.left) + var left = transaction.getInsertion(_o.left) while (left.deleted != null) { if (left.left != null) { - left = yield * transaction.getInsertion(left.left) + left = transaction.getInsertion(left.left) } else { left = null break @@ -690,7 +690,7 @@ export default function Utils (Y) { this.writeBuffer = createEmptyOpsArray(5) this.readBuffer = createEmptyOpsArray(10) } - * find (id, noSuperCall) { + find (id, noSuperCall) { var i, r for (i = this.readBuffer.length - 1; i >= 0; i--) { r = this.readBuffer[i] @@ -716,7 +716,7 @@ export default function Utils (Y) { if (i < 0 && noSuperCall === undefined) { // did not reach break in last loop // read id and put it to the end of readBuffer - o = yield * super.find(id) + o = super.find(id) } if (o != null) { for (i = 0; i < this.readBuffer.length - 1; i++) { @@ -726,7 +726,7 @@ export default function Utils (Y) { } return o } - * put (o) { + put (o) { var id = o.id var i, r // helper variables for (i = this.writeBuffer.length - 1; i >= 0; i--) { @@ -746,7 +746,7 @@ export default function Utils (Y) { // write writeBuffer[0] var write = this.writeBuffer[0] if (write.id[0] !== null) { - yield * super.put(write) + super.put(write) } // put o to the end of writeBuffer for (i = 0; i < this.writeBuffer.length - 1; i++) { @@ -766,7 +766,7 @@ export default function Utils (Y) { } this.readBuffer[this.readBuffer.length - 1] = o } - * delete (id) { + delete (id) { var i, r for (i = 0; i < this.readBuffer.length; i++) { r = this.readBuffer[i] @@ -776,44 +776,44 @@ export default function Utils (Y) { } } } - yield * this.flush() - yield * super.delete(id) + this.flush() + super.delete(id) } - * findWithLowerBound (id) { - var o = yield * this.find(id, true) + findWithLowerBound (id) { + var o = this.find(id, true) if (o != null) { return o } else { - yield * this.flush() - return yield * super.findWithLowerBound.apply(this, arguments) + this.flush() + return super.findWithLowerBound.apply(this, arguments) } } - * findWithUpperBound (id) { - var o = yield * this.find(id, true) + findWithUpperBound (id) { + var o = this.find(id, true) if (o != null) { return o } else { - yield * this.flush() - return yield * super.findWithUpperBound.apply(this, arguments) + this.flush() + return super.findWithUpperBound.apply(this, arguments) } } - * findNext () { - yield * this.flush() - return yield * super.findNext.apply(this, arguments) + findNext () { + this.flush() + return super.findNext.apply(this, arguments) } - * findPrev () { - yield * this.flush() - return yield * super.findPrev.apply(this, arguments) + findPrev () { + this.flush() + return super.findPrev.apply(this, arguments) } - * iterate () { - yield * this.flush() - yield * super.iterate.apply(this, arguments) + iterate () { + this.flush() + super.iterate.apply(this, arguments) } - * flush () { + flush () { for (var i = 0; i < this.writeBuffer.length; i++) { var write = this.writeBuffer[i] if (write.id[0] !== null) { - yield * super.put(write) + super.put(write) this.writeBuffer[i] = { id: [null, null] } diff --git a/src/y-memory.js b/src/y-memory.js new file mode 100644 index 00000000..67a593f2 --- /dev/null +++ b/src/y-memory.js @@ -0,0 +1,64 @@ + +import extendRBTree from './RedBlackTree' + +export default function extend (Y) { + extendRBTree(Y) + + class Transaction extends Y.Transaction { + constructor (store) { + super(store) + this.store = store + this.ss = store.ss + this.os = store.os + this.ds = store.ds + } + } + var Store = Y.utils.RBTree + var BufferedStore = Y.utils.createSmallLookupBuffer(Store) + + class Database extends Y.AbstractDatabase { + constructor (y, opts) { + super(y, opts) + this.os = new BufferedStore() + this.ds = new Store() + this.ss = new BufferedStore() + } + logTable () { + var self = this + self.requestTransaction(function () { + console.log('User: ', this.store.y.connector.userId, "==============================") // eslint-disable-line + console.log("State Set (SS):", this.getStateSet()) // eslint-disable-line + console.log("Operation Store (OS):") // eslint-disable-line + this.os.logTable() // eslint-disable-line + console.log("Deletion Store (DS):") //eslint-disable-line + this.ds.logTable() // eslint-disable-line + if (this.store.gc1.length > 0 || this.store.gc2.length > 0) { + console.warn('GC1|2 not empty!', this.store.gc1, this.store.gc2) + } + if (JSON.stringify(this.store.listenersById) !== '{}') { + console.warn('listenersById not empty!') + } + if (JSON.stringify(this.store.listenersByIdExecuteNow) !== '[]') { + console.warn('listenersByIdExecuteNow not empty!') + } + if (this.store.transactionInProgress) { + console.warn('Transaction still in progress!') + } + }, true) + } + transact (makeGen) { + const t = new Transaction(this) + while (makeGen != null) { + makeGen.call(t) + makeGen = this.getNextRequest() + } + } + destroy () { + super.destroy() + delete this.os + delete this.ss + delete this.ds + } + } + Y.memory = Database +} diff --git a/src/y.js b/src/y.js index ebc07591..07403e3a 100644 --- a/src/y.js +++ b/src/y.js @@ -4,6 +4,7 @@ import extendDatabase from './Database.js' import extendTransaction from './Transaction.js' import extendStruct from './Struct.js' import extendUtils from './Utils.js' +import extendMemory from './y-memory.js' import debug from 'debug' import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js' @@ -13,6 +14,7 @@ extendDatabase(Y) extendTransaction(Y) extendStruct(Y) extendUtils(Y) +extendMemory(Y) Y.debug = debug debug.formatters.Y = formatYjsMessage @@ -182,7 +184,7 @@ class YConfig extends Y.utils.NamedEventHandler { var opts = this.options var share = {} this.share = share - this.db.requestTransaction(function * requestTransaction () { + this.db.requestTransaction(function requestTransaction () { // create shared object for (var propertyname in opts.share) { var typeConstructor = opts.share[propertyname].split('(') @@ -195,7 +197,7 @@ class YConfig extends Y.utils.NamedEventHandler { var typedef = type.typeDefinition var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeArgs] let args = Y.utils.parseTypeDefinition(type, typeArgs) - share[propertyname] = yield * this.store.initType.call(this, id, args) + share[propertyname] = this.store.initType.call(this, id, args) } }) if (this.persistence != null) { @@ -250,8 +252,8 @@ class YConfig extends Y.utils.NamedEventHandler { return this.db.whenTransactionsFinished().then(function () { self.db.destroyTypes() // make sure to wait for all transactions before destroying the db - self.db.requestTransaction(function * () { - yield * self.db.destroy() + self.db.requestTransaction(function () { + self.db.destroy() }) return self.db.whenTransactionsFinished() }) diff --git a/test/encode-decode.js b/test/encode-decode.js index ce6d69b0..bace3a09 100644 --- a/test/encode-decode.js +++ b/test/encode-decode.js @@ -161,7 +161,9 @@ test('encode/decode List operations', async function binList (t) { testEncoding(t, writeList, readList, { struct: 'List', id: [100, 33], - type: 'Array' + type: 'Array', + start: null, + end: null }) }) diff --git a/test/red-black-tree.js b/test/red-black-tree.js new file mode 100644 index 00000000..14a19164 --- /dev/null +++ b/test/red-black-tree.js @@ -0,0 +1,217 @@ +import Y from '../src/y.js' +import Chance from 'chance' +import { test, proxyConsole } from 'cutest' + +proxyConsole() + +var numberOfRBTreeTests = 10000 + +function checkRedNodesDoNotHaveBlackChildren (t, tree) { + let correct = true + function traverse (n) { + if (n == null) { + return + } + if (n.isRed()) { + if (n.left != null) { + correct = correct && !n.left.isRed() + } + if (n.right != null) { + correct = correct && !n.right.isRed() + } + } + traverse(n.left) + traverse(n.right) + } + traverse(tree.root) + t.assert(correct, 'Red nodes do not have black children') +} + +function checkBlackHeightOfSubTreesAreEqual (t, tree) { + let correct = true + function traverse (n) { + if (n == null) { + return 0 + } + var sub1 = traverse(n.left) + var sub2 = traverse(n.right) + if (sub1 !== sub2) { + correct = false + } + if (n.isRed()) { + return sub1 + } else { + return sub1 + 1 + } + } + traverse(tree.root) + t.assert(correct, 'Black-height of sub-trees are equal') +} + +function checkRootNodeIsBlack (t, tree) { + t.assert(tree.root == null || tree.root.isBlack(), 'root node is black') +} + +test('RedBlack Tree', async function redBlackTree (t) { + let memory = new Y.memory(null, { // eslint-disable-line + name: 'Memory', + gcTimeout: -1 + }) + let tree = memory.os + memory.requestTransaction(function () { + 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]}) + }) + await memory.whenTransactionsFinished() + checkRootNodeIsBlack(t, tree) + checkBlackHeightOfSubTreesAreEqual(t, tree) + checkRedNodesDoNotHaveBlackChildren(t, tree) +}) + +test(`random tests (${numberOfRBTreeTests})`, async function random (t) { + let chance = new Chance(t.getSeed() * 1000000000) + let memory = new Y.memory(null, { // eslint-disable-line + name: 'Memory', + gcTimeout: -1 + }) + let tree = memory.os + let elements = [] + memory.requestTransaction(function () { + for (var i = 0; i < numberOfRBTreeTests; i++) { + if (chance.bool({likelihood: 80})) { + // 80% chance to insert an element + let obj = [chance.integer({min: 0, max: numberOfRBTreeTests})] + let nodeExists = tree.find(obj) + if (!nodeExists) { + if (elements.some(e => e[0] === obj[0])) { + t.assert(false, 'tree and elements contain different results') + } + elements.push(obj) + tree.put({id: obj}) + } + } else if (elements.length > 0) { + // ~20% chance to delete an element + var elem = chance.pickone(elements) + elements = elements.filter(function (e) { + return !Y.utils.compareIds(e, elem) + }) + tree.delete(elem) + } + } + }) + await memory.whenTransactionsFinished() + checkRootNodeIsBlack(t, tree) + checkBlackHeightOfSubTreesAreEqual(t, tree) + checkRedNodesDoNotHaveBlackChildren(t, tree) + memory.requestTransaction(function () { + let allNodesExist = true + for (let id of elements) { + let node = tree.find(id) + if (!Y.utils.compareIds(node.id, id)) { + allNodesExist = false + } + } + t.assert(allNodesExist, 'All inserted nodes exist') + }) + memory.requestTransaction(function () { + let findAllNodesWithLowerBoundSerach = true + for (let id of elements) { + let node = tree.findWithLowerBound(id) + if (!Y.utils.compareIds(node.id, id)) { + findAllNodesWithLowerBoundSerach = false + } + } + t.assert( + findAllNodesWithLowerBoundSerach, + 'Find every object with lower bound search' + ) + }) + + memory.requestTransaction(function () { + let lowerBound = chance.pickone(elements) + let expectedResults = elements.filter((e, pos) => + (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && + elements.indexOf(e) === pos + ).length + let actualResults = 0 + tree.iterate(this, lowerBound, null, function (val) { + if (val == null) { + t.assert(false, 'val is undefined!') + } + actualResults++ + }) + t.assert( + expectedResults === actualResults, + 'Iterating over a tree with lower bound yields the right amount of results' + ) + }) + + memory.requestTransaction(function () { + let expectedResults = elements.filter((e, pos) => + elements.indexOf(e) === pos + ).length + let actualResults = 0 + tree.iterate(this, null, null, function (val) { + if (val == null) { + t.assert(false, 'val is undefined!') + } + actualResults++ + }) + t.assert( + expectedResults === actualResults, + 'iterating over a tree without bounds yields the right amount of results' + ) + }) + + memory.requestTransaction(function () { + let upperBound = chance.pickone(elements) + let expectedResults = elements.filter((e, pos) => + (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && + elements.indexOf(e) === pos + ).length + let actualResults = 0 + tree.iterate(this, null, upperBound, function (val) { + if (val == null) { + t.assert(false, 'val is undefined!') + } + actualResults++ + }) + t.assert( + expectedResults === actualResults, + 'iterating over a tree with upper bound yields the right amount of results' + ) + }) + + memory.requestTransaction(function () { + let upperBound = chance.pickone(elements) + let lowerBound = chance.pickone(elements) + if (Y.utils.smaller(upperBound, lowerBound)) { + [lowerBound, upperBound] = [upperBound, lowerBound] + } + let expectedResults = elements.filter((e, pos) => + (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && + (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && + elements.indexOf(e) === pos + ).length + let actualResults = 0 + tree.iterate(this, lowerBound, upperBound, function (val) { + if (val == null) { + t.assert(false, 'val is undefined!') + } + actualResults++ + }) + t.assert( + expectedResults === actualResults, + 'iterating over a tree with upper bound yields the right amount of results' + ) + }) + + await memory.whenTransactionsFinished() +}) diff --git a/tests-lib/helper.js b/tests-lib/helper.js index 9236cfa5..5585b1a8 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -4,7 +4,7 @@ import _Y from '../../yjs/src/y.js' import yMemory from '../../y-memory/src/y-memory.js' import yArray from '../../y-array/src/y-array.js' import yText from '../../y-text/src/Text.js' -import yMap from '../../y-map/src/Map.js' +import yMap from '../../y-map/src/y-map.js' import yXml from '../../y-xml/src/y-xml.js' import yTest from './test-connector.js' @@ -17,9 +17,9 @@ Y.extend(yMemory, yArray, yText, yMap, yTest, yXml) export var database = { name: 'memory' } export var connector = { name: 'test', url: 'http://localhost:1234' } -function * getStateSet () { +function getStateSet () { var ss = {} - yield * this.ss.iterate(this, null, null, function * (n) { + this.ss.iterate(this, null, null, function (n) { var user = n.id[0] var clock = n.clock ss[user] = clock @@ -27,9 +27,9 @@ function * getStateSet () { return ss } -function * getDeleteSet () { +function getDeleteSet () { var ds = {} - yield * this.ds.iterate(this, null, null, function * (n) { + this.ds.iterate(this, null, null, function (n) { var user = n.id[0] var counter = n.id[1] var len = n.len @@ -126,9 +126,9 @@ export async function compareUsers (t, users) { let filterDeletedOps = users.every(u => u.db.gc === false) var data = await Promise.all(users.map(async (u) => { var data = {} - u.db.requestTransaction(function * () { + u.db.requestTransaction(function () { let ops = [] - yield * this.os.iterate(this, null, null, function * (op) { + this.os.iterate(this, null, null, function (op) { ops.push(Y.Struct[op.struct].encode(op)) }) @@ -142,7 +142,7 @@ export async function compareUsers (t, users) { as they might have been split up differently.. */ if (filterDeletedOps) { - let opIsDeleted = yield * this.isDeleted(op.id) + let opIsDeleted = this.isDeleted(op.id) if (!opIsDeleted) { data.os[JSON.stringify(op.id)] = op } @@ -150,8 +150,8 @@ export async function compareUsers (t, users) { data.os[JSON.stringify(op.id)] = op } } - data.ds = yield * getDeleteSet.apply(this) - data.ss = yield * getStateSet.apply(this) + data.ds = getDeleteSet.apply(this) + data.ss = getStateSet.apply(this) }) await u.db.whenTransactionsFinished() return data