added comments to most of the classes.

This commit is contained in:
Kevin Jahns 2015-09-17 00:21:01 +02:00
parent 6f3a291ef5
commit b1d70ef25e
16 changed files with 448 additions and 282 deletions

5
.gitignore vendored
View File

@ -3,11 +3,10 @@ bower_components
build build
build_test build_test
.directory .directory
.c9
.codio .codio
.settings .settings
.jshintignore .jshintignore
.jshintrc .jshintrc
.validate.json .validate.json
y.js ./y.js
y.js.map ./y.js.map

View File

@ -41,18 +41,18 @@
}, },
"homepage": "http://y-js.org", "homepage": "http://y-js.org",
"devDependencies": { "devDependencies": {
"babel-eslint": "^4.1.1", "babel-eslint": "^4.1.2",
"gulp": "^3.9.0", "gulp": "^3.9.0",
"gulp-babel": "^5.1.0", "gulp-babel": "^5.2.1",
"gulp-concat": "^2.5.2", "gulp-concat": "^2.6.0",
"gulp-jasmine": "^2.0.1", "gulp-jasmine": "^2.0.1",
"gulp-jasmine-browser": "^0.1.3", "gulp-jasmine-browser": "^0.2.3",
"gulp-sourcemaps": "^1.5.2", "gulp-sourcemaps": "^1.5.2",
"gulp-uglify": "^1.2.0", "gulp-uglify": "^1.4.1",
"gulp-util": "^3.0.5", "gulp-util": "^3.0.6",
"gulp-watch": "^4.2.4", "gulp-watch": "^4.3.5",
"minimist": "^1.1.1", "minimist": "^1.2.0",
"pre-commit": "^1.0.10", "pre-commit": "^1.1.1",
"standard": "^5.2.2" "standard": "^5.2.2"
} }
} }

View File

@ -1,22 +0,0 @@
{
"spec_dir": "build",
"spec_files": [
"**/**.spec.js"
],
"helpers": [
"Helper.spec.js",
"y.js",
"Connector.js",
"OperationStore.js",
"Struct.js",
"Utils.js",
"OperationStores/RedBlackTree.js",
"OperationStores/Memory.js",
"OperationStores/IndexedDB.js",
"Connectors/Test.js",
"Connectors/WebRTC.js",
"Types/Array.js",
"Types/Map.js",
"Types/TextBind.js"
]
}

View File

@ -1,11 +1,12 @@
/* globals Y */ /* globals Y */
'use strict' 'use strict'
class AbstractConnector { // eslint-disable-line no-unused-vars class AbstractConnector {
/* /*
opts opts contains the following information:
.role : String Role of this client ("master" or "slave") role : String Role of this client ("master" or "slave")
.userId : String that uniquely defines the user. userId : String Uniquely defines the user.
debug: Boolean Whether to print debug messages (optional)
*/ */
constructor (y, opts) { constructor (y, opts) {
this.y = y this.y = y
@ -90,8 +91,11 @@ class AbstractConnector { // eslint-disable-line no-unused-vars
this.whenSyncedListeners.push(f) this.whenSyncedListeners.push(f)
} }
} }
// returns false, if there is no sync target /*
// true otherwise
returns false, if there is no sync target
true otherwise
*/
findNextSyncTarget () { findNextSyncTarget () {
if (this.currentSyncTarget != null) { if (this.currentSyncTarget != null) {
return // "The current sync has not finished!" return // "The current sync has not finished!"
@ -115,7 +119,7 @@ class AbstractConnector { // eslint-disable-line no-unused-vars
}) })
}) })
} }
// set the state to synced! // This user synced with at least one user, set the state to synced (TODO: does this suffice?)
if (!this.isSynced) { if (!this.isSynced) {
this.isSynced = true this.isSynced = true
for (var f of this.whenSyncedListeners) { for (var f of this.whenSyncedListeners) {
@ -129,7 +133,9 @@ class AbstractConnector { // eslint-disable-line no-unused-vars
console.log(`me -> ${uid}: ${message.type}`, m);// eslint-disable-line console.log(`me -> ${uid}: ${message.type}`, m);// eslint-disable-line
} }
} }
// You received a raw message, and you know that it is intended for to Yjs. Then call this function. /*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
receiveMessage (sender, m) { receiveMessage (sender, m) {
if (sender === this.userId) { if (sender === this.userId) {
return return

View File

@ -83,26 +83,26 @@ class Test extends Y.AbstractConnector {
} }
} }
flushAll () { flushAll () {
var def = Promise.defer() return new Promise(function (resolve) {
// flushes may result in more created operations, // flushes may result in more created operations,
// flush until there is nothing more to flush // flush until there is nothing more to flush
function nextFlush () { function nextFlush () {
var c = flushOne() var c = flushOne()
if (c) { if (c) {
while (flushOne()) { while (flushOne()) {
// nop // nop
}
wait().then(nextFlush)
} else {
wait().then(function () {
resolve()
})
} }
wait().then(nextFlush)
} else {
wait().then(function () {
def.resolve()
})
} }
} // in the case that there are
// in the case that there are // still actions that want to be performed
// still actions that want to be performed wait(0).then(nextFlush)
wait(0).then(nextFlush) })
return def.promise
} }
flushOne () { flushOne () {
flushOne() flushOne()

View File

@ -3,8 +3,9 @@
/* /*
This is just a compilation of functions that help to test this library! This is just a compilation of functions that help to test this library!
***/ */
// When testing, you store everything on the global object. We call it g
var g var g
if (typeof global !== 'undefined') { if (typeof global !== 'undefined') {
g = global g = global
@ -15,20 +16,29 @@ if (typeof global !== 'undefined') {
} }
g.g = g g.g = g
g.YConcurrency_TestingMode = true
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000
/*
Wait for a specified amount of time (in ms). defaults to 5ms
*/
function wait (t) { function wait (t) {
if (t == null) { if (t == null) {
t = 5 t = 5
} }
var def = Promise.defer() return new Promise(function (resolve) {
setTimeout(function () { setTimeout(function () {
def.resolve() resolve()
}, t) }, t)
return def.promise })
} }
g.wait = wait g.wait = wait
// returns a random element of o /*
// works on Object, and Array returns a random element of o.
works on Object, and Array
*/
function getRandom (o) { function getRandom (o) {
if (o instanceof Array) { if (o instanceof Array) {
return o[Math.floor(Math.random() * o.length)] return o[Math.floor(Math.random() * o.length)]
@ -42,7 +52,7 @@ function getRandom (o) {
} }
g.getRandom = getRandom g.getRandom = getRandom
function getRandomNumber(n) {//eslint-disable-line function getRandomNumber (n) {
if (n == null) { if (n == null) {
n = 9999 n = 9999
} }
@ -50,7 +60,7 @@ function getRandomNumber(n) {//eslint-disable-line
} }
g.getRandomNumber = getRandomNumber g.getRandomNumber = getRandomNumber
g.applyRandomTransactions = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) { //eslint-disable-line g.applyRandomTransactions = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
function randomTransaction (root) { function randomTransaction (root) {
var f = getRandom(transactions) var f = getRandom(transactions)
f(root) f(root)
@ -86,8 +96,12 @@ g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
}) })
g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-line g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-line
var s1, s2, ds1, ds2, allDels1, allDels2 var s1, s2 // state sets
var db1 = [] var ds1, ds2 // delete sets
var allDels1, allDels2 // all deletions
var db1 = [] // operation store of user1
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
function * t1 () { function * t1 () {
s1 = yield* this.getStateSet() s1 = yield* this.getStateSet()
ds1 = yield* this.getDeleteSet() ds1 = yield* this.getDeleteSet()
@ -105,6 +119,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-
}) })
} }
yield users[0].connector.flushAll() yield users[0].connector.flushAll()
// gc two times because of the two gc phases (really collect everything)
yield g.garbageCollectAllUsers(users) yield g.garbageCollectAllUsers(users)
yield wait(50) yield wait(50)
yield g.garbageCollectAllUsers(users) yield g.garbageCollectAllUsers(users)
@ -137,7 +152,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-
if (s1 == null) { if (s1 == null) {
u.db.requestTransaction(t1) u.db.requestTransaction(t1)
yield wait() yield wait()
u.db.os.iterate(null, null, function(o){//eslint-disable-line u.db.os.iterate(null, null, function (o) {
db1.push(o) db1.push(o)
}) })
} else { } else {
@ -147,22 +162,22 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-
expect(allDels1).toEqual(allDels2) // inner structure expect(allDels1).toEqual(allDels2) // inner structure
expect(ds1).toEqual(ds2) // exported structure expect(ds1).toEqual(ds2) // exported structure
var count = 0 var count = 0
u.db.os.iterate(null, null, function(o){//eslint-disable-line u.db.os.iterate(null, null, function (o) {
expect(db1[count++]).toEqual(o) expect(db1[count++]).toEqual(o)
}) })
} }
} }
}) })
g.createUsers = async(function * createUsers (self, numberOfUsers) { //eslint-disable-line g.createUsers = async(function * createUsers (self, numberOfUsers) {
if (Y.utils.globalRoom.users[0] != null) {//eslint-disable-line if (Y.utils.globalRoom.users[0] != null) {
yield Y.utils.globalRoom.users[0].flushAll()//eslint-disable-line yield Y.utils.globalRoom.users[0].flushAll()
} }
// destroy old users // destroy old users
for (var u in Y.utils.globalRoom.users) {//eslint-disable-line for (var u in Y.utils.globalRoom.users) {
Y.utils.globalRoom.users[u].y.destroy()//eslint-disable-line Y.utils.globalRoom.users[u].y.destroy()
} }
self.users = [] self.users = null
var promises = [] var promises = []
for (var i = 0; i < numberOfUsers; i++) { for (var i = 0; i < numberOfUsers; i++) {
@ -178,14 +193,18 @@ g.createUsers = async(function * createUsers (self, numberOfUsers) { //eslint-di
})) }))
} }
self.users = yield Promise.all(promises) self.users = yield Promise.all(promises)
return self.users
}) })
/*
Until async/await arrives in js, we use this function to wait for promises
by yielding them.
*/
function async (makeGenerator) { function async (makeGenerator) {
return function (arg) { return function (arg) {
var generator = makeGenerator.apply(this, arguments) var generator = makeGenerator.apply(this, arguments)
function handle (result) { function handle (result) {
// result => { done: [Boolean], value: [Object] }
if (result.done) return Promise.resolve(result.value) if (result.done) return Promise.resolve(result.value)
return Promise.resolve(result.value).then(function (res) { return Promise.resolve(result.value).then(function (res) {
@ -194,12 +213,14 @@ function async (makeGenerator) {
return handle(generator.throw(err)) return handle(generator.throw(err))
}) })
} }
// this may throw errors here, but its ok since this is used only for debugging
try { return handle(generator.next())
/* try {
return handle(generator.next()) return handle(generator.next())
} catch (ex) { } catch (ex) {
return Promise.reject(ex) generator.throw(ex) // TODO: check this out
} // return Promise.reject(ex)
}*/
} }
} }
g.async = async g.async = async

View File

@ -1,7 +1,76 @@
/* global Y */ /* global Y */
'use strict' 'use strict'
/*
Partial definition of a transaction
By convention, a transaction has the following properties:
* ss for StateSet
* os for OperationStore
* ds for DeleteStore
A transaction must also define the following methods:
* checkDeleteStoreForState(state)
- When increasing the state of a user, an operation with an higher id
may already be garbage collected, and therefore it will never be received.
update the state to reflect this knowledge. This won't call a method to save the state!
* getDeleteSet(id)
- Get the delete set in a readable format:
{
"userX": [
[5,1], // starting from position 5, one operations is deleted
[9,4] // starting from position 9, four operations are deleted
],
"userY": ...
}
* isDeleted(id)
* getOpsFromDeleteSet(ds) -- TODO: just call Struct.Delete.delete(id) here
- get a set of deletions that need to be applied in order to get to
achieve the state of the supplied ds
* setOperation(op)
- write `op` to the database.
Note: this is allowed to return an in-memory object.
E.g. the Memory adapter returns the object that it has in-memory.
Changing values on this object will be stored directly in the database
without calling this function. Therefore,
setOperation may have no functionality in some adapters. This also has
implications on the way we use operations that were served from the database.
We try not to call copyObject, if not necessary.
* addOperation(op)
- add an operation to the database.
This may only be called once for every op.id
* getOperation(id)
* removeOperation(id)
- remove an operation from the database. This is called when an operation
is garbage collected.
* setState(state)
- `state` is of the form
{
user: "1",
clock: 4
} <- meaning that we have four operations from user "1"
(with these id's respectively: 0, 1, 2, and 3)
* getState(user)
* getStateVector()
- Get the state of the OS in the form
[{
user: "userX",
clock: 11
},
..
]
* getStateSet()
- Get the state of the OS in the form
{
"userX": 11,
"userY": 22
}
* getOperations(startSS)
- Get the all the operations that are necessary in order to achive the
stateSet of this user, starting from a stateSet supplied by another user
* makeOperationReady(ss, op)
- this is called only by `getOperations(startSS)`. It makes an operation
applyable on a given SS.
*/
class AbstractTransaction { class AbstractTransaction {
constructor (store) { constructor (store) {
this.store = store this.store = store
@ -41,6 +110,18 @@ 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 { // eslint-disable-line no-unused-vars class AbstractOperationStore { // eslint-disable-line no-unused-vars
constructor (y, opts) { constructor (y, opts) {
this.y = y this.y = y
@ -71,44 +152,44 @@ class AbstractOperationStore { // eslint-disable-line no-unused-vars
this.gcTimeout = opts.gcTimeout || 5000 this.gcTimeout = opts.gcTimeout || 5000
var os = this var os = this
function garbageCollect () { function garbageCollect () {
var def = Promise.defer() return new Promise((resolve) => {
os.requestTransaction(function * () { os.requestTransaction(function * () {
for (var i in os.gc2) { for (var i in os.gc2) {
var oid = os.gc2[i] var oid = os.gc2[i]
var o = yield* this.getOperation(oid) var o = yield* this.getOperation(oid)
if (o.left != null) { if (o.left != null) {
var left = yield* this.getOperation(o.left) var left = yield* this.getOperation(o.left)
left.right = o.right left.right = o.right
yield* this.setOperation(left) yield* this.setOperation(left)
}
if (o.right != null) {
var right = yield* this.getOperation(o.right)
right.left = o.left
yield* this.setOperation(right)
}
var parent = yield* this.getOperation(o.parent)
var setParent = false
if (Y.utils.compareIds(parent.start, o.id)) {
setParent = true
parent.start = o.right
}
if (Y.utils.compareIds(parent.end, o.id)) {
setParent = true
parent.end = o.left
}
if (setParent) {
yield* this.setOperation(parent)
}
yield* this.removeOperation(o.id)
} }
if (o.right != null) { os.gc2 = os.gc1
var right = yield* this.getOperation(o.right) os.gc1 = []
right.left = o.left if (os.gcTimeout > 0) {
yield* this.setOperation(right) os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
} }
var parent = yield* this.getOperation(o.parent) resolve()
var setParent = false })
if (Y.utils.compareIds(parent.start, o.id)) {
setParent = true
parent.start = o.right
}
if (Y.utils.compareIds(parent.end, o.id)) {
setParent = true
parent.end = o.left
}
if (setParent) {
yield* this.setOperation(parent)
}
yield* this.removeOperation(o.id)
}
os.gc2 = os.gc1
os.gc1 = []
if (os.gcTimeout > 0) {
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
}
def.resolve()
}) })
return def.promise
} }
this.garbageCollect = garbageCollect this.garbageCollect = garbageCollect
if (this.gcTimeout > 0) { if (this.gcTimeout > 0) {

View File

@ -1,15 +1,6 @@
/* global Y */ /* global Y */
'use strict' 'use strict'
function copyObject (o) {
var c = {}
for (var key in o) {
c[key] = o[key]
}
return c
}
Y.utils.copyObject = copyObject
class DeleteStore extends Y.utils.RBTree { class DeleteStore extends Y.utils.RBTree {
constructor () { constructor () {
super() super()
@ -120,8 +111,8 @@ class DeleteStore extends Y.utils.RBTree {
Y.utils.DeleteStore = DeleteStore Y.utils.DeleteStore = DeleteStore
Y.Memory = (function () { // eslint-disable-line no-unused-vars Y.Memory = (function () {
class Transaction extends Y.AbstractTransaction { // eslint-disable-line class Transaction extends Y.AbstractTransaction {
constructor (store) { constructor (store) {
super(store) super(store)
@ -144,28 +135,25 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars
* getOpsFromDeleteSet (ds) { * getOpsFromDeleteSet (ds) {
return this.ds.getDeletions(ds) return this.ds.getDeletions(ds)
} }
* setOperation (op) { // eslint-disable-line * setOperation (op) {
// TODO: you can remove this step! probs.. // TODO: you can remove this step! probs..
var n = this.os.findNode(op.id) var n = this.os.findNode(op.id)
n.val = op n.val = op
return op return op
} }
* addOperation (op) { // eslint-disable-line * addOperation (op) {
this.os.add(op) this.os.add(op)
} }
* getOperation (id) { // eslint-disable-line * getOperation (id) {
if (id == null) {
throw new Error('You must define id!')
}
return this.os.find(id) return this.os.find(id)
} }
* removeOperation (id) { // eslint-disable-line * removeOperation (id) {
this.os.delete(id) this.os.delete(id)
} }
* setState (state) { // eslint-disable-line * setState (state) {
this.ss[state.user] = state.clock this.ss[state.user] = state.clock
} }
* getState (user) { // eslint-disable-line * getState (user) {
var clock = this.ss[user] var clock = this.ss[user]
if (clock == null) { if (clock == null) {
clock = 0 clock = 0
@ -175,7 +163,7 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars
clock: clock clock: clock
} }
} }
* getStateVector () { // eslint-disable-line * getStateVector () {
var stateVector = [] var stateVector = []
for (var user in this.ss) { for (var user in this.ss) {
var clock = this.ss[user] var clock = this.ss[user]
@ -186,7 +174,7 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars
} }
return stateVector return stateVector
} }
* getStateSet () { // eslint-disable-line * getStateSet () {
return this.ss return this.ss
} }
* getOperations (startSS) { * getOperations (startSS) {
@ -225,7 +213,7 @@ Y.Memory = (function () { // eslint-disable-line no-unused-vars
} }
* makeOperationReady (ss, op) { * makeOperationReady (ss, op) {
// instead of ss, you could use currSS (a ss that increments when you add an operation) // instead of ss, you could use currSS (a ss that increments when you add an operation)
op = copyObject(op) op = Y.utils.copyObject(op)
var o = op var o = op
var clock var clock
while (o.right != null) { while (o.right != null) {

View File

@ -1,10 +1,9 @@
/* global Y */ /* global Y */
'use strict' 'use strict'
function smaller (a, b) { /*
return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]) This file contains a not so fancy implemantion of a Red Black Tree.
} */
Y.utils.smaller = smaller
class N { class N {
// A created node is always red! // A created node is always red!
@ -126,7 +125,7 @@ class N {
} }
} }
class RBTree { // eslint-disable-line no-unused-vars class RBTree {
constructor () { constructor () {
this.root = null this.root = null
this.length = 0 this.length = 0
@ -140,11 +139,11 @@ class RBTree { // eslint-disable-line no-unused-vars
return null return null
} else { } else {
while (true) { while (true) {
if ((from === null || smaller(from, o.val.id)) && o.left !== null) { if ((from === null || Y.utils.smaller(from, o.val.id)) && o.left !== null) {
// o is included in the bound // o is included in the bound
// try to find an element that is closer to the bound // try to find an element that is closer to the bound
o = o.left o = o.left
} else if (from !== null && smaller(o.val.id, from)) { } else if (from !== null && Y.utils.smaller(o.val.id, from)) {
// o is not within the bound, maybe one of the right elements is.. // o is not within the bound, maybe one of the right elements is..
if (o.right !== null) { if (o.right !== null) {
o = o.right o = o.right
@ -168,11 +167,11 @@ class RBTree { // eslint-disable-line no-unused-vars
return null return null
} else { } else {
while (true) { while (true) {
if ((to === null || smaller(o.val.id, to)) && o.right !== null) { if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) {
// o is included in the bound // o is included in the bound
// try to find an element that is closer to the bound // try to find an element that is closer to the bound
o = o.right o = o.right
} else if (to !== null && smaller(to, o.val.id)) { } else if (to !== null && Y.utils.smaller(to, o.val.id)) {
// o is not within the bound, maybe one of the left elements is.. // o is not within the bound, maybe one of the left elements is..
if (o.left !== null) { if (o.left !== null) {
o = o.left o = o.left
@ -189,7 +188,7 @@ class RBTree { // eslint-disable-line no-unused-vars
} }
iterate (from, to, f) { iterate (from, to, f) {
var o = this.findNodeWithLowerBound(from) var o = this.findNodeWithLowerBound(from)
while (o !== null && (to === null || smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) { while (o !== null && (to === null || Y.utils.smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) {
f(o.val) f(o.val)
o = o.next() o = o.next()
} }
@ -226,9 +225,9 @@ class RBTree { // eslint-disable-line no-unused-vars
if (o === null) { if (o === null) {
return false return false
} }
if (smaller(id, o.val.id)) { if (Y.utils.smaller(id, o.val.id)) {
o = o.left o = o.left
} else if (smaller(o.val.id, id)) { } else if (Y.utils.smaller(o.val.id, id)) {
o = o.right o = o.right
} else { } else {
return o return o
@ -386,14 +385,14 @@ class RBTree { // eslint-disable-line no-unused-vars
if (this.root !== null) { if (this.root !== null) {
var p = this.root // p abbrev. parent var p = this.root // p abbrev. parent
while (true) { while (true) {
if (smaller(node.val.id, p.val.id)) { if (Y.utils.smaller(node.val.id, p.val.id)) {
if (p.left === null) { if (p.left === null) {
p.left = node p.left = node
break break
} else { } else {
p = p.left p = p.left
} }
} else if (smaller(p.val.id, node.val.id)) { } else if (Y.utils.smaller(p.val.id, node.val.id)) {
if (p.right === null) { if (p.right === null) {
p.right = node p.right = node
break break

View File

@ -1,25 +1,31 @@
/* global Y */ /* global Y */
'use strict' 'use strict'
function compareIds (id1, id2) { /*
if (id1 == null || id2 == null) { An operation also defines the structure of a type. This is why operation and
if (id1 == null && id2 == null) { structure are used interchangeably here.
return true
} It must be of the type Object. I hope to achieve some performance
return false improvements when working on databases that support the json format.
}
if (id1[0] === id2[0] && id1[1] === id2[1]) { An operation must have the following properties:
return true
} else { * encode
return false - Encode the structure in a readable format (preferably string- todo)
} * decode (todo)
} - decode structure to json
Y.utils.compareIds = compareIds * execute
- Execute the semantics of an operation.
* requiredOps
- Operations that are required to execute this operation.
*/
var Struct = { var Struct = {
/* This Operations does _not_ have an id! /* This is the only operation that is actually not a structure, because
{ it is not stored in the OS. This is why it _does not_ have an id
target: Id
op = {
target: Id
} }
*/ */
Delete: { Delete: {
@ -29,16 +35,18 @@ var Struct = {
requiredOps: function (op) { requiredOps: function (op) {
return [op.target] return [op.target]
}, },
execute: function * (op) { /*
// console.log('Delete', op, console.trace()) Delete an operation from the OS, and add it to the GC, if necessary.
var target = yield* this.getOperation(op.target) */
delete: function * (targetId) {
var target = yield* this.getOperation(targetId)
if (target != null && !target.deleted) { if (target != null && !target.deleted) {
target.deleted = true target.deleted = true
if (target.left !== null && (yield* this.getOperation(target.left)).deleted) { if (target.left != null && (yield* this.getOperation(target.left)).deleted) {
this.store.addToGarbageCollector(target.id) this.store.addToGarbageCollector(target.id)
target.gc = true target.gc = true
} }
if (target.right !== null) { if (target.right != null) {
var right = yield* this.getOperation(target.right) var right = yield* this.getOperation(target.right)
if (right.deleted && right.gc == null) { if (right.deleted && right.gc == null) {
this.store.addToGarbageCollector(right.id) this.store.addToGarbageCollector(right.id)
@ -49,15 +57,21 @@ var Struct = {
yield* this.setOperation(target) yield* this.setOperation(target)
var t = this.store.initializedTypes[JSON.stringify(target.parent)] var t = this.store.initializedTypes[JSON.stringify(target.parent)]
if (t != null) { if (t != null) {
yield* t._changed(this, Y.utils.copyObject(op)) yield* t._changed(this, {
struct: 'Delete',
target: targetId
})
} }
} }
this.ds.delete(op.target) this.ds.delete(targetId)
var state = yield* this.getState(op.target[0]) var state = yield* this.getState(targetId[0])
if (state.clock === op.target[1]) { if (state.clock === targetId[1]) {
yield* this.checkDeleteStoreForState(state) yield* this.checkDeleteStoreForState(state)
yield* this.setState(state) yield* this.setState(state)
} }
},
execute: function * (op) {
yield* Struct.Delete.delete.call(this, op.target)
} }
}, },
Insert: { Insert: {
@ -65,27 +79,15 @@ var Struct = {
content: any, content: any,
left: Id, left: Id,
right: Id, right: Id,
origin: id, origin: Id,
parent: Id, parent: Id,
parentSub: string (optional), parentSub: string (optional), // child of Map type
id: this.os.getNextOpId() id: Id
} }
*/ */
encode: function (op) { encode: function (op) {
/* bad idea, right? // TODO: you could not send the "left" property, then you also have to
var e = { // "op.left = null" in $execute or $decode
id: op.id,
left: op.left,
right: op.right,
origin: op.origin,
parent: op.parent,
content: op.content,
struct: "Insert"
}
if (op.parentSub != null){
e.parentSub = op.parentSub
}
return e;*/
return op return op
}, },
requiredOps: function (op) { requiredOps: function (op) {
@ -96,7 +98,7 @@ var Struct = {
if (op.right != null) { if (op.right != null) {
ids.push(op.right) ids.push(op.right)
} }
// if(op.right == null && op.left == null) {} // if (op.right == null && op.left == null) {
ids.push(op.parent) ids.push(op.parent)
if (op.opContent != null) { if (op.opContent != null) {
@ -104,13 +106,13 @@ var Struct = {
} }
return ids return ids
}, },
getDistanceToOrigin: function *(op) { getDistanceToOrigin: function * (op) {
if (op.left == null) { if (op.left == null) {
return 0 return 0
} else { } else {
var d = 0 var d = 0
var o = yield* this.getOperation(op.left) var o = yield* this.getOperation(op.left)
while (!compareIds(op.origin, (o ? o.id : null))) { while (!Y.utils.compareIds(op.origin, (o ? o.id : null))) {
d++ d++
if (o.left == null) { if (o.left == null) {
break break
@ -156,7 +158,7 @@ var Struct = {
// handle conflicts // handle conflicts
while (true) { while (true) {
if (o != null && !compareIds(o.id, op.right)) { if (o != null && !Y.utils.compareIds(o.id, op.right)) {
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o) var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
if (oOriginDistance === i) { if (oOriginDistance === i) {
// case 1 // case 1
@ -250,7 +252,7 @@ var Struct = {
*/ */
return [] return []
}, },
execute: function * (op) { // eslint-disable-line execute: function * (op) {
op.start = null op.start = null
op.end = null op.end = null
}, },
@ -277,7 +279,7 @@ var Struct = {
map: function * (o, f) { map: function * (o, f) {
o = o.start o = o.start
var res = [] var res = []
while (o !== null) { while (o !== null) { // TODO: change to != (at least some convention)
var operation = yield* this.getOperation(o) var operation = yield* this.getOperation(o)
if (!operation.deleted) { if (!operation.deleted) {
res.push(f(operation)) res.push(f(operation))
@ -305,16 +307,12 @@ var Struct = {
} }
}, },
requiredOps: function () { requiredOps: function () {
/*
var ids = []
for (var end in op.map) {
ids.push(op.map[end])
}
return ids
*/
return [] return []
}, },
execute: function * () {}, execute: function * () {},
/*
Get a property by name
*/
get: function * (op, name) { get: function * (op, name) {
var oid = op.map[name] var oid = op.map[name]
if (oid != null) { if (oid != null) {
@ -323,6 +321,9 @@ var Struct = {
? res.content : yield* this.getType(res.opContent)) ? res.content : yield* this.getType(res.opContent))
} }
}, },
/*
Delete a property by name
*/
delete: function * (op, name) { delete: function * (op, name) {
var v = op.map[name] || null var v = op.map[name] || null
if (v != null) { if (v != null) {

View File

@ -49,7 +49,7 @@
throw new Error('Unexpected struct!') throw new Error('Unexpected struct!')
} }
} }
this.eventHandler.callUserEventListeners(userEvents) this.eventHandler.callEventListeners(userEvents)
}) })
} }
get length () { get length () {
@ -110,7 +110,7 @@
ops[j].right = mostRight ops[j].right = mostRight
} }
yield* this.applyCreatedOperations(ops) yield* this.applyCreatedOperations(ops)
eventHandler.awaitedLastInserts(ops.length) eventHandler.awaitedInserts(ops.length)
}) })
} }
delete (pos, length) { delete (pos, length) {
@ -139,11 +139,11 @@
eventHandler.awaitAndPrematurelyCall(dels) eventHandler.awaitAndPrematurelyCall(dels)
this.os.requestTransaction(function *() { this.os.requestTransaction(function *() {
yield* this.applyCreatedOperations(dels) yield* this.applyCreatedOperations(dels)
eventHandler.awaitedLastDeletes(dels.length, newLeft) eventHandler.awaitedDeletes(dels.length, newLeft)
}) })
} }
observe (f) { observe (f) {
this.eventHandler.addUserEventListener(f) this.eventHandler.addEventListener(f)
} }
* _changed (transaction, op) { * _changed (transaction, op) {
if (!op.deleted) { if (!op.deleted) {

View File

@ -6,7 +6,6 @@ var numberOfYArrayTests = 10
describe('Array Type', function () { describe('Array Type', function () {
var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100
beforeEach(async(function * (done) { beforeEach(async(function * (done) {
yield createUsers(this, 3) yield createUsers(this, 3)
y1 = (yconfig1 = this.users[0]).root y1 = (yconfig1 = this.users[0]).root
@ -59,7 +58,7 @@ describe('Array Type', function () {
expect(l2.toArray()).toEqual(l3.toArray()) expect(l2.toArray()).toEqual(l3.toArray())
expect(l2.toArray()).toEqual([0, 2, 'y']) expect(l2.toArray()).toEqual([0, 2, 'y'])
done() done()
})) }), 100)
it('Handles getOperations ascending ids bug in late sync', async(function * (done) { it('Handles getOperations ascending ids bug in late sync', async(function * (done) {
var l1, l2 var l1, l2
l1 = yield y1.set('Array', Y.Array) l1 = yield y1.set('Array', Y.Array)

View File

@ -21,11 +21,11 @@
if (this.opContents[key] != null) { if (this.opContents[key] != null) {
let prevType = this.opContents[key] let prevType = this.opContents[key]
oldValue = () => {// eslint-disable-line oldValue = () => {// eslint-disable-line
let def = Promise.defer() return new Promise((resolve) => {
this.os.requestTransaction(function *() {// eslint-disable-line this.os.requestTransaction(function *() {// eslint-disable-line
def.resolve(yield* this.getType(prevType)) resolve(yield* this.getType(prevType))
})
}) })
return def.promise
} }
} else { } else {
oldValue = this.contents[key] oldValue = this.contents[key]
@ -77,7 +77,7 @@
throw new Error('Unexpected Operation!') throw new Error('Unexpected Operation!')
} }
} }
this.eventHandler.callUserEventListeners(userEvents) this.eventHandler.callEventListeners(userEvents)
}) })
} }
get (key) { get (key) {
@ -91,12 +91,12 @@
return this.contents[key] return this.contents[key]
} }
} else { } else {
let def = Promise.defer() return new Promise((resolve) => {
var oid = this.opContents[key] var oid = this.opContents[key]
this.os.requestTransaction(function *() { this.os.requestTransaction(function *() {
def.resolve(yield* this.getType(oid)) resolve(yield* this.getType(oid))
})
}) })
return def.promise
} }
} }
delete (key) { delete (key) {
@ -112,7 +112,7 @@
eventHandler.awaitAndPrematurelyCall([modDel]) eventHandler.awaitAndPrematurelyCall([modDel])
this.os.requestTransaction(function *() { this.os.requestTransaction(function *() {
yield* this.applyCreatedOperations([del]) yield* this.applyCreatedOperations([del])
eventHandler.awaitedLastDeletes(1) eventHandler.awaitedDeletes(1)
}) })
} }
} }
@ -130,35 +130,35 @@
parentSub: key, parentSub: key,
struct: 'Insert' struct: 'Insert'
} }
var def = Promise.defer() return new Promise((resolve) => {
if (value instanceof Y.utils.CustomType) { if (value instanceof Y.utils.CustomType) {
// construct a new type // construct a new type
this.os.requestTransaction(function *() { this.os.requestTransaction(function *() {
var type = yield* value.createType.call(this) var type = yield* value.createType.call(this)
insert.opContent = type._model insert.opContent = type._model
insert.id = this.store.getNextOpId() insert.id = this.store.getNextOpId()
yield* this.applyCreatedOperations([insert]) yield* this.applyCreatedOperations([insert])
def.resolve(type) resolve(type)
}) })
} else { } else {
insert.content = value insert.content = value
insert.id = this.os.getNextOpId() insert.id = this.os.getNextOpId()
var eventHandler = this.eventHandler var eventHandler = this.eventHandler
eventHandler.awaitAndPrematurelyCall([insert]) eventHandler.awaitAndPrematurelyCall([insert])
this.os.requestTransaction(function *() { this.os.requestTransaction(function *() {
yield* this.applyCreatedOperations([insert]) yield* this.applyCreatedOperations([insert])
eventHandler.awaitedLastInserts(1) eventHandler.awaitedInserts(1)
}) })
def.resolve(value) resolve(value)
} }
return def.promise })
} }
observe (f) { observe (f) {
this.eventHandler.addUserEventListener(f) this.eventHandler.addEventListener(f)
} }
unobserve (f) { unobserve (f) {
this.eventHandler.removeUserEventListener(f) this.eventHandler.removeEventListener(f)
} }
observePath (path, f) { observePath (path, f) {
var self = this var self = this

View File

@ -6,7 +6,6 @@ var numberOfYMapTests = 5
describe('Map Type', function () { describe('Map Type', function () {
var y1, y2, y3, y4, flushAll var y1, y2, y3, y4, flushAll
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000
beforeEach(async(function * (done) { beforeEach(async(function * (done) {
yield createUsers(this, 5) yield createUsers(this, 5)
y1 = this.users[0].root y1 = this.users[0].root

View File

@ -1,13 +1,44 @@
/* global Y */ /* global Y */
'use strict' 'use strict'
class EventHandler { // eslint-disable-line /*
EventHandler is an helper class for constructing custom types.
Why: When constructing custom types, you sometimes want your types to work
synchronous: E.g.
``` Synchronous
mytype.setSomething("yay")
mytype.getSomething() === "yay"
```
``` Asynchronous
mytype.setSomething("yay")
mytype.getSomething() === undefined
mytype.waitForSomething().then(function(){
mytype.getSomething() === "yay"
})
The structures usually work asynchronously (you have to wait for the
database request to finish). EventHandler will help you to make your type
synchronously.
*/
class EventHandler {
/*
onevent: is called when the structure changes.
Note: "awaiting opertations" is used to denote operations that were
prematurely called. Events for received operations can not be executed until
all prematurely called operations were executed ("waiting operations")
*/
constructor (onevent) { constructor (onevent) {
this.waiting = [] this.waiting = []
this.awaiting = 0 this.awaiting = 0
this.onevent = onevent this.onevent = onevent
this.userEventListeners = [] this.eventListeners = []
} }
/*
Call this when a new operation arrives. It will be executed right away if
there are no waiting operations, that you prematurely executed
*/
receivedOp (op) { receivedOp (op) {
if (this.awaiting <= 0) { if (this.awaiting <= 0) {
this.onevent([op]) this.onevent([op])
@ -15,31 +46,43 @@ class EventHandler { // eslint-disable-line
this.waiting.push(Y.utils.copyObject(op)) this.waiting.push(Y.utils.copyObject(op))
} }
} }
/*
You created some operations, and you want the `onevent` function to be
called right away. Received operations will not be executed untill all
prematurely called operations are executed
*/
awaitAndPrematurelyCall (ops) { awaitAndPrematurelyCall (ops) {
this.awaiting++ this.awaiting++
this.onevent(ops) this.onevent(ops)
} }
addUserEventListener (f) { /*
this.userEventListeners.push(f) Basic event listener boilerplate...
TODO: maybe put this in a different type..
*/
addEventListener (f) {
this.eventListeners.push(f)
} }
removeUserEventListener (f) { removeEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(function (g) { this.eventListeners = this.eventListeners.filter(function (g) {
return f !== g return f !== g
}) })
} }
removeAllUserEventListeners () { removeAllEventListeners () {
this.userEventListeners = [] this.eventListeners = []
} }
callUserEventListeners (event) { callEventListeners (event) {
for (var i in this.userEventListeners) { for (var i in this.eventListeners) {
try { try {
this.userEventListeners[i](event) this.eventListeners[i](event)
} catch (e) { } catch (e) {
console.log('User events must not throw Errors!');// eslint-disable-line console.log('User events must not throw Errors!') // eslint-disable-line
} }
} }
} }
awaitedLastInserts (n) { /*
Call this when you successfully awaited the execution of n Insert operations
*/
awaitedInserts (n) {
var ops = this.waiting.splice(this.waiting.length - n) var ops = this.waiting.splice(this.waiting.length - n)
for (var oid = 0; oid < ops.length; oid++) { for (var oid = 0; oid < ops.length; oid++) {
var op = ops[oid] var op = ops[oid]
@ -57,9 +100,12 @@ class EventHandler { // eslint-disable-line
} }
} }
} }
this.tryCallEvents() this._tryCallEvents()
} }
awaitedLastDeletes (n, newLeft) { /*
Call this when you successfully awaited the execution of n Delete operations
*/
awaitedDeletes (n, newLeft) {
var ops = this.waiting.splice(this.waiting.length - n) var ops = this.waiting.splice(this.waiting.length - n)
for (var j in ops) { for (var j in ops) {
var del = ops[j] var del = ops[j]
@ -73,9 +119,12 @@ class EventHandler { // eslint-disable-line
} }
} }
} }
this.tryCallEvents() this._tryCallEvents()
} }
tryCallEvents () { /* (private)
Try to execute the events for the waiting operations
*/
_tryCallEvents () {
this.awaiting-- this.awaiting--
if (this.awaiting <= 0 && this.waiting.length > 0) { if (this.awaiting <= 0 && this.waiting.length > 0) {
var events = this.waiting var events = this.waiting
@ -86,6 +135,17 @@ class EventHandler { // eslint-disable-line
} }
Y.utils.EventHandler = EventHandler Y.utils.EventHandler = EventHandler
/*
A wrapper for the definition of a custom type.
Every custom type must have three properties:
* createType
- Defines the model of a newly created custom type and returns the type
* initType
- Given a model, creates a custom type
* class
- the constructor of the custom type (e.g. in order to inherit from a type)
*/
class CustomType { // eslint-disable-line class CustomType { // eslint-disable-line
constructor (def) { constructor (def) {
if (def.createType == null || if (def.createType == null ||
@ -100,3 +160,39 @@ class CustomType { // eslint-disable-line
} }
} }
Y.utils.CustomType = CustomType Y.utils.CustomType = CustomType
/*
Make a flat copy of an object
(just copy properties)
*/
function copyObject (o) {
var c = {}
for (var key in o) {
c[key] = o[key]
}
return c
}
Y.utils.copyObject = copyObject
/*
Defines a smaller relation on Id's
*/
function smaller (a, b) {
return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1])
}
Y.utils.smaller = smaller
function compareIds (id1, id2) {
if (id1 == null || id2 == null) {
if (id1 == null && id2 == null) {
return true
}
return false
}
if (id1[0] === id2[0] && id1[1] === id2[1]) {
return true
} else {
return false
}
}
Y.utils.compareIds = compareIds

View File

@ -2,20 +2,19 @@
'use strict' 'use strict'
function Y (opts) { function Y (opts) {
var def = Promise.defer() return new Promise(function (resolve) {
new YConfig(opts, function (yconfig) { // eslint-disable-line var yconfig = new YConfig(opts, function () {
yconfig.db.whenUserIdSet(function () { yconfig.db.whenUserIdSet(function () {
def.resolve(yconfig) resolve(yconfig)
})
}) })
}) })
return def.promise
} }
class YConfig { // eslint-disable-line no-unused-vars class YConfig {
constructor (opts, callback) { constructor (opts, callback) {
this.db = new Y[opts.db.name](this, opts.db) this.db = new Y[opts.db.name](this, opts.db)
this.connector = new Y[opts.connector.name](this, opts.connector) this.connector = new Y[opts.connector.name](this, opts.connector)
var yconfig = this
this.db.requestTransaction(function * requestTransaction () { this.db.requestTransaction(function * requestTransaction () {
// create initial Map type // create initial Map type
var model = { var model = {
@ -27,7 +26,7 @@ class YConfig { // eslint-disable-line no-unused-vars
yield* this.addOperation(model) yield* this.addOperation(model)
var root = yield* this.createType(model) var root = yield* this.createType(model)
this.store.y.root = root this.store.y.root = root
callback(yconfig) callback()
}) })
} }
isConnected () { isConnected () {
@ -55,7 +54,7 @@ class YConfig { // eslint-disable-line no-unused-vars
} }
} }
if (g) { // eslint-disable-line if (typeof YConcurrency_TestingMode !== 'undefined') {
g.Y = Y //eslint-disable-line g.Y = Y //eslint-disable-line
// debugger //eslint-disable-line // debugger //eslint-disable-line
} }