removed generators

This commit is contained in:
Kevin Jahns 2017-09-06 20:10:38 +02:00
parent 6b5c02f1ce
commit ccf6d86c98
17 changed files with 1118 additions and 769 deletions

View File

@ -1,7 +1,7 @@
import Y from '../src/y.js'
import yArray from '../../y-array/src/y-array.js'
import yMap from '../../y-map/src/Map.js'
import yMap from '../../y-map/src/y-map.js'
import yText from '../../y-text/src/Text.js'
import yXml from '../../y-xml/src/y-xml.js'
import yMemory from '../../y-memory/src/y-memory.js'

View File

@ -1,4 +1,3 @@
import inject from 'rollup-plugin-inject'
import babel from 'rollup-plugin-babel'
import uglify from 'rollup-plugin-uglify'
import nodeResolve from 'rollup-plugin-node-resolve'
@ -16,12 +15,7 @@ export default {
browser: true
}),
commonjs(),
babel({
runtimeHelpers: true
}),
inject({
regeneratorRuntime: 'regenerator-runtime'
}),
babel(),
uglify({
output: {
comments: function (node, comment) {

View File

@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry'
export default {
entry: 'test/y-xml.tests.js',
entry: 'test/*.js',
moduleName: 'y-tests',
format: 'umd',
plugins: [

View File

@ -105,6 +105,7 @@ export default function extendConnector (Y/* :any */) {
}
}
}
userJoined (user, role, auth) {
if (role == null) {
throw new Error('You must specify the role of the joined user!')
@ -133,6 +134,7 @@ export default function extendConnector (Y/* :any */) {
}
this._syncWithUser(user)
}
// Execute a function _when_ we are connected.
// If not connected, wait until connected
whenSynced (f) {
@ -142,18 +144,20 @@ export default function extendConnector (Y/* :any */) {
this.whenSyncedListeners.push(f)
}
}
_syncWithUser (userid) {
if (this.role === 'slave') {
return // "The current sync has not finished or this is controlled by a master!"
}
sendSyncStep1(this, userid)
}
_fireIsSyncedListeners () {
this.y.db.whenTransactionsFinished().then(() => {
if (!this.isSynced) {
this.isSynced = true
// It is safer to remove this!
// TODO: remove: yield * this.garbageCollectAfterSync()
// TODO: remove: this.garbageCollectAfterSync()
// call whensynced listeners
for (var f of this.whenSyncedListeners) {
f()
@ -162,6 +166,7 @@ export default function extendConnector (Y/* :any */) {
}
})
}
send (uid, buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
@ -169,6 +174,7 @@ export default function extendConnector (Y/* :any */) {
this.log('%s: Send \'%y\' to %s', this.userId, buffer, uid)
this.logMessage('Message: %Y', buffer)
}
broadcast (buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
@ -176,6 +182,7 @@ export default function extendConnector (Y/* :any */) {
this.log('%s: Broadcast \'%y\'', this.userId, buffer)
this.logMessage('Message: %Y', buffer)
}
/*
Buffer operations, and broadcast them when ready.
*/
@ -207,6 +214,7 @@ export default function extendConnector (Y/* :any */) {
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
}
}
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
@ -224,14 +232,11 @@ export default function extendConnector (Y/* :any */) {
encoder.writeVarString(roomname)
let messageType = decoder.readVarString()
let senderConn = this.connections.get(sender)
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
this.logMessage('Message: %Y', buffer)
if (senderConn == null && !skipAuth) {
throw new Error('Received message from unknown peer!')
}
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
let auth = decoder.readVarUint()
if (senderConn.auth == null) {
@ -284,97 +289,6 @@ export default function extendConnector (Y/* :any */) {
this._fireIsSyncedListeners()
}
}
/*
Currently, the HB encodes operations as JSON. For the moment I want to keep it
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
too much overhead. Y is very likely to get changed a lot in the future
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
we encode the JSON as XML.
When the HB support encoding as XML, the format should look pretty much like this.
does not support primitive values as array elements
expects an ltx (less than xml) object
*/
parseMessageFromXml (m/* :any */) {
function parseArray (node) {
for (var n of node.children) {
if (n.getAttribute('isArray') === 'true') {
return parseArray(n)
} else {
return parseObject(n)
}
}
}
function parseObject (node/* :any */) {
var json = {}
for (var attrName in node.attrs) {
var value = node.attrs[attrName]
var int = parseInt(value, 10)
if (isNaN(int) || ('' + int) !== value) {
json[attrName] = value
} else {
json[attrName] = int
}
}
for (var n/* :any */ in node.children) {
var name = n.name
if (n.getAttribute('isArray') === 'true') {
json[name] = parseArray(n)
} else {
json[name] = parseObject(n)
}
}
return json
}
parseObject(m)
}
/*
encode message in xml
we use string because Strophe only accepts an "xml-string"..
So {a:4,b:{c:5}} will look like
<y a="4">
<b c="5"></b>
</y>
m - ltx element
json - Object
*/
encodeMessageToXml (msg, obj) {
// attributes is optional
function encodeObject (m, json) {
for (var name in json) {
var value = json[name]
if (name == null) {
// nop
} else if (value.constructor === Object) {
encodeObject(m.c(name), value)
} else if (value.constructor === Array) {
encodeArray(m.c(name), value)
} else {
m.setAttribute(name, value)
}
}
}
function encodeArray (m, array) {
m.setAttribute('isArray', 'true')
for (var e of array) {
if (e.constructor === Object) {
encodeObject(m.c('array-element'), e)
} else {
encodeArray(m.c('array-element'), e)
}
}
}
if (obj.constructor === Object) {
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
} else if (obj.constructor === Array) {
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
} else {
throw new Error("I can't encode this json!")
}
}
}
Y.AbstractConnector = AbstractConnector
}

View File

@ -85,11 +85,11 @@ export default function extendDatabase (Y /* :any */) {
console.warn('gc should be empty when not synced!')
}
return new Promise((resolve) => {
os.requestTransaction(function * () {
os.requestTransaction(function () {
if (os.y.connector != null && os.y.connector.isSynced) {
for (var i = 0; i < os.gc2.length; i++) {
var oid = os.gc2[i]
yield * this.garbageCollectOperation(oid)
this.garbageCollectOperation(oid)
}
os.gc2 = os.gc1
os.gc1 = []
@ -197,15 +197,15 @@ export default function extendDatabase (Y /* :any */) {
this.gc = false
this.gcTimeout = -1
return new Promise(function (resolve) {
self.requestTransaction(function * () {
self.requestTransaction(function () {
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
self.gc1 = []
self.gc2 = []
for (var i = 0; i < ungc.length; i++) {
var op = yield * this.getOperation(ungc[i])
var op = this.getOperation(ungc[i])
if (op != null) {
delete op.gc
yield * this.setOperation(op)
this.setOperation(op)
}
}
resolve()
@ -224,7 +224,7 @@ export default function extendDatabase (Y /* :any */) {
returns true iff op was added to GC
*/
* addToGarbageCollector (op, left) {
addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true &&
@ -235,12 +235,12 @@ export default function extendDatabase (Y /* :any */) {
if (left != null && left.deleted === true) {
gc = true
} else if (op.content != null && op.content.length > 1) {
op = yield * this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
op = this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
gc = true
}
if (gc) {
op.gc = true
yield * this.setOperation(op)
this.setOperation(op)
this.store.queueGarbageCollector(op.id)
return true
}
@ -265,7 +265,7 @@ export default function extendDatabase (Y /* :any */) {
}
}
}
* destroy () {
destroy () {
clearTimeout(this.gcInterval)
this.gcInterval = null
this.stopRepairCheck()
@ -274,9 +274,9 @@ export default function extendDatabase (Y /* :any */) {
if (!this.userIdPromise.inProgress) {
this.userIdPromise.inProgress = true
var self = this
self.requestTransaction(function * () {
self.requestTransaction(function () {
self.userId = userId
var state = yield * this.getState(userId)
var state = this.getState(userId)
self.opClock = state.clock
self.userIdPromise.resolve(userId)
})
@ -355,7 +355,7 @@ export default function extendDatabase (Y /* :any */) {
this.listenersByIdRequestPending = true
var store = this
this.requestTransaction(function * () {
this.requestTransaction(function () {
var exeNow = store.listenersByIdExecuteNow
store.listenersByIdExecuteNow = []
@ -366,7 +366,7 @@ export default function extendDatabase (Y /* :any */) {
for (let key = 0; key < exeNow.length; key++) {
let o = exeNow[key].op
yield * store.tryExecute.call(this, o)
store.tryExecute.call(this, o)
}
for (var sid in ls) {
@ -374,9 +374,9 @@ export default function extendDatabase (Y /* :any */) {
var id = JSON.parse(sid)
var op
if (typeof id[1] === 'string') {
op = yield * this.getOperation(id)
op = this.getOperation(id)
} else {
op = yield * this.getInsertion(id)
op = this.getInsertion(id)
}
if (op == null) {
store.listenersById[sid] = l
@ -385,7 +385,7 @@ export default function extendDatabase (Y /* :any */) {
let listener = l[i]
let o = listener.op
if (--listener.missing === 0) {
yield * store.tryExecute.call(this, o)
store.tryExecute.call(this, o)
}
}
}
@ -402,15 +402,15 @@ export default function extendDatabase (Y /* :any */) {
addOperation: any;
whenOperationsExist: any;
*/
* tryExecute (op) {
this.store.addToDebug('yield * this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
tryExecute (op) {
this.store.addToDebug('this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
if (op.struct === 'Delete') {
yield * Y.Struct.Delete.execute.call(this, op)
Y.Struct.Delete.execute.call(this, op)
// this is now called in Transaction.deleteOperation!
// yield * this.store.operationAdded(this, op)
// this.store.operationAdded(this, op)
} else {
// check if this op was defined
var defined = yield * this.getInsertion(op.id)
var defined = this.getInsertion(op.id)
while (defined != null && defined.content != null) {
// check if this op has a longer content in the case it is defined
if (defined.id[1] + defined.content.length < op.id[1] + op.content.length) {
@ -419,23 +419,23 @@ export default function extendDatabase (Y /* :any */) {
op.id = [op.id[0], op.id[1] + overlapSize]
op.left = Y.utils.getLastId(defined)
op.origin = op.left
defined = yield * this.getOperation(op.id) // getOperation suffices here
defined = this.getOperation(op.id) // getOperation suffices here
} else {
break
}
}
if (defined == null) {
var opid = op.id
var isGarbageCollected = yield * this.isGarbageCollected(opid)
var isGarbageCollected = this.isGarbageCollected(opid)
if (!isGarbageCollected) {
// TODO: reduce number of get / put calls for op ..
yield * Y.Struct[op.struct].execute.call(this, op)
yield * this.addOperation(op)
yield * this.store.operationAdded(this, op)
Y.Struct[op.struct].execute.call(this, op)
this.addOperation(op)
this.store.operationAdded(this, op)
// operationAdded can change op..
op = yield * this.getOperation(opid)
op = this.getOperation(opid)
// if insertion, try to combine with left
yield * this.tryCombineWithLeft(op)
this.tryCombineWithLeft(op)
}
}
}
@ -452,15 +452,15 @@ export default function extendDatabase (Y /* :any */) {
* Always:
* * Call type
*/
* operationAdded (transaction, op) {
operationAdded (transaction, op) {
if (op.struct === 'Delete') {
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
if (type != null) {
yield * type._changed(transaction, op)
type._changed(transaction, op)
}
} else {
// increase SS
yield * transaction.updateState(op.id[0])
transaction.updateState(op.id[0])
var opLen = op.content != null ? op.content.length : 1
for (let i = 0; i < opLen; i++) {
// notify whenOperation listeners (by id)
@ -480,9 +480,9 @@ export default function extendDatabase (Y /* :any */) {
// if parent is deleted, mark as gc'd and return
if (op.parent != null) {
var parentIsDeleted = yield * transaction.isDeleted(op.parent)
var parentIsDeleted = transaction.isDeleted(op.parent)
if (parentIsDeleted) {
yield * transaction.deleteList(op.id)
transaction.deleteList(op.id)
return
}
}
@ -490,7 +490,7 @@ export default function extendDatabase (Y /* :any */) {
// notify parent, if it was instanciated as a custom type
if (t != null) {
let o = Y.utils.copyOperation(op)
yield * t._changed(transaction, o)
t._changed(transaction, o)
}
if (!op.deleted) {
// Delete if DS says this is actually deleted
@ -499,13 +499,13 @@ export default function extendDatabase (Y /* :any */) {
// TODO: !! console.log('TODO: change this before commiting')
for (let i = 0; i < len; i++) {
var id = [startId[0], startId[1] + i]
var opIsDeleted = yield * transaction.isDeleted(id)
var opIsDeleted = transaction.isDeleted(id)
if (opIsDeleted) {
var delop = {
struct: 'Delete',
target: id
}
yield * this.tryExecute.call(transaction, delop)
this.tryExecute.call(transaction, delop)
}
}
}
@ -528,6 +528,7 @@ export default function extendDatabase (Y /* :any */) {
return Promise.resolve()
}
}
// Check if there is another transaction request.
// * the last transaction is always a flush :)
getNextRequest () {
@ -542,8 +543,8 @@ export default function extendDatabase (Y /* :any */) {
return null
} else {
this.transactionIsFlushed = true
return function * () {
yield * this.flush()
return function () {
this.flush()
}
}
} else {
@ -570,13 +571,13 @@ export default function extendDatabase (Y /* :any */) {
Init type. This is called when a remote operation is retrieved, and transformed to a type
TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
*/
* initType (id, args) {
initType (id, args) {
var sid = JSON.stringify(id)
var t = this.store.initializedTypes[sid]
if (t == null) {
var op/* :MapStruct | ListStruct */ = yield * this.getOperation(id)
var op/* :MapStruct | ListStruct */ = this.getOperation(id)
if (op != null) {
t = yield * Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
t = Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
this.store.initializedTypes[sid] = t
}
}
@ -591,11 +592,11 @@ export default function extendDatabase (Y /* :any */) {
var op = Y.Struct[structname].create(id, typedefinition[1])
op.type = typedefinition[0].name
this.requestTransaction(function * () {
this.requestTransaction(function () {
if (op.id[0] === 0xFFFFFF) {
yield * this.setOperation(op)
this.setOperation(op)
} else {
yield * this.applyCreatedOperations([op])
this.applyCreatedOperations([op])
}
})
var t = Y[op.type].typeDefinition.createType(this, op, typedefinition[1])

View File

@ -1,354 +0,0 @@
/* global async, databases, describe, beforeEach, afterEach */
/* eslint-env browser,jasmine,console */
'use strict'
var Y = require('./SpecHelper.js')
for (let database of databases) {
describe(`Database (${database})`, function () {
var store
describe('DeleteStore', function () {
describe('Basic', function () {
beforeEach(function () {
store = new Y[database](null, {
gcTimeout: -1,
namespace: 'testing'
})
})
afterEach(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
done()
})
})
it('Deleted operation is deleted', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['u1', 10], 1)
expect(yield * this.isDeleted(['u1', 10])).toBeTruthy()
expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
done()
})
}))
it('Deleted operation extends other deleted operation', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['u1', 10], 1)
yield * this.markDeleted(['u1', 11], 1)
expect(yield * this.isDeleted(['u1', 10])).toBeTruthy()
expect(yield * this.isDeleted(['u1', 11])).toBeTruthy()
expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]})
done()
})
}))
it('Deleted operation extends other deleted operation', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['0', 3], 1)
yield * this.markDeleted(['0', 4], 1)
yield * this.markDeleted(['0', 2], 1)
expect(yield * this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
done()
})
}))
it('Debug #1', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['166', 0], 1)
yield * this.markDeleted(['166', 2], 1)
yield * this.markDeleted(['166', 0], 1)
yield * this.markDeleted(['166', 2], 1)
yield * this.markGarbageCollected(['166', 2], 1)
yield * this.markDeleted(['166', 1], 1)
yield * this.markDeleted(['166', 3], 1)
yield * this.markGarbageCollected(['166', 3], 1)
yield * this.markDeleted(['166', 0], 1)
expect(yield * this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]})
done()
})
}))
it('Debug #2', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['293', 0], 1)
yield * this.markDeleted(['291', 2], 1)
yield * this.markDeleted(['291', 2], 1)
yield * this.markGarbageCollected(['293', 0], 1)
yield * this.markDeleted(['293', 1], 1)
yield * this.markGarbageCollected(['291', 2], 1)
expect(yield * this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]})
done()
})
}))
it('Debug #3', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['581', 0], 1)
yield * this.markDeleted(['581', 1], 1)
yield * this.markDeleted(['580', 0], 1)
yield * this.markDeleted(['580', 0], 1)
yield * this.markGarbageCollected(['581', 0], 1)
yield * this.markDeleted(['581', 2], 1)
yield * this.markDeleted(['580', 1], 1)
yield * this.markDeleted(['580', 2], 1)
yield * this.markDeleted(['580', 1], 1)
yield * this.markDeleted(['580', 2], 1)
yield * this.markGarbageCollected(['581', 2], 1)
yield * this.markGarbageCollected(['581', 1], 1)
yield * this.markGarbageCollected(['580', 1], 1)
expect(yield * this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]})
done()
})
}))
it('Debug #4', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['544', 0], 1)
yield * this.markDeleted(['543', 2], 1)
yield * this.markDeleted(['544', 0], 1)
yield * this.markDeleted(['543', 2], 1)
yield * this.markGarbageCollected(['544', 0], 1)
yield * this.markDeleted(['545', 1], 1)
yield * this.markDeleted(['543', 4], 1)
yield * this.markDeleted(['543', 3], 1)
yield * this.markDeleted(['544', 1], 1)
yield * this.markDeleted(['544', 2], 1)
yield * this.markDeleted(['544', 1], 1)
yield * this.markDeleted(['544', 2], 1)
yield * this.markGarbageCollected(['543', 2], 1)
yield * this.markGarbageCollected(['543', 4], 1)
yield * this.markGarbageCollected(['544', 2], 1)
yield * this.markGarbageCollected(['543', 3], 1)
expect(yield * this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]})
done()
})
}))
it('Debug #5', async(function * (done) {
store.requestTransaction(function * () {
yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]})
expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]})
done()
})
}))
it('Debug #6', async(function * (done) {
store.requestTransaction(function * () {
yield * this.applyDeleteSet({'40': [[0, 3, false]]})
expect(yield * this.getDeleteSet()).toEqual({'40': [[0, 3, false]]})
yield * this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
expect(yield * this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
done()
})
}))
it('Debug #7', async(function * (done) {
store.requestTransaction(function * () {
yield * this.markDeleted(['9', 2], 1)
yield * this.markDeleted(['11', 2], 1)
yield * this.markDeleted(['11', 4], 1)
yield * this.markDeleted(['11', 1], 1)
yield * this.markDeleted(['9', 4], 1)
yield * this.markDeleted(['10', 0], 1)
yield * this.markGarbageCollected(['11', 2], 1)
yield * this.markDeleted(['11', 2], 1)
yield * this.markGarbageCollected(['11', 3], 1)
yield * this.markDeleted(['11', 3], 1)
yield * this.markDeleted(['11', 3], 1)
yield * this.markDeleted(['9', 4], 1)
yield * this.markDeleted(['10', 0], 1)
yield * this.markGarbageCollected(['11', 1], 1)
yield * this.markDeleted(['11', 1], 1)
expect(yield * this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]})
done()
})
}))
})
})
describe('OperationStore', function () {
describe('Basic Tests', function () {
beforeEach(function () {
store = new Y[database](null, {
gcTimeout: -1,
namespace: 'testing'
})
})
afterEach(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
done()
})
})
it('debug #1', function (done) {
store.requestTransaction(function * () {
yield * this.os.put({id: [2]})
yield * this.os.put({id: [0]})
yield * this.os.delete([2])
yield * this.os.put({id: [1]})
expect(yield * this.os.find([0])).toBeTruthy()
expect(yield * this.os.find([1])).toBeTruthy()
expect(yield * this.os.find([2])).toBeFalsy()
done()
})
})
it('can add&retrieve 5 elements', function (done) {
store.requestTransaction(function * () {
yield * this.os.put({val: 'four', id: [4]})
yield * this.os.put({val: 'one', id: [1]})
yield * this.os.put({val: 'three', id: [3]})
yield * this.os.put({val: 'two', id: [2]})
yield * this.os.put({val: 'five', id: [5]})
expect((yield * this.os.find([1])).val).toEqual('one')
expect((yield * this.os.find([2])).val).toEqual('two')
expect((yield * this.os.find([3])).val).toEqual('three')
expect((yield * this.os.find([4])).val).toEqual('four')
expect((yield * this.os.find([5])).val).toEqual('five')
done()
})
})
it('5 elements do not exist anymore after deleting them', function (done) {
store.requestTransaction(function * () {
yield * this.os.put({val: 'four', id: [4]})
yield * this.os.put({val: 'one', id: [1]})
yield * this.os.put({val: 'three', id: [3]})
yield * this.os.put({val: 'two', id: [2]})
yield * this.os.put({val: 'five', id: [5]})
yield * this.os.delete([4])
expect(yield * this.os.find([4])).not.toBeTruthy()
yield * this.os.delete([3])
expect(yield * this.os.find([3])).not.toBeTruthy()
yield * this.os.delete([2])
expect(yield * this.os.find([2])).not.toBeTruthy()
yield * this.os.delete([1])
expect(yield * this.os.find([1])).not.toBeTruthy()
yield * this.os.delete([5])
expect(yield * this.os.find([5])).not.toBeTruthy()
done()
})
})
})
var numberOfOSTests = 1000
describe(`Random Tests - after adding&deleting (0.8/0.2) ${numberOfOSTests} times`, function () {
var elements = []
beforeAll(function (done) {
store = new Y[database](null, {
gcTimeout: -1,
namespace: 'testing'
})
store.requestTransaction(function * () {
for (var i = 0; i < numberOfOSTests; i++) {
var r = Math.random()
if (r < 0.8) {
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
if (!(yield * this.os.find(obj))) {
elements.push(obj)
yield * this.os.put({id: obj})
}
} else if (elements.length > 0) {
var elemid = Math.floor(Math.random() * elements.length)
var elem = elements[elemid]
elements = elements.filter(function (e) {
return !Y.utils.compareIds(e, elem)
})
yield * this.os.delete(elem)
}
}
done()
})
})
afterAll(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
done()
})
})
it('can find every object', function (done) {
store.requestTransaction(function * () {
for (var id of elements) {
expect((yield * this.os.find(id)).id).toEqual(id)
}
done()
})
})
it('can find every object with lower bound search', function (done) {
store.requestTransaction(function * () {
for (var id of elements) {
var e = yield * this.os.findWithLowerBound(id)
expect(e.id).toEqual(id)
}
done()
})
})
it('iterating over a tree with lower bound yields the right amount of results', function (done) {
var lowerBound = elements[Math.floor(Math.random() * elements.length)]
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, null, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree without bounds yield the right amount of results', function (done) {
var lowerBound = null
var expectedResults = elements.filter(function (e, pos) {
return elements.indexOf(e) === pos
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, null, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree with upper bound yields the right amount of results', function (done) {
var upperBound = elements[Math.floor(Math.random() * elements.length)]
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, null, upperBound, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree with upper and lower bounds yield the right amount of results', function (done) {
var b1 = elements[Math.floor(Math.random() * elements.length)]
var b2 = elements[Math.floor(Math.random() * elements.length)]
var upperBound, lowerBound
if (Y.utils.smaller(b1, b2)) {
lowerBound = b1
upperBound = b2
} else {
lowerBound = b2
upperBound = b1
}
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, upperBound, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
})
})
})
}

View File

@ -58,7 +58,7 @@ export function computeMessageUpdate (decoder, encoder, conn) {
}
export function sendSyncStep1 (conn, syncUser) {
conn.y.db.requestTransaction(function * () {
conn.y.db.requestTransaction(function () {
let encoder = new BinaryEncoder()
encoder.writeVarString(conn.opts.room || '')
encoder.writeVarString('sync step 1')
@ -66,7 +66,7 @@ export function sendSyncStep1 (conn, syncUser) {
encoder.writeVarUint(conn.protocolVersion)
let preferUntransformed = conn.preferUntransformed && this.os.length === 0 // TODO: length may not be defined
encoder.writeUint8(preferUntransformed ? 1 : 0)
yield * this.writeStateSet(encoder)
this.writeStateSet(encoder)
conn.send(syncUser, encoder.createBuffer())
})
}
@ -99,19 +99,19 @@ export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sen
return conn.y.db.whenTransactionsFinished().then(() => {
// send sync step 2
conn.y.db.requestTransaction(function * () {
conn.y.db.requestTransaction(function () {
encoder.writeVarString('sync step 2')
encoder.writeVarString(conn.authInfo || '')
if (preferUntransformed) {
encoder.writeUint8(1)
yield * this.writeOperationsUntransformed(encoder)
this.writeOperationsUntransformed(encoder)
} else {
encoder.writeUint8(0)
yield * this.writeOperations(encoder, decoder)
this.writeOperations(encoder, decoder)
}
yield * this.writeDeleteSet(encoder)
this.writeDeleteSet(encoder)
conn.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true
})
@ -174,17 +174,17 @@ export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sen
let defer = senderConn.syncStep2
// apply operations first
db.requestTransaction(function * () {
db.requestTransaction(function () {
let osUntransformed = decoder.readUint8()
if (osUntransformed === 1) {
yield * this.applyOperationsUntransformed(decoder)
this.applyOperationsUntransformed(decoder)
} else {
this.store.applyOperations(decoder)
}
})
// then apply ds
db.requestTransaction(function * () {
yield * this.applyDeleteSet(decoder)
db.requestTransaction(function () {
this.applyDeleteSet(decoder)
})
return db.whenTransactionsFinished().then(() => {
conn._setSyncedWith(sender)

View File

@ -8,9 +8,11 @@ export default function extendPersistence (Y) {
this.saveOperationsBuffer = []
this.log = Y.debug('y:persistence')
}
saveToMessageQueue (binary) {
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
}
saveOperations (ops) {
ops = ops.map(function (op) {
return Y.Struct[op.struct].encode(op)
@ -39,5 +41,6 @@ export default function extendPersistence (Y) {
}
}
}
Y.AbstractPersistence = AbstractPersistence
}

506
src/RedBlackTree.js Normal file
View File

@ -0,0 +1,506 @@
export default function extendRBTree (Y) {
class N {
// A created node is always red!
constructor (val) {
this.val = val
this.color = true
this._left = null
this._right = null
this._parent = null
if (val.id === null) {
throw new Error('You must define id!')
}
}
isRed () { return this.color }
isBlack () { return !this.color }
redden () { this.color = true; return this }
blacken () { this.color = false; return this }
get grandparent () {
return this.parent.parent
}
get parent () {
return this._parent
}
get sibling () {
return (this === this.parent.left)
? this.parent.right : this.parent.left
}
get left () {
return this._left
}
get right () {
return this._right
}
set left (n) {
if (n !== null) {
n._parent = this
}
this._left = n
}
set right (n) {
if (n !== null) {
n._parent = this
}
this._right = n
}
rotateLeft (tree) {
var parent = this.parent
var newParent = this.right
var newRight = this.right.left
newParent.left = this
this.right = newRight
if (parent === null) {
tree.root = newParent
newParent._parent = null
} else if (parent.left === this) {
parent.left = newParent
} else if (parent.right === this) {
parent.right = newParent
} else {
throw new Error('The elements are wrongly connected!')
}
}
next () {
if (this.right !== null) {
// search the most left node in the right tree
var o = this.right
while (o.left !== null) {
o = o.left
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.left) {
p = p.parent
}
return p.parent
}
}
prev () {
if (this.left !== null) {
// search the most right node in the left tree
var o = this.left
while (o.right !== null) {
o = o.right
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.right) {
p = p.parent
}
return p.parent
}
}
rotateRight (tree) {
var parent = this.parent
var newParent = this.left
var newLeft = this.left.right
newParent.right = this
this.left = newLeft
if (parent === null) {
tree.root = newParent
newParent._parent = null
} else if (parent.left === this) {
parent.left = newParent
} else if (parent.right === this) {
parent.right = newParent
} else {
throw new Error('The elements are wrongly connected!')
}
}
getUncle () {
// we can assume that grandparent exists when this is called!
if (this.parent === this.parent.parent.left) {
return this.parent.parent.right
} else {
return this.parent.parent.left
}
}
}
class RBTree {
constructor () {
this.root = null
this.length = 0
}
findNext (id) {
return this.findWithLowerBound([id[0], id[1] + 1])
}
findPrev (id) {
return this.findWithUpperBound([id[0], id[1] - 1])
}
findNodeWithLowerBound (from) {
if (from === void 0) {
throw new Error('You must define from!')
}
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if ((from === null || Y.utils.smaller(from, o.val.id)) && o.left !== null) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.left
} else if (from !== null && Y.utils.smaller(o.val.id, from)) {
// o is not within the bound, maybe one of the right elements is..
if (o.right !== null) {
o = o.right
} else {
// there is no right element. Search for the next bigger element,
// this should be within the bounds
return o.next()
}
} else {
return o
}
}
}
}
findNodeWithUpperBound (to) {
if (to === void 0) {
throw new Error('You must define from!')
}
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.right
} else if (to !== null && Y.utils.smaller(to, o.val.id)) {
// o is not within the bound, maybe one of the left elements is..
if (o.left !== null) {
o = o.left
} else {
// there is no left element. Search for the prev smaller element,
// this should be within the bounds
return o.prev()
}
} else {
return o
}
}
}
}
findSmallestNode () {
var o = this.root
while (o != null && o.left != null) {
o = o.left
}
return o
}
findWithLowerBound (from) {
var n = this.findNodeWithLowerBound(from)
return n == null ? null : n.val
}
findWithUpperBound (to) {
var n = this.findNodeWithUpperBound(to)
return n == null ? null : n.val
}
iterate (t, from, to, f) {
var o
if (from === null) {
o = this.findSmallestNode()
} else {
o = this.findNodeWithLowerBound(from)
}
while (
o !== null &&
(
to === null || // eslint-disable-line no-unmodified-loop-condition
Y.utils.smaller(o.val.id, to) ||
Y.utils.compareIds(o.val.id, to)
)
) {
f.call(t, o.val)
o = o.next()
}
return true
}
logTable (from, to, filter) {
if (filter == null) {
filter = function () {
return true
}
}
if (from == null) { from = null }
if (to == null) { to = null }
var os = []
this.iterate(this, from, to, function (o) {
if (filter(o)) {
var o_ = {}
for (var key in o) {
if (typeof o[key] === 'object') {
o_[key] = JSON.stringify(o[key])
} else {
o_[key] = o[key]
}
}
os.push(o_)
}
})
if (console.table != null) {
console.table(os)
}
}
find (id) {
var n
return (n = this.findNode(id)) ? n.val : null
}
findNode (id) {
if (id == null || id.constructor !== Array) {
throw new Error('Expect id to be an array!')
}
var o = this.root
if (o === null) {
return false
} else {
while (true) {
if (o === null) {
return false
}
if (Y.utils.smaller(id, o.val.id)) {
o = o.left
} else if (Y.utils.smaller(o.val.id, id)) {
o = o.right
} else {
return o
}
}
}
}
delete (id) {
if (id == null || id.constructor !== Array) {
throw new Error('id is expected to be an Array!')
}
var d = this.findNode(id)
if (d == null) {
// throw new Error('Element does not exist!')
return
}
this.length--
if (d.left !== null && d.right !== null) {
// switch d with the greates element in the left subtree.
// o should have at most one child.
var o = d.left
// find
while (o.right !== null) {
o = o.right
}
// switch
d.val = o.val
d = o
}
// d has at most one child
// let n be the node that replaces d
var isFakeChild
var child = d.left || d.right
if (child === null) {
isFakeChild = true
child = new N({id: 0})
child.blacken()
d.right = child
} else {
isFakeChild = false
}
if (d.parent === null) {
if (!isFakeChild) {
this.root = child
child.blacken()
child._parent = null
} else {
this.root = null
}
return
} else if (d.parent.left === d) {
d.parent.left = child
} else if (d.parent.right === d) {
d.parent.right = child
} else {
throw new Error('Impossible!')
}
if (d.isBlack()) {
if (child.isRed()) {
child.blacken()
} else {
this._fixDelete(child)
}
}
this.root.blacken()
if (isFakeChild) {
if (child.parent.left === child) {
child.parent.left = null
} else if (child.parent.right === child) {
child.parent.right = null
} else {
throw new Error('Impossible #3')
}
}
}
_fixDelete (n) {
function isBlack (node) {
return node !== null ? node.isBlack() : true
}
function isRed (node) {
return node !== null ? node.isRed() : false
}
if (n.parent === null) {
// this can only be called after the first iteration of fixDelete.
return
}
// d was already replaced by the child
// d is not the root
// d and child are black
var sibling = n.sibling
if (isRed(sibling)) {
// make sibling the grandfather
n.parent.redden()
sibling.blacken()
if (n === n.parent.left) {
n.parent.rotateLeft(this)
} else if (n === n.parent.right) {
n.parent.rotateRight(this)
} else {
throw new Error('Impossible #2')
}
sibling = n.sibling
}
// parent, sibling, and children of n are black
if (n.parent.isBlack() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
this._fixDelete(n.parent)
} else if (n.parent.isRed() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
n.parent.blacken()
} else {
if (n === n.parent.left &&
sibling.isBlack() &&
isRed(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
sibling.left.blacken()
sibling.rotateRight(this)
sibling = n.sibling
} else if (n === n.parent.right &&
sibling.isBlack() &&
isRed(sibling.right) &&
isBlack(sibling.left)
) {
sibling.redden()
sibling.right.blacken()
sibling.rotateLeft(this)
sibling = n.sibling
}
sibling.color = n.parent.color
n.parent.blacken()
if (n === n.parent.left) {
sibling.right.blacken()
n.parent.rotateLeft(this)
} else {
sibling.left.blacken()
n.parent.rotateRight(this)
}
}
}
put (v) {
if (v == null || v.id == null || v.id.constructor !== Array) {
throw new Error('v is expected to have an id property which is an Array!')
}
var node = new N(v)
if (this.root !== null) {
var p = this.root // p abbrev. parent
while (true) {
if (Y.utils.smaller(node.val.id, p.val.id)) {
if (p.left === null) {
p.left = node
break
} else {
p = p.left
}
} else if (Y.utils.smaller(p.val.id, node.val.id)) {
if (p.right === null) {
p.right = node
break
} else {
p = p.right
}
} else {
p.val = node.val
return p
}
}
this._fixInsert(node)
} else {
this.root = node
}
this.length++
this.root.blacken()
return node
}
_fixInsert (n) {
if (n.parent === null) {
n.blacken()
return
} else if (n.parent.isBlack()) {
return
}
var uncle = n.getUncle()
if (uncle !== null && uncle.isRed()) {
// Note: parent: red, uncle: red
n.parent.blacken()
uncle.blacken()
n.grandparent.redden()
this._fixInsert(n.grandparent)
} else {
// Note: parent: red, uncle: black or null
// Now we transform the tree in such a way that
// either of these holds:
// 1) grandparent.left.isRed
// and grandparent.left.left.isRed
// 2) grandparent.right.isRed
// and grandparent.right.right.isRed
if (n === n.parent.right && n.parent === n.grandparent.left) {
n.parent.rotateLeft(this)
// Since we rotated and want to use the previous
// cases, we need to set n in such a way that
// n.parent.isRed again
n = n.left
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
n.parent.rotateRight(this)
// see above
n = n.right
}
// Case 1) or 2) hold from here on.
// Now traverse grandparent, make parent a black node
// on the highest level which holds two red nodes.
n.parent.blacken()
n.grandparent.redden()
if (n === n.parent.left) {
// Case 1
n.grandparent.rotateRight(this)
} else {
// Case 2
n.grandparent.rotateLeft(this)
}
}
}
flush () {}
}
Y.utils.RBTree = RBTree
}

View File

@ -73,8 +73,8 @@ export default function extendStruct (Y) {
requiredOps: function (op) {
return [] // [op.target]
},
execute: function * (op) {
return yield * this.deleteOperation(op.target, op.length || 1)
execute: function (op) {
return this.deleteOperation(op.target, op.length || 1)
}
}
@ -216,18 +216,18 @@ export default function extendStruct (Y) {
}
return ids
},
getDistanceToOrigin: function * (op) {
getDistanceToOrigin: function (op) {
if (op.left == null) {
return 0
} else {
var d = 0
var o = yield * this.getInsertion(op.left)
var o = this.getInsertion(op.left)
while (!Y.utils.matchesId(o, op.origin)) {
d++
if (o.left == null) {
break
} else {
o = yield * this.getInsertion(o.left)
o = this.getInsertion(o.left)
}
}
return d
@ -248,7 +248,7 @@ export default function extendStruct (Y) {
# case 3: $origin > $o.origin
# $this insert_position is to the left of $o (forever!)
*/
execute: function * (op) {
execute: function (op) {
var i // loop counter
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
@ -258,17 +258,17 @@ export default function extendStruct (Y) {
if (op.origin != null) { // TODO: !== instead of !=
// we save in origin that op originates in it
// we need that later when we eventually garbage collect origin (see transaction)
var origin = yield * this.getInsertionCleanEnd(op.origin)
var origin = this.getInsertionCleanEnd(op.origin)
if (origin.originOf == null) {
origin.originOf = []
}
origin.originOf.push(op.id)
yield * this.setOperation(origin)
this.setOperation(origin)
if (origin.right != null) {
tryToRemergeLater.push(origin.right)
}
}
var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
var distanceToOrigin = i = Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
// now we begin to insert op in the list of insertions..
var o
@ -277,29 +277,29 @@ export default function extendStruct (Y) {
// find o. o is the first conflicting operation
if (op.left != null) {
o = yield * this.getInsertionCleanEnd(op.left)
o = this.getInsertionCleanEnd(op.left)
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
// only if not added previously
tryToRemergeLater.push(o.right)
}
o = (o.right == null) ? null : yield * this.getOperation(o.right)
o = (o.right == null) ? null : this.getOperation(o.right)
} else { // left == null
parent = yield * this.getOperation(op.parent)
parent = this.getOperation(op.parent)
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
start = startId == null ? null : yield * this.getOperation(startId)
start = startId == null ? null : this.getOperation(startId)
o = start
}
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
if (op.right != null) {
tryToRemergeLater.push(op.right)
yield * this.getInsertionCleanStart(op.right)
this.getInsertionCleanStart(op.right)
}
// handle conflicts
while (true) {
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o)
var oOriginDistance = Struct.Insert.getDistanceToOrigin.call(this, o)
if (oOriginDistance === i) {
// case 1
if (o.id[0] < op.id[0]) {
@ -317,7 +317,7 @@ export default function extendStruct (Y) {
}
i++
if (o.right != null) {
o = yield * this.getInsertion(o.right)
o = this.getInsertion(o.right)
} else {
o = null
}
@ -330,17 +330,17 @@ export default function extendStruct (Y) {
var left = null
var right = null
if (parent == null) {
parent = yield * this.getOperation(op.parent)
parent = this.getOperation(op.parent)
}
// reconnect left and set right of op
if (op.left != null) {
left = yield * this.getInsertion(op.left)
left = this.getInsertion(op.left)
// link left
op.right = left.right
left.right = op.id
yield * this.setOperation(left)
this.setOperation(left)
} else {
// set op.right from parent, if necessary
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
@ -348,33 +348,33 @@ export default function extendStruct (Y) {
// reconnect right
if (op.right != null) {
// TODO: wanna connect right too?
right = yield * this.getOperation(op.right)
right = this.getOperation(op.right)
right.left = Y.utils.getLastId(op)
// if right exists, and it is supposed to be gc'd. Remove it from the gc
if (right.gc != null) {
if (right.content != null && right.content.length > 1) {
right = yield * this.getInsertionCleanEnd(right.id)
right = this.getInsertionCleanEnd(right.id)
}
this.store.removeFromGarbageCollector(right)
}
yield * this.setOperation(right)
this.setOperation(right)
}
// update parents .map/start/end properties
if (op.parentSub != null) {
if (left == null) {
parent.map[op.parentSub] = op.id
yield * this.setOperation(parent)
this.setOperation(parent)
}
// is a child of a map struct.
// Then also make sure that only the most left element is not deleted
// We do not call the type in this case (this is what the third parameter is for)
if (op.right != null) {
yield * this.deleteOperation(op.right, 1, true)
this.deleteOperation(op.right, 1, true)
}
if (op.left != null) {
yield * this.deleteOperation(op.id, 1, true)
this.deleteOperation(op.id, 1, true)
}
} else {
if (right == null || left == null) {
@ -384,14 +384,14 @@ export default function extendStruct (Y) {
if (left == null) {
parent.start = op.id
}
yield * this.setOperation(parent)
this.setOperation(parent)
}
}
// try to merge original op.left and op.origin
for (i = 0; i < tryToRemergeLater.length; i++) {
var m = yield * this.getOperation(tryToRemergeLater[i])
yield * this.tryCombineWithLeft(m)
var m = this.getOperation(tryToRemergeLater[i])
this.tryCombineWithLeft(m)
}
}
}
@ -451,16 +451,16 @@ export default function extendStruct (Y) {
*/
return []
},
execute: function * (op) {
execute: function (op) {
op.start = null
op.end = null
},
ref: function * (op, pos) {
ref: function (op, pos) {
if (op.start == null) {
return null
}
var res = null
var o = yield * this.getOperation(op.start)
var o = this.getOperation(op.start)
while (true) {
if (!o.deleted) {
@ -468,18 +468,18 @@ export default function extendStruct (Y) {
pos--
}
if (pos >= 0 && o.right != null) {
o = yield * this.getOperation(o.right)
o = this.getOperation(o.right)
} else {
break
}
}
return res
},
map: function * (o, f) {
map: function (o, f) {
o = o.start
var res = []
while (o != null) { // TODO: change to != (at least some convention)
var operation = yield * this.getOperation(o)
var operation = this.getOperation(o)
if (!operation.deleted) {
res.push(f(operation))
}
@ -532,23 +532,23 @@ export default function extendStruct (Y) {
requiredOps: function () {
return []
},
execute: function * (op) {
execute: function (op) {
op.start = null
op.end = null
},
/*
Get a property by name
*/
get: function * (op, name) {
get: function (op, name) {
var oid = op.map[name]
if (oid != null) {
var res = yield * this.getOperation(oid)
var res = this.getOperation(oid)
if (res == null || res.deleted) {
return void 0
} else if (res.opContent == null) {
return res.content[0]
} else {
return yield * this.getType(res.opContent)
return this.getType(res.opContent)
}
}
}
@ -608,7 +608,7 @@ export default function extendStruct (Y) {
requiredOps: function () {
return []
},
execute: function * () {},
execute: function () {},
ref: Struct.List.ref,
map: Struct.List.map,
/*

View File

@ -86,11 +86,11 @@ export default function extendTransaction (Y) {
* does not check for Struct.*.requiredOps()
* also broadcasts it through the connector
*/
* applyCreatedOperations (ops) {
applyCreatedOperations (ops) {
var send = []
for (var i = 0; i < ops.length; i++) {
var op = ops[i]
yield * this.store.tryExecute.call(this, op)
this.store.tryExecute.call(this, op)
if (op.id == null || typeof op.id[1] !== 'string') {
send.push(Y.Struct[op.struct].encode(op))
}
@ -104,17 +104,17 @@ export default function extendTransaction (Y) {
}
}
* deleteList (start) {
deleteList (start) {
while (start != null) {
start = yield * this.getOperation(start)
start = this.getOperation(start)
if (!start.gc) {
start.gc = true
start.deleted = true
yield * this.setOperation(start)
this.setOperation(start)
var delLength = start.content != null ? start.content.length : 1
yield * this.markDeleted(start.id, delLength)
this.markDeleted(start.id, delLength)
if (start.opContent != null) {
yield * this.deleteOperation(start.opContent)
this.deleteOperation(start.opContent)
}
this.store.queueGarbageCollector(start.id)
}
@ -125,14 +125,14 @@ export default function extendTransaction (Y) {
/*
Mark an operation as deleted, and add it to the GC, if possible.
*/
* deleteOperation (targetId, length, preventCallType) /* :Generator<any, any, any> */ {
deleteOperation (targetId, length, preventCallType) /* :Generator<any, any, any> */ {
if (length == null) {
length = 1
}
yield * this.markDeleted(targetId, length)
this.markDeleted(targetId, length)
while (length > 0) {
var callType = false
var target = yield * this.os.findWithUpperBound([targetId[0], targetId[1] + length - 1])
var target = this.os.findWithUpperBound([targetId[0], targetId[1] + length - 1])
var targetLength = target != null && target.content != null ? target.content.length : 1
if (target == null || target.id[0] !== targetId[0] || target.id[1] + targetLength <= targetId[1]) {
// does not exist or is not in the range of the deletion
@ -143,12 +143,12 @@ export default function extendTransaction (Y) {
if (!target.deleted) {
if (target.id[1] < targetId[1]) {
// starts to the left of the deletion range
target = yield * this.getInsertionCleanStart(targetId)
target = this.getInsertionCleanStart(targetId)
targetLength = target.content.length // must have content property!
}
if (target.id[1] + targetLength > targetId[1] + length) {
// ends to the right of the deletion range
target = yield * this.getInsertionCleanEnd([targetId[0], targetId[1] + length - 1])
target = this.getInsertionCleanEnd([targetId[0], targetId[1] + length - 1])
targetLength = target.content.length
}
}
@ -163,35 +163,35 @@ export default function extendTransaction (Y) {
// delete containing lists
if (target.start != null) {
// TODO: don't do it like this .. -.-
yield * this.deleteList(target.start)
// yield * this.deleteList(target.id) -- do not gc itself because this may still get referenced
this.deleteList(target.start)
// this.deleteList(target.id) -- do not gc itself because this may still get referenced
}
if (target.map != null) {
for (var name in target.map) {
yield * this.deleteList(target.map[name])
this.deleteList(target.map[name])
}
// TODO: here to.. (see above)
// yield * this.deleteList(target.id) -- see above
// this.deleteList(target.id) -- see above
}
if (target.opContent != null) {
yield * this.deleteOperation(target.opContent)
this.deleteOperation(target.opContent)
// target.opContent = null
}
if (target.requires != null) {
for (var i = 0; i < target.requires.length; i++) {
yield * this.deleteOperation(target.requires[i])
this.deleteOperation(target.requires[i])
}
}
}
var left
if (target.left != null) {
left = yield * this.getInsertion(target.left)
left = this.getInsertion(target.left)
} else {
left = null
}
// set here because it was deleted and/or gc'd
yield * this.setOperation(target)
this.setOperation(target)
/*
Check if it is possible to add right to the gc.
@ -200,12 +200,12 @@ export default function extendTransaction (Y) {
*/
var right
if (target.right != null) {
right = yield * this.getOperation(target.right)
right = this.getOperation(target.right)
} else {
right = null
}
if (callType && !preventCallType) {
yield * this.store.operationAdded(this, {
this.store.operationAdded(this, {
struct: 'Delete',
target: target.id,
length: targetLength,
@ -213,9 +213,9 @@ export default function extendTransaction (Y) {
})
}
// need to gc in the end!
yield * this.store.addToGarbageCollector.call(this, target, left)
this.store.addToGarbageCollector.call(this, target, left)
if (right != null) {
yield * this.store.addToGarbageCollector.call(this, right, target)
this.store.addToGarbageCollector.call(this, right, target)
}
}
}
@ -223,25 +223,25 @@ export default function extendTransaction (Y) {
/*
Mark an operation as deleted&gc'd
*/
* markGarbageCollected (id, len) {
markGarbageCollected (id, len) {
// this.mem.push(["gc", id]);
this.store.addToDebug('yield * this.markGarbageCollected(', id, ', ', len, ')')
var n = yield * this.markDeleted(id, len)
this.store.addToDebug('this.markGarbageCollected(', id, ', ', len, ')')
var n = this.markDeleted(id, len)
if (n.id[1] < id[1] && !n.gc) {
// un-extend left
var newlen = n.len - (id[1] - n.id[1])
n.len -= newlen
yield * this.ds.put(n)
this.ds.put(n)
n = {id: id, len: newlen, gc: false}
yield * this.ds.put(n)
this.ds.put(n)
}
// get prev&next before adding a new operation
var prev = yield * this.ds.findPrev(id)
var next = yield * this.ds.findNext(id)
var prev = this.ds.findPrev(id)
var next = this.ds.findNext(id)
if (id[1] + len < n.id[1] + n.len && !n.gc) {
// un-extend right
yield * this.ds.put({id: [id[0], id[1] + len], len: n.len - len, gc: false})
this.ds.put({id: [id[0], id[1] + len], len: n.len - len, gc: false})
n.len = len
}
// set gc'd
@ -253,7 +253,7 @@ export default function extendTransaction (Y) {
Y.utils.compareIds([prev.id[0], prev.id[1] + prev.len], n.id)
) {
prev.len += n.len
yield * this.ds.delete(n.id)
this.ds.delete(n.id)
n = prev
// ds.put n here?
}
@ -264,22 +264,22 @@ export default function extendTransaction (Y) {
Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id)
) {
n.len += next.len
yield * this.ds.delete(next.id)
this.ds.delete(next.id)
}
yield * this.ds.put(n)
yield * this.updateState(n.id[0])
this.ds.put(n)
this.updateState(n.id[0])
}
/*
Mark an operation as deleted.
returns the delete node
*/
* markDeleted (id, length) {
markDeleted (id, length) {
if (length == null) {
length = 1
}
// this.mem.push(["del", id]);
var n = yield * this.ds.findWithUpperBound(id)
var n = this.ds.findWithUpperBound(id)
if (n != null && n.id[0] === id[0]) {
if (n.id[1] <= id[1] && id[1] <= n.id[1] + n.len) {
// id is in n's range
@ -293,7 +293,7 @@ export default function extendTransaction (Y) {
if (diff < length) {
// a partial deletion
n = {id: [id[0], id[1] + diff], len: length - diff, gc: false}
yield * this.ds.put(n)
this.ds.put(n)
} else {
// already gc'd
throw new Error(
@ -308,15 +308,15 @@ export default function extendTransaction (Y) {
} else {
// cannot extend left (there is no left!)
n = {id: id, len: length, gc: false}
yield * this.ds.put(n) // TODO: you double-put !!
this.ds.put(n) // TODO: you double-put !!
}
} else {
// cannot extend left
n = {id: id, len: length, gc: false}
yield * this.ds.put(n)
this.ds.put(n)
}
// can extend right?
var next = yield * this.ds.findNext(n.id)
var next = this.ds.findNext(n.id)
if (
next != null &&
n.id[0] === next.id[0] &&
@ -332,8 +332,8 @@ export default function extendTransaction (Y) {
// delete the missing range after next
diff = diff - next.len // missing range after next
if (diff > 0) {
yield * this.ds.put(n) // unneccessary? TODO!
yield * this.markDeleted([next.id[0], next.id[1] + next.len], diff)
this.ds.put(n) // unneccessary? TODO!
this.markDeleted([next.id[0], next.id[1] + next.len], diff)
}
}
break
@ -342,8 +342,8 @@ export default function extendTransaction (Y) {
if (diff > next.len) {
// n is even longer than next
// get next.next, and try to extend it
var _next = yield * this.ds.findNext(next.id)
yield * this.ds.delete(next.id)
var _next = this.ds.findNext(next.id)
this.ds.delete(next.id)
if (_next == null || n.id[0] !== _next.id[0]) {
break
} else {
@ -354,13 +354,13 @@ export default function extendTransaction (Y) {
} else {
// n just partially overlaps with next. extend n, delete next, and break this loop
n.len += next.len - diff
yield * this.ds.delete(next.id)
this.ds.delete(next.id)
break
}
}
}
}
yield * this.ds.put(n)
this.ds.put(n)
return n
}
/*
@ -368,7 +368,7 @@ export default function extendTransaction (Y) {
other clients (e.g. master). This will query the database for
operations that can be gc'd and add them to the garbage collector.
*/
* garbageCollectAfterSync () {
garbageCollectAfterSync () {
// debugger
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
console.warn('gc should be empty after sync')
@ -376,28 +376,28 @@ export default function extendTransaction (Y) {
if (!this.store.gc) {
return
}
yield * this.os.iterate(this, null, null, function * (op) {
this.os.iterate(this, null, null, function (op) {
if (op.gc) {
delete op.gc
yield * this.setOperation(op)
this.setOperation(op)
}
if (op.parent != null) {
var parentDeleted = yield * this.isDeleted(op.parent)
var parentDeleted = this.isDeleted(op.parent)
if (parentDeleted) {
op.gc = true
if (!op.deleted) {
yield * this.markDeleted(op.id, op.content != null ? op.content.length : 1)
this.markDeleted(op.id, op.content != null ? op.content.length : 1)
op.deleted = true
if (op.opContent != null) {
yield * this.deleteOperation(op.opContent)
this.deleteOperation(op.opContent)
}
if (op.requires != null) {
for (var i = 0; i < op.requires.length; i++) {
yield * this.deleteOperation(op.requires[i])
this.deleteOperation(op.requires[i])
}
}
}
yield * this.setOperation(op)
this.setOperation(op)
this.store.gc1.push(op.id) // this is ok becaues its shortly before sync (otherwise use queueGarbageCollector!)
return
}
@ -405,9 +405,9 @@ export default function extendTransaction (Y) {
if (op.deleted) {
var left = null
if (op.left != null) {
left = yield * this.getInsertion(op.left)
left = this.getInsertion(op.left)
}
yield * this.store.addToGarbageCollector.call(this, op, left)
this.store.addToGarbageCollector.call(this, op, left)
}
})
}
@ -420,10 +420,10 @@ export default function extendTransaction (Y) {
* reset parent.end
* reset origins of all right ops
*/
* garbageCollectOperation (id) {
this.store.addToDebug('yield * this.garbageCollectOperation(', id, ')')
var o = yield * this.getOperation(id)
yield * this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd
garbageCollectOperation (id) {
this.store.addToDebug('this.garbageCollectOperation(', id, ')')
var o = this.getOperation(id)
this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd
// if op exists, then clean that mess up..
if (o != null) {
var deps = []
@ -434,32 +434,32 @@ export default function extendTransaction (Y) {
deps = deps.concat(o.requires)
}
for (var i = 0; i < deps.length; i++) {
var dep = yield * this.getOperation(deps[i])
var dep = this.getOperation(deps[i])
if (dep != null) {
if (!dep.deleted) {
yield * this.deleteOperation(dep.id)
dep = yield * this.getOperation(dep.id)
this.deleteOperation(dep.id)
dep = this.getOperation(dep.id)
}
dep.gc = true
yield * this.setOperation(dep)
this.setOperation(dep)
this.store.queueGarbageCollector(dep.id)
} else {
yield * this.markGarbageCollected(deps[i], 1)
this.markGarbageCollected(deps[i], 1)
}
}
// remove gc'd op from the left op, if it exists
if (o.left != null) {
var left = yield * this.getInsertion(o.left)
var left = this.getInsertion(o.left)
left.right = o.right
yield * this.setOperation(left)
this.setOperation(left)
}
// remove gc'd op from the right op, if it exists
// also reset origins of right ops
if (o.right != null) {
var right = yield * this.getOperation(o.right)
var right = this.getOperation(o.right)
right.left = o.left
yield * this.setOperation(right)
this.setOperation(right)
if (o.originOf != null && o.originOf.length > 0) {
// find new origin of right ops
@ -478,7 +478,7 @@ export default function extendTransaction (Y) {
right.origin = neworigin
// search until you find origin pointer to the left of o
if (right.right != null) {
var i = yield * this.getOperation(right.right)
var i = this.getOperation(right.right)
var ids = [o.id, o.right]
while (ids.some(function (id) {
return Y.utils.compareIds(id, i.origin)
@ -486,14 +486,14 @@ export default function extendTransaction (Y) {
if (Y.utils.compareIds(i.origin, o.id)) {
// reset origin of i
i.origin = neworigin
yield * this.setOperation(i)
this.setOperation(i)
}
// get next i
if (i.right == null) {
break
} else {
ids.push(i.id)
i = yield * this.getOperation(i.right)
i = this.getOperation(i.right)
}
}
}
@ -502,20 +502,20 @@ export default function extendTransaction (Y) {
// ** Now the new implementation starts **
// reset neworigin of all originOf[*]
for (var _i in o.originOf) {
var originsIn = yield * this.getOperation(o.originOf[_i])
var originsIn = this.getOperation(o.originOf[_i])
if (originsIn != null) {
originsIn.origin = neworigin
yield * this.setOperation(originsIn)
this.setOperation(originsIn)
}
}
if (neworigin != null) {
var neworigin_ = yield * this.getInsertion(neworigin)
var neworigin_ = this.getInsertion(neworigin)
if (neworigin_.originOf == null) {
neworigin_.originOf = o.originOf
} else {
neworigin_.originOf = o.originOf.concat(neworigin_.originOf)
}
yield * this.setOperation(neworigin_)
this.setOperation(neworigin_)
}
// we don't need to set right here, because
// right should be in o.originOf => it is set it the previous for loop
@ -524,15 +524,15 @@ export default function extendTransaction (Y) {
// o may originate in another operation.
// Since o is deleted, we have to reset o.origin's `originOf` property
if (o.origin != null) {
var origin = yield * this.getInsertion(o.origin)
var origin = this.getInsertion(o.origin)
origin.originOf = origin.originOf.filter(function (_id) {
return !Y.utils.compareIds(id, _id)
})
yield * this.setOperation(origin)
this.setOperation(origin)
}
var parent
if (o.parent != null) {
parent = yield * this.getOperation(o.parent)
parent = this.getOperation(o.parent)
}
// remove gc'd op from parent, if it exists
if (parent != null) {
@ -559,38 +559,38 @@ export default function extendTransaction (Y) {
}
}
if (setParent) {
yield * this.setOperation(parent)
this.setOperation(parent)
}
}
// finally remove it from the os
yield * this.removeOperation(o.id)
this.removeOperation(o.id)
}
}
* checkDeleteStoreForState (state) {
var n = yield * this.ds.findWithUpperBound([state.user, state.clock])
checkDeleteStoreForState (state) {
var n = this.ds.findWithUpperBound([state.user, state.clock])
if (n != null && n.id[0] === state.user && n.gc) {
state.clock = Math.max(state.clock, n.id[1] + n.len)
}
}
* updateState (user) {
var state = yield * this.getState(user)
yield * this.checkDeleteStoreForState(state)
var o = yield * this.getInsertion([user, state.clock])
updateState (user) {
var state = this.getState(user)
this.checkDeleteStoreForState(state)
var o = this.getInsertion([user, state.clock])
var oLength = (o != null && o.content != null) ? o.content.length : 1
while (o != null && user === o.id[0] && o.id[1] <= state.clock && o.id[1] + oLength > state.clock) {
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
state.clock += oLength
yield * this.checkDeleteStoreForState(state)
o = yield * this.os.findNext(o.id)
this.checkDeleteStoreForState(state)
o = this.os.findNext(o.id)
oLength = (o != null && o.content != null) ? o.content.length : 1
}
yield * this.setState(state)
this.setState(state)
}
/*
apply a delete set in order to get
the state of the supplied ds
*/
* applyDeleteSet (decoder) {
applyDeleteSet (decoder) {
var deletions = []
let dsLength = decoder.readUint32()
@ -606,7 +606,7 @@ export default function extendTransaction (Y) {
}
var pos = 0
var d = dv[pos]
yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function (n) {
// cases:
// 1. d deletes something to the right of n
// => go to next n (break)
@ -655,14 +655,14 @@ export default function extendTransaction (Y) {
for (var i = 0; i < deletions.length; i++) {
var del = deletions[i]
// always try to delete..
yield * this.deleteOperation([del[0], del[1]], del[2])
this.deleteOperation([del[0], del[1]], del[2])
if (del[3]) {
// gc..
yield * this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd
this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd
// remove operation..
var counter = del[1] + del[2]
while (counter >= del[1]) {
var o = yield * this.os.findWithUpperBound([del[0], counter - 1])
var o = this.os.findWithUpperBound([del[0], counter - 1])
if (o == null) {
break
}
@ -673,14 +673,14 @@ export default function extendTransaction (Y) {
}
if (o.id[1] + oLen > del[1] + del[2]) {
// overlaps right
o = yield * this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1])
o = this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1])
}
if (o.id[1] < del[1]) {
// overlaps left
o = yield * this.getInsertionCleanStart([del[0], del[1]])
o = this.getInsertionCleanStart([del[0], del[1]])
}
counter = o.id[1]
yield * this.garbageCollectOperation(o.id)
this.garbageCollectOperation(o.id)
}
}
if (this.store.forwardAppliedOperations || this.store.y.persistence != null) {
@ -695,16 +695,16 @@ export default function extendTransaction (Y) {
}
}
}
* isGarbageCollected (id) {
var n = yield * this.ds.findWithUpperBound(id)
isGarbageCollected (id) {
var n = this.ds.findWithUpperBound(id)
return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len && n.gc
}
/*
A DeleteSet (ds) describes all the deleted ops in the OS
*/
* writeDeleteSet (encoder) {
writeDeleteSet (encoder) {
var ds = new Map()
yield * this.ds.iterate(this, null, null, function * (n) {
this.ds.iterate(this, null, null, function (n) {
var user = n.id[0]
var counter = n.id[1]
var len = n.len
@ -731,16 +731,16 @@ export default function extendTransaction (Y) {
}
}
}
* isDeleted (id) {
var n = yield * this.ds.findWithUpperBound(id)
isDeleted (id) {
var n = this.ds.findWithUpperBound(id)
return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len
}
* setOperation (op) {
yield * this.os.put(op)
setOperation (op) {
this.os.put(op)
return op
}
* addOperation (op) {
yield * this.os.put(op)
addOperation (op) {
this.os.put(op)
// case op is created by this user, op is already broadcasted in applyCreatedOperations
if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') {
if (this.store.forwardAppliedOperations) {
@ -753,7 +753,7 @@ export default function extendTransaction (Y) {
}
}
// if insertion, try to combine with left insertion (if both have content property)
* tryCombineWithLeft (op) {
tryCombineWithLeft (op) {
if (
op != null &&
op.left != null &&
@ -761,7 +761,7 @@ export default function extendTransaction (Y) {
op.left[0] === op.id[0] &&
Y.utils.compareIds(op.left, op.origin)
) {
var left = yield * this.getInsertion(op.left)
var left = this.getInsertion(op.left)
if (left.content != null &&
left.id[1] + left.content.length === op.id[1] &&
left.originOf.length === 1 &&
@ -776,13 +776,13 @@ export default function extendTransaction (Y) {
}
left.content = left.content.concat(op.content)
left.right = op.right
yield * this.os.delete(op.id)
yield * this.setOperation(left)
this.os.delete(op.id)
this.setOperation(left)
}
}
}
* getInsertion (id) {
var ins = yield * this.os.findWithUpperBound(id)
getInsertion (id) {
var ins = this.os.findWithUpperBound(id)
if (ins == null) {
return null
} else {
@ -794,14 +794,14 @@ export default function extendTransaction (Y) {
}
}
}
* getInsertionCleanStartEnd (id) {
yield * this.getInsertionCleanStart(id)
return yield * this.getInsertionCleanEnd(id)
getInsertionCleanStartEnd (id) {
this.getInsertionCleanStart(id)
return this.getInsertionCleanEnd(id)
}
// Return an insertion such that id is the first element of content
// This function manipulates an operation, if necessary
* getInsertionCleanStart (id) {
var ins = yield * this.getInsertion(id)
getInsertionCleanStart (id) {
var ins = this.getInsertion(id)
if (ins != null) {
if (ins.id[1] === id[1]) {
return ins
@ -815,8 +815,8 @@ export default function extendTransaction (Y) {
left.right = ins.id
ins.left = leftLid
// debugger // check
yield * this.setOperation(left)
yield * this.setOperation(ins)
this.setOperation(left)
this.setOperation(ins)
if (left.gc) {
this.store.queueGarbageCollector(ins.id)
}
@ -828,8 +828,8 @@ export default function extendTransaction (Y) {
}
// Return an insertion such that id is the last element of content
// This function manipulates an operation, if necessary
* getInsertionCleanEnd (id) {
var ins = yield * this.getInsertion(id)
getInsertionCleanEnd (id) {
var ins = this.getInsertion(id)
if (ins != null) {
if (ins.content == null || (ins.id[1] + ins.content.length - 1 === id[1])) {
return ins
@ -843,8 +843,8 @@ export default function extendTransaction (Y) {
ins.right = right.id
right.left = insLid
// debugger // check
yield * this.setOperation(right)
yield * this.setOperation(ins)
this.setOperation(right)
this.setOperation(ins)
if (ins.gc) {
this.store.queueGarbageCollector(right.id)
}
@ -854,8 +854,8 @@ export default function extendTransaction (Y) {
return null
}
}
* getOperation (id/* :any */)/* :Transaction<any> */ {
var o = yield * this.os.find(id)
getOperation (id/* :any */)/* :Transaction<any> */ {
var o = this.os.find(id)
if (id[0] !== 0xFFFFFF || o != null) {
return o
} else { // type is string
@ -870,7 +870,7 @@ export default function extendTransaction (Y) {
}
var op = Y.Struct[struct].create(id, args)
op.type = comp[1]
yield * this.setOperation(op)
this.setOperation(op)
return op
} else {
throw new Error(
@ -880,18 +880,18 @@ export default function extendTransaction (Y) {
}
}
}
* removeOperation (id) {
yield * this.os.delete(id)
removeOperation (id) {
this.os.delete(id)
}
* setState (state) {
setState (state) {
var val = {
id: [state.user],
clock: state.clock
}
yield * this.ss.put(val)
this.ss.put(val)
}
* getState (user) {
var n = yield * this.ss.find([user])
getState (user) {
var n = this.ss.find([user])
var clock = n == null ? null : n.clock
if (clock == null) {
clock = 0
@ -901,9 +901,9 @@ export default function extendTransaction (Y) {
clock: clock
}
}
* getStateVector () {
getStateVector () {
var stateVector = []
yield * this.ss.iterate(this, null, null, function * (n) {
this.ss.iterate(this, null, null, function (n) {
stateVector.push({
user: n.id[0],
clock: n.clock
@ -911,18 +911,18 @@ export default function extendTransaction (Y) {
})
return stateVector
}
* getStateSet () {
getStateSet () {
var ss = {}
yield * this.ss.iterate(this, null, null, function * (n) {
this.ss.iterate(this, null, null, function (n) {
ss[n.id[0]] = n.clock
})
return ss
}
* writeStateSet (encoder) {
writeStateSet (encoder) {
let lenPosition = encoder.pos
let len = 0
encoder.writeUint32(0)
yield * this.ss.iterate(this, null, null, function * (n) {
this.ss.iterate(this, null, null, function (n) {
encoder.writeVarUint(n.id[0])
encoder.writeVarUint(n.clock)
len++
@ -976,14 +976,14 @@ export default function extendTransaction (Y) {
3. Found o = op.origin -> set op.left = op.origin, and send it to the user. start again from 1. (set op = o)
4. Found some o -> set o.right = op, o.left = o.origin, send it to the user, continue
*/
* getOperations (startSS) {
getOperations (startSS) {
// TODO: use bounds here!
if (startSS == null) {
startSS = new Map()
}
var send = []
var endSV = yield * this.getStateVector()
var endSV = this.getStateVector()
for (let endState of endSV) {
let user = endState.user
if (user === 0xFFFFFF) {
@ -993,7 +993,7 @@ export default function extendTransaction (Y) {
if (startPos > 0) {
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
// find out if that is the case
let firstMissing = yield * this.getInsertion([user, startPos])
let firstMissing = this.getInsertion([user, startPos])
if (firstMissing != null) {
// update startPos
startPos = firstMissing.id[1]
@ -1007,7 +1007,7 @@ export default function extendTransaction (Y) {
if (user === 0xFFFFFF) {
continue
}
yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function (op) {
op = Y.Struct[op.struct].encode(op)
if (op.struct !== 'Insert') {
send.push(op)
@ -1038,7 +1038,7 @@ export default function extendTransaction (Y) {
*/
break
}
o = yield * this.getInsertion(o.left)
o = this.getInsertion(o.left)
// we set another o, check if we can reduce $missingOrigins
while (missingOrigins.length > 0 && Y.utils.matchesId(o, missingOrigins[missingOrigins.length - 1].origin)) {
missingOrigins.pop()
@ -1076,7 +1076,7 @@ export default function extendTransaction (Y) {
return send.reverse()
}
* writeOperations (encoder, decoder) {
writeOperations (encoder, decoder) {
let ss = new Map()
let ssLength = decoder.readUint32()
for (let i = 0; i < ssLength; i++) {
@ -1084,7 +1084,7 @@ export default function extendTransaction (Y) {
let clock = decoder.readVarUint()
ss.set(user, clock)
}
let ops = yield * this.getOperations(ss)
let ops = this.getOperations(ss)
encoder.writeUint32(ops.length)
for (let i = 0; i < ops.length; i++) {
let op = ops[i]
@ -1092,17 +1092,17 @@ export default function extendTransaction (Y) {
}
}
* toBinary () {
toBinary () {
let encoder = new BinaryEncoder()
yield * this.writeOperationsUntransformed(encoder)
yield * this.writeDeleteSet(encoder)
this.writeOperationsUntransformed(encoder)
this.writeDeleteSet(encoder)
return encoder.createBuffer()
}
* fromBinary (buffer) {
fromBinary (buffer) {
let decoder = new BinaryDecoder(buffer)
yield * this.applyOperationsUntransformed(decoder)
yield * this.applyDeleteSet(decoder)
this.applyOperationsUntransformed(decoder)
this.applyDeleteSet(decoder)
}
/*
@ -1110,43 +1110,43 @@ export default function extendTransaction (Y) {
* You can apply these operations using .applyOperationsUntransformed(ops)
*
*/
* writeOperationsUntransformed (encoder) {
writeOperationsUntransformed (encoder) {
let lenPosition = encoder.pos
let len = 0
encoder.writeUint32(0) // placeholder
yield * this.os.iterate(this, null, null, function * (op) {
this.os.iterate(this, null, null, function (op) {
if (op.id[0] !== 0xFFFFFF) {
len++
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
}
})
encoder.setUint32(lenPosition, len)
yield * this.writeStateSet(encoder)
this.writeStateSet(encoder)
}
* applyOperationsUntransformed (decoder) {
applyOperationsUntransformed (decoder) {
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let op = Y.Struct.binaryDecodeOperation(decoder)
yield * this.os.put(op)
this.os.put(op)
}
yield * this.os.iterate(this, null, null, function * (op) {
this.os.iterate(this, null, null, function (op) {
if (op.parent != null) {
if (op.struct === 'Insert') {
// update parents .map/start/end properties
if (op.parentSub != null && op.left == null) {
// op is child of Map
let parent = yield * this.getOperation(op.parent)
let parent = this.getOperation(op.parent)
parent.map[op.parentSub] = op.id
yield * this.setOperation(parent)
this.setOperation(parent)
} else if (op.right == null || op.left == null) {
let parent = yield * this.getOperation(op.parent)
let parent = this.getOperation(op.parent)
if (op.right == null) {
parent.end = Y.utils.getLastId(op)
}
if (op.left == null) {
parent.start = op.id
}
yield * this.setOperation(parent)
this.setOperation(parent)
}
}
}
@ -1155,14 +1155,14 @@ export default function extendTransaction (Y) {
for (let i = 0; i < stateSetLength; i++) {
let user = decoder.readVarUint()
let clock = decoder.readVarUint()
yield * this.ss.put({
this.ss.put({
id: [user],
clock: clock
})
}
}
/* this is what we used before.. use this as a reference..
* makeOperationReady (startSS, op) {
makeOperationReady (startSS, op) {
op = Y.Struct[op.struct].encode(op)
op = Y.utils.copyObject(op) -- use copyoperation instead now!
var o = op
@ -1172,7 +1172,7 @@ export default function extendTransaction (Y) {
// or the o that has no origin to the right of op
// (this is why we use the ids array)
while (o.right != null) {
var right = yield * this.getOperation(o.right)
var right = this.getOperation(o.right)
if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) {
return Y.utils.compareIds(id, right.origin)
})) {
@ -1186,10 +1186,10 @@ export default function extendTransaction (Y) {
return op
}
*/
* flush () {
yield * this.os.flush()
yield * this.ss.flush()
yield * this.ds.flush()
flush () {
this.os.flush()
this.ss.flush()
this.ds.flush()
}
}
Y.Transaction = TransactionInterface

View File

@ -315,7 +315,7 @@ export default function Utils (Y) {
this.awaiting++
ops.map(Y.utils.copyOperation).forEach(this.onevent)
}
* awaitOps (transaction, f, args) {
awaitOps (transaction, f, args) {
function notSoSmartSort (array) {
// this function sorts insertions in a executable order
var result = []
@ -339,7 +339,7 @@ export default function Utils (Y) {
}
var before = this.waiting.length
// somehow create new operations
yield * f.apply(transaction, args)
f.apply(transaction, args)
// remove all appended ops / awaited ops
this.waiting.splice(before)
if (this.awaiting > 0) this.awaiting--
@ -349,7 +349,7 @@ export default function Utils (Y) {
for (let i = 0; i < this.waiting.length; i++) {
var o = this.waiting[i]
if (o.struct === 'Insert') {
var _o = yield * transaction.getInsertion(o.id)
var _o = transaction.getInsertion(o.id)
if (_o.parentSub != null && _o.left != null) {
// if o is an insertion of a map struc (parentSub is defined), then it shouldn't be necessary to compute left
this.waiting.splice(i, 1)
@ -361,10 +361,10 @@ export default function Utils (Y) {
o.left = null
} else {
// find next undeleted op
var left = yield * transaction.getInsertion(_o.left)
var left = transaction.getInsertion(_o.left)
while (left.deleted != null) {
if (left.left != null) {
left = yield * transaction.getInsertion(left.left)
left = transaction.getInsertion(left.left)
} else {
left = null
break
@ -690,7 +690,7 @@ export default function Utils (Y) {
this.writeBuffer = createEmptyOpsArray(5)
this.readBuffer = createEmptyOpsArray(10)
}
* find (id, noSuperCall) {
find (id, noSuperCall) {
var i, r
for (i = this.readBuffer.length - 1; i >= 0; i--) {
r = this.readBuffer[i]
@ -716,7 +716,7 @@ export default function Utils (Y) {
if (i < 0 && noSuperCall === undefined) {
// did not reach break in last loop
// read id and put it to the end of readBuffer
o = yield * super.find(id)
o = super.find(id)
}
if (o != null) {
for (i = 0; i < this.readBuffer.length - 1; i++) {
@ -726,7 +726,7 @@ export default function Utils (Y) {
}
return o
}
* put (o) {
put (o) {
var id = o.id
var i, r // helper variables
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
@ -746,7 +746,7 @@ export default function Utils (Y) {
// write writeBuffer[0]
var write = this.writeBuffer[0]
if (write.id[0] !== null) {
yield * super.put(write)
super.put(write)
}
// put o to the end of writeBuffer
for (i = 0; i < this.writeBuffer.length - 1; i++) {
@ -766,7 +766,7 @@ export default function Utils (Y) {
}
this.readBuffer[this.readBuffer.length - 1] = o
}
* delete (id) {
delete (id) {
var i, r
for (i = 0; i < this.readBuffer.length; i++) {
r = this.readBuffer[i]
@ -776,44 +776,44 @@ export default function Utils (Y) {
}
}
}
yield * this.flush()
yield * super.delete(id)
this.flush()
super.delete(id)
}
* findWithLowerBound (id) {
var o = yield * this.find(id, true)
findWithLowerBound (id) {
var o = this.find(id, true)
if (o != null) {
return o
} else {
yield * this.flush()
return yield * super.findWithLowerBound.apply(this, arguments)
this.flush()
return super.findWithLowerBound.apply(this, arguments)
}
}
* findWithUpperBound (id) {
var o = yield * this.find(id, true)
findWithUpperBound (id) {
var o = this.find(id, true)
if (o != null) {
return o
} else {
yield * this.flush()
return yield * super.findWithUpperBound.apply(this, arguments)
this.flush()
return super.findWithUpperBound.apply(this, arguments)
}
}
* findNext () {
yield * this.flush()
return yield * super.findNext.apply(this, arguments)
findNext () {
this.flush()
return super.findNext.apply(this, arguments)
}
* findPrev () {
yield * this.flush()
return yield * super.findPrev.apply(this, arguments)
findPrev () {
this.flush()
return super.findPrev.apply(this, arguments)
}
* iterate () {
yield * this.flush()
yield * super.iterate.apply(this, arguments)
iterate () {
this.flush()
super.iterate.apply(this, arguments)
}
* flush () {
flush () {
for (var i = 0; i < this.writeBuffer.length; i++) {
var write = this.writeBuffer[i]
if (write.id[0] !== null) {
yield * super.put(write)
super.put(write)
this.writeBuffer[i] = {
id: [null, null]
}

64
src/y-memory.js Normal file
View File

@ -0,0 +1,64 @@
import extendRBTree from './RedBlackTree'
export default function extend (Y) {
extendRBTree(Y)
class Transaction extends Y.Transaction {
constructor (store) {
super(store)
this.store = store
this.ss = store.ss
this.os = store.os
this.ds = store.ds
}
}
var Store = Y.utils.RBTree
var BufferedStore = Y.utils.createSmallLookupBuffer(Store)
class Database extends Y.AbstractDatabase {
constructor (y, opts) {
super(y, opts)
this.os = new BufferedStore()
this.ds = new Store()
this.ss = new BufferedStore()
}
logTable () {
var self = this
self.requestTransaction(function () {
console.log('User: ', this.store.y.connector.userId, "==============================") // eslint-disable-line
console.log("State Set (SS):", this.getStateSet()) // eslint-disable-line
console.log("Operation Store (OS):") // eslint-disable-line
this.os.logTable() // eslint-disable-line
console.log("Deletion Store (DS):") //eslint-disable-line
this.ds.logTable() // eslint-disable-line
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
console.warn('GC1|2 not empty!', this.store.gc1, this.store.gc2)
}
if (JSON.stringify(this.store.listenersById) !== '{}') {
console.warn('listenersById not empty!')
}
if (JSON.stringify(this.store.listenersByIdExecuteNow) !== '[]') {
console.warn('listenersByIdExecuteNow not empty!')
}
if (this.store.transactionInProgress) {
console.warn('Transaction still in progress!')
}
}, true)
}
transact (makeGen) {
const t = new Transaction(this)
while (makeGen != null) {
makeGen.call(t)
makeGen = this.getNextRequest()
}
}
destroy () {
super.destroy()
delete this.os
delete this.ss
delete this.ds
}
}
Y.memory = Database
}

View File

@ -4,6 +4,7 @@ import extendDatabase from './Database.js'
import extendTransaction from './Transaction.js'
import extendStruct from './Struct.js'
import extendUtils from './Utils.js'
import extendMemory from './y-memory.js'
import debug from 'debug'
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
@ -13,6 +14,7 @@ extendDatabase(Y)
extendTransaction(Y)
extendStruct(Y)
extendUtils(Y)
extendMemory(Y)
Y.debug = debug
debug.formatters.Y = formatYjsMessage
@ -182,7 +184,7 @@ class YConfig extends Y.utils.NamedEventHandler {
var opts = this.options
var share = {}
this.share = share
this.db.requestTransaction(function * requestTransaction () {
this.db.requestTransaction(function requestTransaction () {
// create shared object
for (var propertyname in opts.share) {
var typeConstructor = opts.share[propertyname].split('(')
@ -195,7 +197,7 @@ class YConfig extends Y.utils.NamedEventHandler {
var typedef = type.typeDefinition
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeArgs]
let args = Y.utils.parseTypeDefinition(type, typeArgs)
share[propertyname] = yield * this.store.initType.call(this, id, args)
share[propertyname] = this.store.initType.call(this, id, args)
}
})
if (this.persistence != null) {
@ -250,8 +252,8 @@ class YConfig extends Y.utils.NamedEventHandler {
return this.db.whenTransactionsFinished().then(function () {
self.db.destroyTypes()
// make sure to wait for all transactions before destroying the db
self.db.requestTransaction(function * () {
yield * self.db.destroy()
self.db.requestTransaction(function () {
self.db.destroy()
})
return self.db.whenTransactionsFinished()
})

View File

@ -161,7 +161,9 @@ test('encode/decode List operations', async function binList (t) {
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array'
type: 'Array',
start: null,
end: null
})
})

217
test/red-black-tree.js Normal file
View File

@ -0,0 +1,217 @@
import Y from '../src/y.js'
import Chance from 'chance'
import { test, proxyConsole } from 'cutest'
proxyConsole()
var numberOfRBTreeTests = 10000
function checkRedNodesDoNotHaveBlackChildren (t, tree) {
let correct = true
function traverse (n) {
if (n == null) {
return
}
if (n.isRed()) {
if (n.left != null) {
correct = correct && !n.left.isRed()
}
if (n.right != null) {
correct = correct && !n.right.isRed()
}
}
traverse(n.left)
traverse(n.right)
}
traverse(tree.root)
t.assert(correct, 'Red nodes do not have black children')
}
function checkBlackHeightOfSubTreesAreEqual (t, tree) {
let correct = true
function traverse (n) {
if (n == null) {
return 0
}
var sub1 = traverse(n.left)
var sub2 = traverse(n.right)
if (sub1 !== sub2) {
correct = false
}
if (n.isRed()) {
return sub1
} else {
return sub1 + 1
}
}
traverse(tree.root)
t.assert(correct, 'Black-height of sub-trees are equal')
}
function checkRootNodeIsBlack (t, tree) {
t.assert(tree.root == null || tree.root.isBlack(), 'root node is black')
}
test('RedBlack Tree', async function redBlackTree (t) {
let memory = new Y.memory(null, { // eslint-disable-line
name: 'Memory',
gcTimeout: -1
})
let tree = memory.os
memory.requestTransaction(function () {
tree.put({id: [8433]})
tree.put({id: [12844]})
tree.put({id: [1795]})
tree.put({id: [30302]})
tree.put({id: [64287]})
tree.delete([8433])
tree.put({id: [28996]})
tree.delete([64287])
tree.put({id: [22721]})
})
await memory.whenTransactionsFinished()
checkRootNodeIsBlack(t, tree)
checkBlackHeightOfSubTreesAreEqual(t, tree)
checkRedNodesDoNotHaveBlackChildren(t, tree)
})
test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
let chance = new Chance(t.getSeed() * 1000000000)
let memory = new Y.memory(null, { // eslint-disable-line
name: 'Memory',
gcTimeout: -1
})
let tree = memory.os
let elements = []
memory.requestTransaction(function () {
for (var i = 0; i < numberOfRBTreeTests; i++) {
if (chance.bool({likelihood: 80})) {
// 80% chance to insert an element
let obj = [chance.integer({min: 0, max: numberOfRBTreeTests})]
let nodeExists = tree.find(obj)
if (!nodeExists) {
if (elements.some(e => e[0] === obj[0])) {
t.assert(false, 'tree and elements contain different results')
}
elements.push(obj)
tree.put({id: obj})
}
} else if (elements.length > 0) {
// ~20% chance to delete an element
var elem = chance.pickone(elements)
elements = elements.filter(function (e) {
return !Y.utils.compareIds(e, elem)
})
tree.delete(elem)
}
}
})
await memory.whenTransactionsFinished()
checkRootNodeIsBlack(t, tree)
checkBlackHeightOfSubTreesAreEqual(t, tree)
checkRedNodesDoNotHaveBlackChildren(t, tree)
memory.requestTransaction(function () {
let allNodesExist = true
for (let id of elements) {
let node = tree.find(id)
if (!Y.utils.compareIds(node.id, id)) {
allNodesExist = false
}
}
t.assert(allNodesExist, 'All inserted nodes exist')
})
memory.requestTransaction(function () {
let findAllNodesWithLowerBoundSerach = true
for (let id of elements) {
let node = tree.findWithLowerBound(id)
if (!Y.utils.compareIds(node.id, id)) {
findAllNodesWithLowerBoundSerach = false
}
}
t.assert(
findAllNodesWithLowerBoundSerach,
'Find every object with lower bound search'
)
})
memory.requestTransaction(function () {
let lowerBound = chance.pickone(elements)
let expectedResults = elements.filter((e, pos) =>
(Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
elements.indexOf(e) === pos
).length
let actualResults = 0
tree.iterate(this, lowerBound, null, function (val) {
if (val == null) {
t.assert(false, 'val is undefined!')
}
actualResults++
})
t.assert(
expectedResults === actualResults,
'Iterating over a tree with lower bound yields the right amount of results'
)
})
memory.requestTransaction(function () {
let expectedResults = elements.filter((e, pos) =>
elements.indexOf(e) === pos
).length
let actualResults = 0
tree.iterate(this, null, null, function (val) {
if (val == null) {
t.assert(false, 'val is undefined!')
}
actualResults++
})
t.assert(
expectedResults === actualResults,
'iterating over a tree without bounds yields the right amount of results'
)
})
memory.requestTransaction(function () {
let upperBound = chance.pickone(elements)
let expectedResults = elements.filter((e, pos) =>
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) &&
elements.indexOf(e) === pos
).length
let actualResults = 0
tree.iterate(this, null, upperBound, function (val) {
if (val == null) {
t.assert(false, 'val is undefined!')
}
actualResults++
})
t.assert(
expectedResults === actualResults,
'iterating over a tree with upper bound yields the right amount of results'
)
})
memory.requestTransaction(function () {
let upperBound = chance.pickone(elements)
let lowerBound = chance.pickone(elements)
if (Y.utils.smaller(upperBound, lowerBound)) {
[lowerBound, upperBound] = [upperBound, lowerBound]
}
let expectedResults = elements.filter((e, pos) =>
(Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) &&
elements.indexOf(e) === pos
).length
let actualResults = 0
tree.iterate(this, lowerBound, upperBound, function (val) {
if (val == null) {
t.assert(false, 'val is undefined!')
}
actualResults++
})
t.assert(
expectedResults === actualResults,
'iterating over a tree with upper bound yields the right amount of results'
)
})
await memory.whenTransactionsFinished()
})

View File

@ -4,7 +4,7 @@ import _Y from '../../yjs/src/y.js'
import yMemory from '../../y-memory/src/y-memory.js'
import yArray from '../../y-array/src/y-array.js'
import yText from '../../y-text/src/Text.js'
import yMap from '../../y-map/src/Map.js'
import yMap from '../../y-map/src/y-map.js'
import yXml from '../../y-xml/src/y-xml.js'
import yTest from './test-connector.js'
@ -17,9 +17,9 @@ Y.extend(yMemory, yArray, yText, yMap, yTest, yXml)
export var database = { name: 'memory' }
export var connector = { name: 'test', url: 'http://localhost:1234' }
function * getStateSet () {
function getStateSet () {
var ss = {}
yield * this.ss.iterate(this, null, null, function * (n) {
this.ss.iterate(this, null, null, function (n) {
var user = n.id[0]
var clock = n.clock
ss[user] = clock
@ -27,9 +27,9 @@ function * getStateSet () {
return ss
}
function * getDeleteSet () {
function getDeleteSet () {
var ds = {}
yield * this.ds.iterate(this, null, null, function * (n) {
this.ds.iterate(this, null, null, function (n) {
var user = n.id[0]
var counter = n.id[1]
var len = n.len
@ -126,9 +126,9 @@ export async function compareUsers (t, users) {
let filterDeletedOps = users.every(u => u.db.gc === false)
var data = await Promise.all(users.map(async (u) => {
var data = {}
u.db.requestTransaction(function * () {
u.db.requestTransaction(function () {
let ops = []
yield * this.os.iterate(this, null, null, function * (op) {
this.os.iterate(this, null, null, function (op) {
ops.push(Y.Struct[op.struct].encode(op))
})
@ -142,7 +142,7 @@ export async function compareUsers (t, users) {
as they might have been split up differently..
*/
if (filterDeletedOps) {
let opIsDeleted = yield * this.isDeleted(op.id)
let opIsDeleted = this.isDeleted(op.id)
if (!opIsDeleted) {
data.os[JSON.stringify(op.id)] = op
}
@ -150,8 +150,8 @@ export async function compareUsers (t, users) {
data.os[JSON.stringify(op.id)] = op
}
}
data.ds = yield * getDeleteSet.apply(this)
data.ss = yield * getStateSet.apply(this)
data.ds = getDeleteSet.apply(this)
data.ss = getStateSet.apply(this)
})
await u.db.whenTransactionsFinished()
return data