added comments to most of the classes.
This commit is contained in:
parent
6f3a291ef5
commit
b1d70ef25e
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
||||||
|
18
package.json
18
package.json
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
113
src/Struct.js
113
src/Struct.js
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
130
src/Utils.js
130
src/Utils.js
@ -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
|
||||||
|
17
src/y.js
17
src/y.js
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user