refactored test suites

This commit is contained in:
Kevin Jahns 2015-10-14 18:08:39 +02:00
parent 661232f23c
commit ee133ef334
11 changed files with 667 additions and 512 deletions

View File

@ -71,12 +71,13 @@ var polyfills = [
var concatOrder = [
'y.js',
'Connector.js',
'OperationStore.js',
'Database.js',
'Transaction.js',
'Struct.js',
'Utils.js',
'OperationStores/RedBlackTree.js',
'OperationStores/Memory.js',
'OperationStores/IndexedDB.js',
'Databases/RedBlackTree.js',
'Databases/Memory.js',
'Databases/IndexedDB.js',
'Connectors/Test.js',
'Connectors/WebRTC.js',
'Types/Array.js',

283
src/Database.js Normal file
View File

@ -0,0 +1,283 @@
/* global Y */
'use strict'
/*
Partial definition of an OperationStore.
TODO: name it Database, operation store only holds operations.
A database definition must alse define the following methods:
* logTable() (optional)
- show relevant information information in a table
* requestTransaction(makeGen)
- request a transaction
* destroy()
- destroy the database
*/
class AbstractOperationStore {
constructor (y, opts) {
this.y = y
// E.g. this.listenersById[id] : Array<Listener>
this.listenersById = {}
// Execute the next time a transaction is requested
this.listenersByIdExecuteNow = []
// A transaction is requested
this.listenersByIdRequestPending = false
/* To make things more clear, the following naming conventions:
* ls : we put this.listenersById on ls
* l : Array<Listener>
* id : Id (can't use as property name)
* sid : String (converted from id via JSON.stringify
so we can use it as a property name)
Always remember to first overwrite
a property before you iterate over it!
*/
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
// wont be kept in memory.
this.initializedTypes = {}
this.whenUserIdSetListener = null
this.gc1 = [] // first stage
this.gc2 = [] // second stage -> after that, remove the op
this.gcTimeout = opts.gcTimeout || 5000
var os = this
function garbageCollect () {
return new Promise((resolve) => {
os.requestTransaction(function * () {
if (os.y.connector.isSynced) {
for (var i in os.gc2) {
var oid = os.gc2[i]
yield* this.garbageCollectOperation(oid)
}
os.gc2 = os.gc1
os.gc1 = []
}
if (os.gcTimeout > 0) {
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
}
resolve()
})
})
}
this.garbageCollect = garbageCollect
if (this.gcTimeout > 0) {
garbageCollect()
}
}
stopGarbageCollector () {
var self = this
return new Promise(function (resolve) {
self.requestTransaction(function * () {
var ungc = self.gc1.concat(self.gc2)
self.gc1 = []
self.gc2 = []
for (var i in ungc) {
var op = yield* this.getOperation(ungc[i])
delete op.gc
yield* this.setOperation(op)
}
resolve()
})
})
}
* garbageCollectAfterSync () {
this.requestTransaction(function * () {
yield* this.os.iterate(this, null, null, function * (op) {
if (op.deleted && op.left != null) {
var left = yield this.os.find(op.left)
this.store.addToGarbageCollector(op, left)
}
})
})
}
/*
Try to add to GC.
TODO: rename this function
Rulez:
* Only gc if this user is online
* The most left element in a list must not be gc'd.
=> There is at least one element in the list
returns true iff op was added to GC
*/
addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true &&
this.y.connector.isSynced &&
left != null &&
left.deleted === true
) {
op.gc = true
this.gc1.push(op.id)
return true
} else {
return false
}
}
removeFromGarbageCollector (op) {
function filter (o) {
return !Y.utils.compareIds(o, op.id)
}
this.gc1 = this.gc1.filter(filter)
this.gc2 = this.gc2.filter(filter)
delete op.gc
}
destroy () {
clearInterval(this.gcInterval)
this.gcInterval = null
}
setUserId (userId) {
this.userId = userId
this.opClock = 0
if (this.whenUserIdSetListener != null) {
this.whenUserIdSetListener()
this.whenUserIdSetListener = null
}
}
whenUserIdSet (f) {
if (this.userId != null) {
f()
} else {
this.whenUserIdSetListener = f
}
}
getNextOpId () {
if (this.userId == null) {
throw new Error('OperationStore not yet initialized!')
}
return [this.userId, this.opClock++]
}
/*
Apply a list of operations.
* get a transaction
* check whether all Struct.*.requiredOps are in the OS
* check if it is an expected op (otherwise wait for it)
* check if was deleted, apply a delete operation after op was applied
*/
apply (ops) {
for (var key in ops) {
var o = ops[key]
var required = Y.Struct[o.struct].requiredOps(o)
this.whenOperationsExist(required, o)
}
}
/*
op is executed as soon as every operation requested is available.
Note that Transaction can (and should) buffer requests.
*/
whenOperationsExist (ids, op) {
if (ids.length > 0) {
let listener = {
op: op,
missing: ids.length
}
for (let key in ids) {
let id = ids[key]
let sid = JSON.stringify(id)
let l = this.listenersById[sid]
if (l == null) {
l = []
this.listenersById[sid] = l
}
l.push(listener)
}
} else {
this.listenersByIdExecuteNow.push({
op: op
})
}
if (this.listenersByIdRequestPending) {
return
}
this.listenersByIdRequestPending = true
var store = this
this.requestTransaction(function * () {
var exeNow = store.listenersByIdExecuteNow
store.listenersByIdExecuteNow = []
var ls = store.listenersById
store.listenersById = {}
store.listenersByIdRequestPending = false
for (let key in exeNow) {
let o = exeNow[key].op
yield* store.tryExecute.call(this, o)
}
for (var sid in ls) {
var l = ls[sid]
var id = JSON.parse(sid)
if ((yield* this.getOperation(id)) == null) {
store.listenersById[sid] = l
} else {
for (let key in l) {
let listener = l[key]
let o = listener.op
if (--listener.missing === 0) {
yield* store.tryExecute.call(this, o)
}
}
}
}
})
}
/*
Actually execute an operation, when all expected operations are available.
*/
* tryExecute (op) {
if (op.struct === 'Delete') {
yield* Y.Struct.Delete.execute.call(this, op)
} else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) {
yield* Y.Struct[op.struct].execute.call(this, op)
var next = yield* this.addOperation(op)
yield* this.store.operationAdded(this, op, next)
// Delete if DS says this is actually deleted
if (yield* this.isDeleted(op.id)) {
yield* Y.Struct['Delete'].execute.call(this, {struct: 'Delete', target: op.id})
}
}
}
// called by a transaction when an operation is added
* operationAdded (transaction, op, next) {
// increase SS
var o = op
var state = yield* transaction.getState(op.id[0])
while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) {
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
state.clock++
yield* transaction.checkDeleteStoreForState(state)
o = next()
}
yield* transaction.setState(state)
// notify whenOperation listeners (by id)
var sid = JSON.stringify(op.id)
var l = this.listenersById[sid]
delete this.listenersById[sid]
if (l != null) {
for (var key in l) {
var listener = l[key]
if (--listener.missing === 0) {
this.whenOperationsExist([], listener.op)
}
}
}
// notify parent, if it has been initialized as a custom type
var t = this.initializedTypes[JSON.stringify(op.parent)]
if (t != null && !op.deleted) {
yield* t._changed(transaction, Y.utils.copyObject(op))
}
}
}
Y.AbstractOperationStore = AbstractOperationStore

329
src/Database.spec.js Normal file
View File

@ -0,0 +1,329 @@
/* global Y, async */
/* eslint-env browser,jasmine,console */
var databases = ['Memory']
for (var database of databases) {
describe(`Database (${database})`, function () {
var store
describe('DeleteStore', function () {
describe('Basic', function () {
beforeEach(function () {
store = new Y[database](null, {
gcTimeout: -1
})
})
it('Deleted operation is deleted', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['u1', 10])
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])
yield* this.markDeleted(['u1', 11])
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])
yield* this.markDeleted(['0', 4])
yield* this.markDeleted(['0', 2])
expect(yield* this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
done()
})
}))
it('Debug #1', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['166', 0])
yield* this.markDeleted(['166', 2])
yield* this.markDeleted(['166', 0])
yield* this.markDeleted(['166', 2])
yield* this.markGarbageCollected(['166', 2])
yield* this.markDeleted(['166', 1])
yield* this.markDeleted(['166', 3])
yield* this.markGarbageCollected(['166', 3])
yield* this.markDeleted(['166', 0])
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])
yield* this.markDeleted(['291', 2])
yield* this.markDeleted(['291', 2])
yield* this.markGarbageCollected(['293', 0])
yield* this.markDeleted(['293', 1])
yield* this.markGarbageCollected(['291', 2])
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])
yield* this.markDeleted(['581', 1])
yield* this.markDeleted(['580', 0])
yield* this.markDeleted(['580', 0])
yield* this.markGarbageCollected(['581', 0])
yield* this.markDeleted(['581', 2])
yield* this.markDeleted(['580', 1])
yield* this.markDeleted(['580', 2])
yield* this.markDeleted(['580', 1])
yield* this.markDeleted(['580', 2])
yield* this.markGarbageCollected(['581', 2])
yield* this.markGarbageCollected(['581', 1])
yield* this.markGarbageCollected(['580', 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])
yield* this.markDeleted(['543', 2])
yield* this.markDeleted(['544', 0])
yield* this.markDeleted(['543', 2])
yield* this.markGarbageCollected(['544', 0])
yield* this.markDeleted(['545', 1])
yield* this.markDeleted(['543', 4])
yield* this.markDeleted(['543', 3])
yield* this.markDeleted(['544', 1])
yield* this.markDeleted(['544', 2])
yield* this.markDeleted(['544', 1])
yield* this.markDeleted(['544', 2])
yield* this.markGarbageCollected(['543', 2])
yield* this.markGarbageCollected(['543', 4])
yield* this.markGarbageCollected(['544', 2])
yield* this.markGarbageCollected(['543', 3])
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])
yield* this.markDeleted(['11', 2])
yield* this.markDeleted(['11', 4])
yield* this.markDeleted(['11', 1])
yield* this.markDeleted(['9', 4])
yield* this.markDeleted(['10', 0])
yield* this.markGarbageCollected(['11', 2])
yield* this.markDeleted(['11', 2])
yield* this.markGarbageCollected(['11', 3])
yield* this.markDeleted(['11', 3])
yield* this.markDeleted(['11', 3])
yield* this.markDeleted(['9', 4])
yield* this.markDeleted(['10', 0])
yield* this.markGarbageCollected(['11', 1])
yield* this.markDeleted(['11', 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
})
})
it('debug #1', function (done) {
store.requestTransaction(function * () {
yield this.os.set({id: [2]})
yield this.os.set({id: [0]})
yield this.os.delete([2])
yield this.os.set({id: [1]})
expect(yield this.os.find([0])).not.toBeNull()
expect(yield this.os.find([1])).not.toBeNull()
expect(yield this.os.find([2])).toBeNull()
done()
})
})
it('can add&retrieve 5 elements', function (done) {
store.requestTransaction(function * () {
yield this.os.set({val: 'four', id: [4]})
yield this.os.set({val: 'one', id: [1]})
yield this.os.set({val: 'three', id: [3]})
yield this.os.set({val: 'two', id: [2]})
yield this.os.set({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.set({val: 'four', id: [4]})
yield this.os.set({val: 'one', id: [1]})
yield this.os.set({val: 'three', id: [3]})
yield this.os.set({val: 'two', id: [2]})
yield this.os.set({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
})
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.findNode(obj))) {
elements.push(obj)
yield this.os.set({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()
})
})
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) {
expect((yield this.os.findNodeWithLowerBound(id)).val.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()
})
})
})
})
})
}

View File

@ -1,7 +1,9 @@
/* global Y */
'use strict'
Y.IndexedDB = (function () { // eslint-disable-line
class Transaction extends Y.AbstractTransaction { // eslint-disable-line
Y.IndexedDB = (function () {
class Transaction extends Y.AbstractTransaction {
constructor (store) {
super(store)
this.transaction = store.db.transaction(['OperationStore', 'StateVector'], 'readwrite')
@ -81,7 +83,7 @@ Y.IndexedDB = (function () { // eslint-disable-line
return ops
}
}
class OperationStore extends Y.AbstractOperationStore { // eslint-disable-line no-undef
class OperationStore extends Y.AbstractOperationStore {
constructor (y, opts) {
super(y, opts)
if (opts == null) {

View File

@ -30,7 +30,7 @@ if (typeof window !== 'undefined' && false) {
.toEqual(op)
yield* this.removeOperation(['1', 0])
expect(yield* this.getOperation(['1', 0]))
.toBeUndefined()
.toBeNull()
done()
})
})
@ -38,7 +38,7 @@ if (typeof window !== 'undefined' && false) {
it('getOperation(op) returns undefined if op does not exist', function (done) {
ob.requestTransaction(function *() {
var op = yield* this.getOperation("plzDon'tBeThere")
expect(op).toBeUndefined()
expect(op).toBeNull()
done()
})
})
@ -64,7 +64,6 @@ if (typeof window !== 'undefined' && false) {
yield* this.setState(s1)
yield* this.setState(s2)
var sv = yield* this.getStateVector()
expect(sv).not.toBeUndefined()
expect(sv).toEqual([s1, s2])
done()
})
@ -77,7 +76,6 @@ if (typeof window !== 'undefined' && false) {
yield* this.setState(s1)
yield* this.setState(s2)
var sv = yield* this.getStateSet()
expect(sv).not.toBeUndefined()
expect(sv).toEqual({
'1': 1,
'2': 3

View File

@ -17,10 +17,10 @@ Y.Memory = (function () {
constructor (y, opts) {
super(y, opts)
this.os = new Y.utils.RBTree()
this.ss = {}
this.ds = new Y.utils.RBTree()
this.ss = new Y.utils.RBTree()
this.waitingTransactions = []
this.transactionInProgress = false
this.ds = new DeleteStore()
}
logTable () {
var self = this

View File

@ -221,7 +221,8 @@ class RBTree {
}
}
find (id) {
return this.findNode(id).val
var n
return (n = this.findNode(id)) ? n.val : null
}
findNode (id) {
if (id == null || id.constructor !== Array) {
@ -387,7 +388,7 @@ class RBTree {
}
}
}
add (v) {
set (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!')
}
@ -410,7 +411,8 @@ class RBTree {
p = p.right
}
} else {
return null
p.val = node.val
return p
}
}
this._fixInsert(node)

View File

@ -57,57 +57,17 @@ describe('RedBlack Tree', function () {
})
this.tree = this.memory.os
})
it('can add&retrieve 5 elements', function () {
this.tree.add({val: 'four', id: [4]})
this.tree.add({val: 'one', id: [1]})
this.tree.add({val: 'three', id: [3]})
this.tree.add({val: 'two', id: [2]})
this.tree.add({val: 'five', id: [5]})
expect(this.tree.find([1]).val).toEqual('one')
expect(this.tree.find([2]).val).toEqual('two')
expect(this.tree.find([3]).val).toEqual('three')
expect(this.tree.find([4]).val).toEqual('four')
expect(this.tree.find([5]).val).toEqual('five')
})
it('5 elements do not exist anymore after deleting them', function () {
this.tree.add({val: 'four', id: [4]})
this.tree.add({val: 'one', id: [1]})
this.tree.add({val: 'three', id: [3]})
this.tree.add({val: 'two', id: [2]})
this.tree.add({val: 'five', id: [5]})
this.tree.delete([4])
expect(this.tree.find([4])).not.toBeTruthy()
this.tree.delete([3])
expect(this.tree.find([3])).not.toBeTruthy()
this.tree.delete([2])
expect(this.tree.find([2])).not.toBeTruthy()
this.tree.delete([1])
expect(this.tree.find([1])).not.toBeTruthy()
this.tree.delete([5])
expect(this.tree.find([5])).not.toBeTruthy()
})
it('debug #1', function () {
this.tree.add({id: [2]})
this.tree.add({id: [0]})
this.tree.delete([2])
this.tree.add({id: [1]})
expect(this.tree.find([0])).not.toBeUndefined()
expect(this.tree.find([1])).not.toBeUndefined()
expect(this.tree.find([2])).toBeUndefined()
})
describe('debug #2', function () {
var tree = new Y.utils.RBTree()
tree.add({id: [8433]})
tree.add({id: [12844]})
tree.add({id: [1795]})
tree.add({id: [30302]})
tree.add({id: [64287]})
tree.set({id: [8433]})
tree.set({id: [12844]})
tree.set({id: [1795]})
tree.set({id: [30302]})
tree.set({id: [64287]})
tree.delete([8433])
tree.add({id: [28996]})
tree.set({id: [28996]})
tree.delete([64287])
tree.add({id: [22721]})
tree.set({id: [22721]})
itRootNodeIsBlack(tree, [])
itBlackHeightOfSubTreesAreEqual(tree, [])
@ -122,12 +82,14 @@ describe('RedBlack Tree', function () {
var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)]
if (!tree.findNode(obj)) {
elements.push(obj)
tree.add({id: obj})
tree.set({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); }); // eslint-disable-line
elements = elements.filter(function (e) {
return !Y.utils.compareIds(e, elem)
})
tree.delete(elem)
}
}
@ -157,7 +119,7 @@ describe('RedBlack Tree', function () {
var actualResults = 0
this.memory.requestTransaction(function * () {
yield* tree.iterate(this, lowerBound, null, function * (val) {
expect(val).not.toBeUndefined()
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
@ -173,7 +135,7 @@ describe('RedBlack Tree', function () {
var actualResults = 0
this.memory.requestTransaction(function * () {
yield* tree.iterate(this, lowerBound, null, function * (val) {
expect(val).not.toBeUndefined()
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
@ -190,7 +152,7 @@ describe('RedBlack Tree', function () {
var actualResults = 0
this.memory.requestTransaction(function * () {
yield* tree.iterate(this, null, upperBound, function * (val) {
expect(val).not.toBeUndefined()
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
@ -216,7 +178,7 @@ describe('RedBlack Tree', function () {
var actualResults = 0
this.memory.requestTransaction(function * () {
yield* tree.iterate(this, lowerBound, upperBound, function * (val) {
expect(val).not.toBeUndefined()
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)

View File

@ -176,7 +176,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
var o = yield* this.getOperation([d.id[0], d.id[1] + i])
// gc'd or deleted
if (d.gc) {
expect(o).toBeUndefined()
expect(o).toBeNull()
} else {
expect(o.deleted).toBeTruthy()
}

View File

@ -1,148 +0,0 @@
/* global Y, async */
/* eslint-env browser,jasmine,console */
describe('Memory', function () {
describe('DeleteStore', function () {
var store
beforeEach(function () {
store = new Y.Memory(null, {
name: 'Memory',
gcTimeout: -1
})
})
it('Deleted operation is deleted', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['u1', 10])
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])
yield* this.markDeleted(['u1', 11])
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])
yield* this.markDeleted(['0', 4])
yield* this.markDeleted(['0', 2])
expect(yield* this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
done()
})
}))
it('Debug #1', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['166', 0])
yield* this.markDeleted(['166', 2])
yield* this.markDeleted(['166', 0])
yield* this.markDeleted(['166', 2])
yield* this.markGarbageCollected(['166', 2])
yield* this.markDeleted(['166', 1])
yield* this.markDeleted(['166', 3])
yield* this.markGarbageCollected(['166', 3])
yield* this.markDeleted(['166', 0])
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])
yield* this.markDeleted(['291', 2])
yield* this.markDeleted(['291', 2])
yield* this.markGarbageCollected(['293', 0])
yield* this.markDeleted(['293', 1])
yield* this.markGarbageCollected(['291', 2])
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])
yield* this.markDeleted(['581', 1])
yield* this.markDeleted(['580', 0])
yield* this.markDeleted(['580', 0])
yield* this.markGarbageCollected(['581', 0])
yield* this.markDeleted(['581', 2])
yield* this.markDeleted(['580', 1])
yield* this.markDeleted(['580', 2])
yield* this.markDeleted(['580', 1])
yield* this.markDeleted(['580', 2])
yield* this.markGarbageCollected(['581', 2])
yield* this.markGarbageCollected(['581', 1])
yield* this.markGarbageCollected(['580', 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])
yield* this.markDeleted(['543', 2])
yield* this.markDeleted(['544', 0])
yield* this.markDeleted(['543', 2])
yield* this.markGarbageCollected(['544', 0])
yield* this.markDeleted(['545', 1])
yield* this.markDeleted(['543', 4])
yield* this.markDeleted(['543', 3])
yield* this.markDeleted(['544', 1])
yield* this.markDeleted(['544', 2])
yield* this.markDeleted(['544', 1])
yield* this.markDeleted(['544', 2])
yield* this.markGarbageCollected(['543', 2])
yield* this.markGarbageCollected(['543', 4])
yield* this.markGarbageCollected(['544', 2])
yield* this.markGarbageCollected(['543', 3])
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])
yield* this.markDeleted(['11', 2])
yield* this.markDeleted(['11', 4])
yield* this.markDeleted(['11', 1])
yield* this.markDeleted(['9', 4])
yield* this.markDeleted(['10', 0])
yield* this.markGarbageCollected(['11', 2])
yield* this.markDeleted(['11', 2])
yield* this.markGarbageCollected(['11', 3])
yield* this.markDeleted(['11', 3])
yield* this.markDeleted(['11', 3])
yield* this.markDeleted(['9', 4])
yield* this.markDeleted(['10', 0])
yield* this.markGarbageCollected(['11', 1])
yield* this.markDeleted(['11', 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()
})
}))
})
})

View File

@ -206,14 +206,14 @@ class AbstractTransaction {
// un-extend left
var newlen = n.val.len - (id[1] - n.val.id[1])
n.val.len -= newlen
n = yield this.ds.add({id: id, len: newlen, gc: false})
n = yield this.ds.set({id: id, len: newlen, gc: false})
}
// get prev&next before adding a new operation
var prev = n.prev()
var next = n.next()
if (id[1] < n.val.id[1] + n.val.len - 1) {
// un-extend right
yield this.ds.add({id: [id[0], id[1] + 1], len: n.val.len - 1, gc: false})
yield this.ds.set({id: [id[0], id[1] + 1], len: n.val.len - 1, gc: false})
n.val.len = 1
}
// set gc'd
@ -256,11 +256,11 @@ class AbstractTransaction {
n.val.len++
} else {
// cannot extend left
n = yield this.ds.add({id: id, len: 1, gc: false})
n = yield this.ds.set({id: id, len: 1, gc: false})
}
} else {
// cannot extend left
n = yield this.ds.add({id: id, len: 1, gc: false})
n = yield this.ds.set({id: id, len: 1, gc: false})
}
// can extend right?
var next = n.next()
@ -488,7 +488,7 @@ class AbstractTransaction {
return op
}
* addOperation (op) {
var n = yield this.os.add(op)
var n = yield this.os.set(op)
return function () {
if (n != null) {
n = n.next()
@ -505,10 +505,14 @@ class AbstractTransaction {
yield this.os.delete(id)
}
* setState (state) {
this.ss[state.user] = state.clock
this.ss.set({
id: [state.user],
clock: state.clock
})
}
* getState (user) {
var clock = this.ss[user]
var n
var clock = (n = this.ss.find([user])) == null ? null : n.clock
if (clock == null) {
clock = 0
}
@ -519,17 +523,20 @@ class AbstractTransaction {
}
* getStateVector () {
var stateVector = []
for (var user in this.ss) {
var clock = this.ss[user]
yield* this.ss.iterate(this, null, null, function * (n) {
stateVector.push({
user: user,
clock: clock
user: n.id[0],
clock: n.clock
})
}
})
return stateVector
}
* getStateSet () {
return Y.utils.copyObject(this.ss)
var ss = {}
yield* this.ss.iterate(this, null, null, function * (n) {
ss[n.id[0]] = n.clock
})
return ss
}
* getOperations (startSS) {
// TODO: use bounds here!
@ -619,284 +626,3 @@ class AbstractTransaction {
}
}
Y.AbstractTransaction = AbstractTransaction
/*
Partial definition of an OperationStore.
TODO: name it Database, operation store only holds operations.
A database definition must alse define the following methods:
* logTable() (optional)
- show relevant information information in a table
* requestTransaction(makeGen)
- request a transaction
* destroy()
- destroy the database
*/
class AbstractOperationStore {
constructor (y, opts) {
this.y = y
// E.g. this.listenersById[id] : Array<Listener>
this.listenersById = {}
// Execute the next time a transaction is requested
this.listenersByIdExecuteNow = []
// A transaction is requested
this.listenersByIdRequestPending = false
/* To make things more clear, the following naming conventions:
* ls : we put this.listenersById on ls
* l : Array<Listener>
* id : Id (can't use as property name)
* sid : String (converted from id via JSON.stringify
so we can use it as a property name)
Always remember to first overwrite
a property before you iterate over it!
*/
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
// wont be kept in memory.
this.initializedTypes = {}
this.whenUserIdSetListener = null
this.gc1 = [] // first stage
this.gc2 = [] // second stage -> after that, remove the op
this.gcTimeout = opts.gcTimeout || 5000
var os = this
function garbageCollect () {
return new Promise((resolve) => {
os.requestTransaction(function * () {
if (os.y.connector.isSynced) {
for (var i in os.gc2) {
var oid = os.gc2[i]
yield* this.garbageCollectOperation(oid)
}
os.gc2 = os.gc1
os.gc1 = []
}
if (os.gcTimeout > 0) {
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
}
resolve()
})
})
}
this.garbageCollect = garbageCollect
if (this.gcTimeout > 0) {
garbageCollect()
}
}
stopGarbageCollector () {
var self = this
return new Promise(function (resolve) {
self.requestTransaction(function * () {
var ungc = self.gc1.concat(self.gc2)
self.gc1 = []
self.gc2 = []
for (var i in ungc) {
var op = yield* this.getOperation(ungc[i])
delete op.gc
yield* this.setOperation(op)
}
resolve()
})
})
}
* garbageCollectAfterSync () {
this.requestTransaction(function * () {
yield* this.os.iterate(this, null, null, function * (op) {
if (op.deleted && op.left != null) {
var left = yield this.os.find(op.left)
this.store.addToGarbageCollector(op, left)
}
})
})
}
/*
Try to add to GC.
TODO: rename this function
Rulez:
* Only gc if this user is online
* The most left element in a list must not be gc'd.
=> There is at least one element in the list
returns true iff op was added to GC
*/
addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true &&
this.y.connector.isSynced &&
left != null &&
left.deleted === true
) {
op.gc = true
this.gc1.push(op.id)
return true
} else {
return false
}
}
removeFromGarbageCollector (op) {
function filter (o) {
return !Y.utils.compareIds(o, op.id)
}
this.gc1 = this.gc1.filter(filter)
this.gc2 = this.gc2.filter(filter)
delete op.gc
}
destroy () {
clearInterval(this.gcInterval)
this.gcInterval = null
}
setUserId (userId) {
this.userId = userId
this.opClock = 0
if (this.whenUserIdSetListener != null) {
this.whenUserIdSetListener()
this.whenUserIdSetListener = null
}
}
whenUserIdSet (f) {
if (this.userId != null) {
f()
} else {
this.whenUserIdSetListener = f
}
}
getNextOpId () {
if (this.userId == null) {
throw new Error('OperationStore not yet initialized!')
}
return [this.userId, this.opClock++]
}
/*
Apply a list of operations.
* get a transaction
* check whether all Struct.*.requiredOps are in the OS
* check if it is an expected op (otherwise wait for it)
* check if was deleted, apply a delete operation after op was applied
*/
apply (ops) {
for (var key in ops) {
var o = ops[key]
var required = Y.Struct[o.struct].requiredOps(o)
this.whenOperationsExist(required, o)
}
}
/*
op is executed as soon as every operation requested is available.
Note that Transaction can (and should) buffer requests.
*/
whenOperationsExist (ids, op) {
if (ids.length > 0) {
let listener = {
op: op,
missing: ids.length
}
for (let key in ids) {
let id = ids[key]
let sid = JSON.stringify(id)
let l = this.listenersById[sid]
if (l == null) {
l = []
this.listenersById[sid] = l
}
l.push(listener)
}
} else {
this.listenersByIdExecuteNow.push({
op: op
})
}
if (this.listenersByIdRequestPending) {
return
}
this.listenersByIdRequestPending = true
var store = this
this.requestTransaction(function * () {
var exeNow = store.listenersByIdExecuteNow
store.listenersByIdExecuteNow = []
var ls = store.listenersById
store.listenersById = {}
store.listenersByIdRequestPending = false
for (let key in exeNow) {
let o = exeNow[key].op
yield* store.tryExecute.call(this, o)
}
for (var sid in ls) {
var l = ls[sid]
var id = JSON.parse(sid)
if ((yield* this.getOperation(id)) == null) {
store.listenersById[sid] = l
} else {
for (let key in l) {
let listener = l[key]
let o = listener.op
if (--listener.missing === 0) {
yield* store.tryExecute.call(this, o)
}
}
}
}
})
}
/*
Actually execute an operation, when all expected operations are available.
*/
* tryExecute (op) {
if (op.struct === 'Delete') {
yield* Y.Struct.Delete.execute.call(this, op)
} else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) {
yield* Y.Struct[op.struct].execute.call(this, op)
var next = yield* this.addOperation(op)
yield* this.store.operationAdded(this, op, next)
// Delete if DS says this is actually deleted
if (yield* this.isDeleted(op.id)) {
yield* Y.Struct['Delete'].execute.call(this, {struct: 'Delete', target: op.id})
}
}
}
// called by a transaction when an operation is added
* operationAdded (transaction, op, next) {
// increase SS
var o = op
var state = yield* transaction.getState(op.id[0])
while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) {
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
state.clock++
yield* transaction.checkDeleteStoreForState(state)
o = next()
}
yield* transaction.setState(state)
// notify whenOperation listeners (by id)
var sid = JSON.stringify(op.id)
var l = this.listenersById[sid]
delete this.listenersById[sid]
if (l != null) {
for (var key in l) {
var listener = l[key]
if (--listener.missing === 0) {
this.whenOperationsExist([], listener.op)
}
}
}
// notify parent, if it has been initialized as a custom type
var t = this.initializedTypes[JSON.stringify(op.parent)]
if (t != null && !op.deleted) {
yield* t._changed(transaction, Y.utils.copyObject(op))
}
}
}
Y.AbstractOperationStore = AbstractOperationStore