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_test
.directory
.c9
.codio
.settings
.jshintignore
.jshintrc
.validate.json
y.js
y.js.map
./y.js
./y.js.map

View File

@ -41,18 +41,18 @@
},
"homepage": "http://y-js.org",
"devDependencies": {
"babel-eslint": "^4.1.1",
"babel-eslint": "^4.1.2",
"gulp": "^3.9.0",
"gulp-babel": "^5.1.0",
"gulp-concat": "^2.5.2",
"gulp-babel": "^5.2.1",
"gulp-concat": "^2.6.0",
"gulp-jasmine": "^2.0.1",
"gulp-jasmine-browser": "^0.1.3",
"gulp-jasmine-browser": "^0.2.3",
"gulp-sourcemaps": "^1.5.2",
"gulp-uglify": "^1.2.0",
"gulp-util": "^3.0.5",
"gulp-watch": "^4.2.4",
"minimist": "^1.1.1",
"pre-commit": "^1.0.10",
"gulp-uglify": "^1.4.1",
"gulp-util": "^3.0.6",
"gulp-watch": "^4.3.5",
"minimist": "^1.2.0",
"pre-commit": "^1.1.1",
"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 */
'use strict'
class AbstractConnector { // eslint-disable-line no-unused-vars
class AbstractConnector {
/*
opts
.role : String Role of this client ("master" or "slave")
.userId : String that uniquely defines the user.
opts contains the following information:
role : String Role of this client ("master" or "slave")
userId : String Uniquely defines the user.
debug: Boolean Whether to print debug messages (optional)
*/
constructor (y, opts) {
this.y = y
@ -90,8 +91,11 @@ class AbstractConnector { // eslint-disable-line no-unused-vars
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 () {
if (this.currentSyncTarget != null) {
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) {
this.isSynced = true
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
}
}
// 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) {
if (sender === this.userId) {
return

View File

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

View File

@ -3,8 +3,9 @@
/*
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
if (typeof global !== 'undefined') {
g = global
@ -15,20 +16,29 @@ if (typeof global !== 'undefined') {
}
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) {
if (t == null) {
t = 5
}
var def = Promise.defer()
setTimeout(function () {
def.resolve()
}, t)
return def.promise
return new Promise(function (resolve) {
setTimeout(function () {
resolve()
}, t)
})
}
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) {
if (o instanceof Array) {
return o[Math.floor(Math.random() * o.length)]
@ -42,7 +52,7 @@ function getRandom (o) {
}
g.getRandom = getRandom
function getRandomNumber(n) {//eslint-disable-line
function getRandomNumber (n) {
if (n == null) {
n = 9999
}
@ -50,7 +60,7 @@ function getRandomNumber(n) {//eslint-disable-line
}
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) {
var f = getRandom(transactions)
f(root)
@ -86,8 +96,12 @@ g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
})
g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-line
var s1, s2, ds1, ds2, allDels1, allDels2
var db1 = []
var s1, s2 // state sets
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 () {
s1 = yield* this.getStateSet()
ds1 = yield* this.getDeleteSet()
@ -105,6 +119,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-
})
}
yield users[0].connector.flushAll()
// gc two times because of the two gc phases (really collect everything)
yield g.garbageCollectAllUsers(users)
yield wait(50)
yield g.garbageCollectAllUsers(users)
@ -137,7 +152,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-
if (s1 == null) {
u.db.requestTransaction(t1)
yield wait()
u.db.os.iterate(null, null, function(o){//eslint-disable-line
u.db.os.iterate(null, null, function (o) {
db1.push(o)
})
} else {
@ -147,22 +162,22 @@ g.compareAllUsers = async(function * compareAllUsers (users) { //eslint-disable-
expect(allDels1).toEqual(allDels2) // inner structure
expect(ds1).toEqual(ds2) // exported structure
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)
})
}
}
})
g.createUsers = async(function * createUsers (self, numberOfUsers) { //eslint-disable-line
if (Y.utils.globalRoom.users[0] != null) {//eslint-disable-line
yield Y.utils.globalRoom.users[0].flushAll()//eslint-disable-line
g.createUsers = async(function * createUsers (self, numberOfUsers) {
if (Y.utils.globalRoom.users[0] != null) {
yield Y.utils.globalRoom.users[0].flushAll()
}
// destroy old users
for (var u in Y.utils.globalRoom.users) {//eslint-disable-line
Y.utils.globalRoom.users[u].y.destroy()//eslint-disable-line
for (var u in Y.utils.globalRoom.users) {
Y.utils.globalRoom.users[u].y.destroy()
}
self.users = []
self.users = null
var promises = []
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)
return self.users
})
/*
Until async/await arrives in js, we use this function to wait for promises
by yielding them.
*/
function async (makeGenerator) {
return function (arg) {
var generator = makeGenerator.apply(this, arguments)
function handle (result) {
// result => { done: [Boolean], value: [Object] }
if (result.done) return Promise.resolve(result.value)
return Promise.resolve(result.value).then(function (res) {
@ -194,12 +213,14 @@ function async (makeGenerator) {
return handle(generator.throw(err))
})
}
try {
// this may throw errors here, but its ok since this is used only for debugging
return handle(generator.next())
/* try {
return handle(generator.next())
} catch (ex) {
return Promise.reject(ex)
}
generator.throw(ex) // TODO: check this out
// return Promise.reject(ex)
}*/
}
}
g.async = async

View File

@ -1,7 +1,76 @@
/* global Y */
'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 {
constructor (store) {
this.store = store
@ -41,6 +110,18 @@ class AbstractTransaction {
}
Y.AbstractTransaction = AbstractTransaction
/*
Partial definition of an OperationStore.
TODO: name it Database, operation store only holds operations.
A database definition must alse define the following methods:
* logTable() (optional)
- show relevant information information in a table
* requestTransaction(makeGen)
- request a transaction
* destroy()
- destroy the database
*/
class AbstractOperationStore { // eslint-disable-line no-unused-vars
constructor (y, opts) {
this.y = y
@ -71,44 +152,44 @@ class AbstractOperationStore { // eslint-disable-line no-unused-vars
this.gcTimeout = opts.gcTimeout || 5000
var os = this
function garbageCollect () {
var def = Promise.defer()
os.requestTransaction(function * () {
for (var i in os.gc2) {
var oid = os.gc2[i]
var o = yield* this.getOperation(oid)
if (o.left != null) {
var left = yield* this.getOperation(o.left)
left.right = o.right
yield* this.setOperation(left)
return new Promise((resolve) => {
os.requestTransaction(function * () {
for (var i in os.gc2) {
var oid = os.gc2[i]
var o = yield* this.getOperation(oid)
if (o.left != null) {
var left = yield* this.getOperation(o.left)
left.right = o.right
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) {
var right = yield* this.getOperation(o.right)
right.left = o.left
yield* this.setOperation(right)
os.gc2 = os.gc1
os.gc1 = []
if (os.gcTimeout > 0) {
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
}
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)
}
os.gc2 = os.gc1
os.gc1 = []
if (os.gcTimeout > 0) {
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
}
def.resolve()
resolve()
})
})
return def.promise
}
this.garbageCollect = garbageCollect
if (this.gcTimeout > 0) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,44 @@
/* global Y */
'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) {
this.waiting = []
this.awaiting = 0
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) {
if (this.awaiting <= 0) {
this.onevent([op])
@ -15,31 +46,43 @@ class EventHandler { // eslint-disable-line
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) {
this.awaiting++
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) {
this.userEventListeners = this.userEventListeners.filter(function (g) {
removeEventListener (f) {
this.eventListeners = this.eventListeners.filter(function (g) {
return f !== g
})
}
removeAllUserEventListeners () {
this.userEventListeners = []
removeAllEventListeners () {
this.eventListeners = []
}
callUserEventListeners (event) {
for (var i in this.userEventListeners) {
callEventListeners (event) {
for (var i in this.eventListeners) {
try {
this.userEventListeners[i](event)
this.eventListeners[i](event)
} 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)
for (var oid = 0; oid < ops.length; 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)
for (var j in ops) {
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--
if (this.awaiting <= 0 && this.waiting.length > 0) {
var events = this.waiting
@ -86,6 +135,17 @@ class EventHandler { // eslint-disable-line
}
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
constructor (def) {
if (def.createType == null ||
@ -100,3 +160,39 @@ class CustomType { // eslint-disable-line
}
}
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'
function Y (opts) {
var def = Promise.defer()
new YConfig(opts, function (yconfig) { // eslint-disable-line
yconfig.db.whenUserIdSet(function () {
def.resolve(yconfig)
return new Promise(function (resolve) {
var yconfig = new YConfig(opts, function () {
yconfig.db.whenUserIdSet(function () {
resolve(yconfig)
})
})
})
return def.promise
}
class YConfig { // eslint-disable-line no-unused-vars
class YConfig {
constructor (opts, callback) {
this.db = new Y[opts.db.name](this, opts.db)
this.connector = new Y[opts.connector.name](this, opts.connector)
var yconfig = this
this.db.requestTransaction(function * requestTransaction () {
// create initial Map type
var model = {
@ -27,7 +26,7 @@ class YConfig { // eslint-disable-line no-unused-vars
yield* this.addOperation(model)
var root = yield* this.createType(model)
this.store.y.root = root
callback(yconfig)
callback()
})
}
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
// debugger //eslint-disable-line
}