refactored test suites
This commit is contained in:
parent
661232f23c
commit
ee133ef334
@ -71,12 +71,13 @@ var polyfills = [
|
|||||||
var concatOrder = [
|
var concatOrder = [
|
||||||
'y.js',
|
'y.js',
|
||||||
'Connector.js',
|
'Connector.js',
|
||||||
'OperationStore.js',
|
'Database.js',
|
||||||
|
'Transaction.js',
|
||||||
'Struct.js',
|
'Struct.js',
|
||||||
'Utils.js',
|
'Utils.js',
|
||||||
'OperationStores/RedBlackTree.js',
|
'Databases/RedBlackTree.js',
|
||||||
'OperationStores/Memory.js',
|
'Databases/Memory.js',
|
||||||
'OperationStores/IndexedDB.js',
|
'Databases/IndexedDB.js',
|
||||||
'Connectors/Test.js',
|
'Connectors/Test.js',
|
||||||
'Connectors/WebRTC.js',
|
'Connectors/WebRTC.js',
|
||||||
'Types/Array.js',
|
'Types/Array.js',
|
||||||
|
283
src/Database.js
Normal file
283
src/Database.js
Normal 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
329
src/Database.spec.js
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
|
/* global Y */
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
Y.IndexedDB = (function () { // eslint-disable-line
|
Y.IndexedDB = (function () {
|
||||||
class Transaction extends Y.AbstractTransaction { // eslint-disable-line
|
class Transaction extends Y.AbstractTransaction {
|
||||||
constructor (store) {
|
constructor (store) {
|
||||||
super(store)
|
super(store)
|
||||||
this.transaction = store.db.transaction(['OperationStore', 'StateVector'], 'readwrite')
|
this.transaction = store.db.transaction(['OperationStore', 'StateVector'], 'readwrite')
|
||||||
@ -81,7 +83,7 @@ Y.IndexedDB = (function () { // eslint-disable-line
|
|||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class OperationStore extends Y.AbstractOperationStore { // eslint-disable-line no-undef
|
class OperationStore extends Y.AbstractOperationStore {
|
||||||
constructor (y, opts) {
|
constructor (y, opts) {
|
||||||
super(y, opts)
|
super(y, opts)
|
||||||
if (opts == null) {
|
if (opts == null) {
|
@ -30,7 +30,7 @@ if (typeof window !== 'undefined' && false) {
|
|||||||
.toEqual(op)
|
.toEqual(op)
|
||||||
yield* this.removeOperation(['1', 0])
|
yield* this.removeOperation(['1', 0])
|
||||||
expect(yield* this.getOperation(['1', 0]))
|
expect(yield* this.getOperation(['1', 0]))
|
||||||
.toBeUndefined()
|
.toBeNull()
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -38,7 +38,7 @@ if (typeof window !== 'undefined' && false) {
|
|||||||
it('getOperation(op) returns undefined if op does not exist', function (done) {
|
it('getOperation(op) returns undefined if op does not exist', function (done) {
|
||||||
ob.requestTransaction(function *() {
|
ob.requestTransaction(function *() {
|
||||||
var op = yield* this.getOperation("plzDon'tBeThere")
|
var op = yield* this.getOperation("plzDon'tBeThere")
|
||||||
expect(op).toBeUndefined()
|
expect(op).toBeNull()
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -64,7 +64,6 @@ if (typeof window !== 'undefined' && false) {
|
|||||||
yield* this.setState(s1)
|
yield* this.setState(s1)
|
||||||
yield* this.setState(s2)
|
yield* this.setState(s2)
|
||||||
var sv = yield* this.getStateVector()
|
var sv = yield* this.getStateVector()
|
||||||
expect(sv).not.toBeUndefined()
|
|
||||||
expect(sv).toEqual([s1, s2])
|
expect(sv).toEqual([s1, s2])
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
@ -77,7 +76,6 @@ if (typeof window !== 'undefined' && false) {
|
|||||||
yield* this.setState(s1)
|
yield* this.setState(s1)
|
||||||
yield* this.setState(s2)
|
yield* this.setState(s2)
|
||||||
var sv = yield* this.getStateSet()
|
var sv = yield* this.getStateSet()
|
||||||
expect(sv).not.toBeUndefined()
|
|
||||||
expect(sv).toEqual({
|
expect(sv).toEqual({
|
||||||
'1': 1,
|
'1': 1,
|
||||||
'2': 3
|
'2': 3
|
@ -17,10 +17,10 @@ Y.Memory = (function () {
|
|||||||
constructor (y, opts) {
|
constructor (y, opts) {
|
||||||
super(y, opts)
|
super(y, opts)
|
||||||
this.os = new Y.utils.RBTree()
|
this.os = new Y.utils.RBTree()
|
||||||
this.ss = {}
|
this.ds = new Y.utils.RBTree()
|
||||||
|
this.ss = new Y.utils.RBTree()
|
||||||
this.waitingTransactions = []
|
this.waitingTransactions = []
|
||||||
this.transactionInProgress = false
|
this.transactionInProgress = false
|
||||||
this.ds = new DeleteStore()
|
|
||||||
}
|
}
|
||||||
logTable () {
|
logTable () {
|
||||||
var self = this
|
var self = this
|
@ -221,7 +221,8 @@ class RBTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
find (id) {
|
find (id) {
|
||||||
return this.findNode(id).val
|
var n
|
||||||
|
return (n = this.findNode(id)) ? n.val : null
|
||||||
}
|
}
|
||||||
findNode (id) {
|
findNode (id) {
|
||||||
if (id == null || id.constructor !== Array) {
|
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) {
|
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!')
|
throw new Error('v is expected to have an id property which is an Array!')
|
||||||
}
|
}
|
||||||
@ -410,7 +411,8 @@ class RBTree {
|
|||||||
p = p.right
|
p = p.right
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return null
|
p.val = node.val
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._fixInsert(node)
|
this._fixInsert(node)
|
@ -57,57 +57,17 @@ describe('RedBlack Tree', function () {
|
|||||||
})
|
})
|
||||||
this.tree = this.memory.os
|
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 () {
|
describe('debug #2', function () {
|
||||||
var tree = new Y.utils.RBTree()
|
var tree = new Y.utils.RBTree()
|
||||||
tree.add({id: [8433]})
|
tree.set({id: [8433]})
|
||||||
tree.add({id: [12844]})
|
tree.set({id: [12844]})
|
||||||
tree.add({id: [1795]})
|
tree.set({id: [1795]})
|
||||||
tree.add({id: [30302]})
|
tree.set({id: [30302]})
|
||||||
tree.add({id: [64287]})
|
tree.set({id: [64287]})
|
||||||
tree.delete([8433])
|
tree.delete([8433])
|
||||||
tree.add({id: [28996]})
|
tree.set({id: [28996]})
|
||||||
tree.delete([64287])
|
tree.delete([64287])
|
||||||
tree.add({id: [22721]})
|
tree.set({id: [22721]})
|
||||||
|
|
||||||
itRootNodeIsBlack(tree, [])
|
itRootNodeIsBlack(tree, [])
|
||||||
itBlackHeightOfSubTreesAreEqual(tree, [])
|
itBlackHeightOfSubTreesAreEqual(tree, [])
|
||||||
@ -122,12 +82,14 @@ describe('RedBlack Tree', function () {
|
|||||||
var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)]
|
var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)]
|
||||||
if (!tree.findNode(obj)) {
|
if (!tree.findNode(obj)) {
|
||||||
elements.push(obj)
|
elements.push(obj)
|
||||||
tree.add({id: obj})
|
tree.set({id: obj})
|
||||||
}
|
}
|
||||||
} else if (elements.length > 0) {
|
} else if (elements.length > 0) {
|
||||||
var elemid = Math.floor(Math.random() * elements.length)
|
var elemid = Math.floor(Math.random() * elements.length)
|
||||||
var elem = elements[elemid]
|
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)
|
tree.delete(elem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,7 +119,7 @@ describe('RedBlack Tree', function () {
|
|||||||
var actualResults = 0
|
var actualResults = 0
|
||||||
this.memory.requestTransaction(function * () {
|
this.memory.requestTransaction(function * () {
|
||||||
yield* tree.iterate(this, lowerBound, null, function * (val) {
|
yield* tree.iterate(this, lowerBound, null, function * (val) {
|
||||||
expect(val).not.toBeUndefined()
|
expect(val).toBeDefined()
|
||||||
actualResults++
|
actualResults++
|
||||||
})
|
})
|
||||||
expect(expectedResults).toEqual(actualResults)
|
expect(expectedResults).toEqual(actualResults)
|
||||||
@ -173,7 +135,7 @@ describe('RedBlack Tree', function () {
|
|||||||
var actualResults = 0
|
var actualResults = 0
|
||||||
this.memory.requestTransaction(function * () {
|
this.memory.requestTransaction(function * () {
|
||||||
yield* tree.iterate(this, lowerBound, null, function * (val) {
|
yield* tree.iterate(this, lowerBound, null, function * (val) {
|
||||||
expect(val).not.toBeUndefined()
|
expect(val).toBeDefined()
|
||||||
actualResults++
|
actualResults++
|
||||||
})
|
})
|
||||||
expect(expectedResults).toEqual(actualResults)
|
expect(expectedResults).toEqual(actualResults)
|
||||||
@ -190,7 +152,7 @@ describe('RedBlack Tree', function () {
|
|||||||
var actualResults = 0
|
var actualResults = 0
|
||||||
this.memory.requestTransaction(function * () {
|
this.memory.requestTransaction(function * () {
|
||||||
yield* tree.iterate(this, null, upperBound, function * (val) {
|
yield* tree.iterate(this, null, upperBound, function * (val) {
|
||||||
expect(val).not.toBeUndefined()
|
expect(val).toBeDefined()
|
||||||
actualResults++
|
actualResults++
|
||||||
})
|
})
|
||||||
expect(expectedResults).toEqual(actualResults)
|
expect(expectedResults).toEqual(actualResults)
|
||||||
@ -216,7 +178,7 @@ describe('RedBlack Tree', function () {
|
|||||||
var actualResults = 0
|
var actualResults = 0
|
||||||
this.memory.requestTransaction(function * () {
|
this.memory.requestTransaction(function * () {
|
||||||
yield* tree.iterate(this, lowerBound, upperBound, function * (val) {
|
yield* tree.iterate(this, lowerBound, upperBound, function * (val) {
|
||||||
expect(val).not.toBeUndefined()
|
expect(val).toBeDefined()
|
||||||
actualResults++
|
actualResults++
|
||||||
})
|
})
|
||||||
expect(expectedResults).toEqual(actualResults)
|
expect(expectedResults).toEqual(actualResults)
|
@ -176,7 +176,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
|
|||||||
var o = yield* this.getOperation([d.id[0], d.id[1] + i])
|
var o = yield* this.getOperation([d.id[0], d.id[1] + i])
|
||||||
// gc'd or deleted
|
// gc'd or deleted
|
||||||
if (d.gc) {
|
if (d.gc) {
|
||||||
expect(o).toBeUndefined()
|
expect(o).toBeNull()
|
||||||
} else {
|
} else {
|
||||||
expect(o.deleted).toBeTruthy()
|
expect(o.deleted).toBeTruthy()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
})
|
|
@ -206,14 +206,14 @@ class AbstractTransaction {
|
|||||||
// un-extend left
|
// un-extend left
|
||||||
var newlen = n.val.len - (id[1] - n.val.id[1])
|
var newlen = n.val.len - (id[1] - n.val.id[1])
|
||||||
n.val.len -= newlen
|
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
|
// get prev&next before adding a new operation
|
||||||
var prev = n.prev()
|
var prev = n.prev()
|
||||||
var next = n.next()
|
var next = n.next()
|
||||||
if (id[1] < n.val.id[1] + n.val.len - 1) {
|
if (id[1] < n.val.id[1] + n.val.len - 1) {
|
||||||
// un-extend right
|
// 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
|
n.val.len = 1
|
||||||
}
|
}
|
||||||
// set gc'd
|
// set gc'd
|
||||||
@ -256,11 +256,11 @@ class AbstractTransaction {
|
|||||||
n.val.len++
|
n.val.len++
|
||||||
} else {
|
} else {
|
||||||
// cannot extend left
|
// 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 {
|
} else {
|
||||||
// cannot extend left
|
// 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?
|
// can extend right?
|
||||||
var next = n.next()
|
var next = n.next()
|
||||||
@ -488,7 +488,7 @@ class AbstractTransaction {
|
|||||||
return op
|
return op
|
||||||
}
|
}
|
||||||
* addOperation (op) {
|
* addOperation (op) {
|
||||||
var n = yield this.os.add(op)
|
var n = yield this.os.set(op)
|
||||||
return function () {
|
return function () {
|
||||||
if (n != null) {
|
if (n != null) {
|
||||||
n = n.next()
|
n = n.next()
|
||||||
@ -505,10 +505,14 @@ class AbstractTransaction {
|
|||||||
yield this.os.delete(id)
|
yield this.os.delete(id)
|
||||||
}
|
}
|
||||||
* setState (state) {
|
* setState (state) {
|
||||||
this.ss[state.user] = state.clock
|
this.ss.set({
|
||||||
|
id: [state.user],
|
||||||
|
clock: state.clock
|
||||||
|
})
|
||||||
}
|
}
|
||||||
* getState (user) {
|
* getState (user) {
|
||||||
var clock = this.ss[user]
|
var n
|
||||||
|
var clock = (n = this.ss.find([user])) == null ? null : n.clock
|
||||||
if (clock == null) {
|
if (clock == null) {
|
||||||
clock = 0
|
clock = 0
|
||||||
}
|
}
|
||||||
@ -519,17 +523,20 @@ class AbstractTransaction {
|
|||||||
}
|
}
|
||||||
* getStateVector () {
|
* getStateVector () {
|
||||||
var stateVector = []
|
var stateVector = []
|
||||||
for (var user in this.ss) {
|
yield* this.ss.iterate(this, null, null, function * (n) {
|
||||||
var clock = this.ss[user]
|
|
||||||
stateVector.push({
|
stateVector.push({
|
||||||
user: user,
|
user: n.id[0],
|
||||||
clock: clock
|
clock: n.clock
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
return stateVector
|
return stateVector
|
||||||
}
|
}
|
||||||
* getStateSet () {
|
* 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) {
|
* getOperations (startSS) {
|
||||||
// TODO: use bounds here!
|
// TODO: use bounds here!
|
||||||
@ -619,284 +626,3 @@ class AbstractTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Y.AbstractTransaction = 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
|
|
Loading…
x
Reference in New Issue
Block a user