diff --git a/gulpfile.helper.js b/gulpfile.helper.js
index df6e2213..e9b975fd 100644
--- a/gulpfile.helper.js
+++ b/gulpfile.helper.js
@@ -21,31 +21,10 @@ module.exports = function (gulp, helperOptions) {
options.regenerator = false
// TODO: include './node_modules/gulp-babel/node_modules/babel-core/node_modules/regenerator/runtime.js'
}
- var concatOrder = [
- 'y.js',
- 'Connector.js',
- 'Database.js',
- 'Transaction.js',
- 'Struct.js',
- 'Utils.js',
- 'Databases/RedBlackTree.js',
- 'Databases/Memory.js',
- 'Databases/IndexedDB.js',
- 'Connectors/Test.js',
- 'Types/Array.js',
- 'Types/Map.js',
- 'Types/TextBind.js'
- ]
- var yjsfiles = concatOrder.map(function (f) {
- return '../yjs/src/' + f
- })
var files = {
- dist: helperOptions.polyfills.concat(helperOptions.files.map(function (f) {
- return 'src/' + f
- })),
- test: ['../yjs/src/Helper.spec.js'].concat(yjsfiles).concat(helperOptions.files.map(function (f) {
- return 'src/' + f
- }).concat(['src/' + options.testfiles]))
+ dist: helperOptions.entry,
+ specs: helperOptions.specs,
+ src: './src/**/*.js'
}
var babelOptions = {
@@ -54,38 +33,10 @@ module.exports = function (gulp, helperOptions) {
experimental: true
}
if (options.regenerator) {
- files.test = helperOptions.polyfills.concat(files.test)
+ files.specs = helperOptions.polyfills.concat(files.specs)
} else {
babelOptions.blacklist = 'regenerator'
}
- // babelOptions.blacklist = 'regenerator'
-
- gulp.task('dist', ['build:dist'], function () {
- function createDist (pipe) {
- return pipe
- .pipe($.if(options.debug, $.sourcemaps.init({loadMaps: true})))
- .pipe($.concat(options.targetName))
- .pipe($.if(!options.debug && options.regenerator, $.uglify()))
- .pipe($.if(options.debug, $.sourcemaps.write('.')))
- .pipe(gulp.dest('./dist/'))
- }
- var pipe
- if (options.browserify || true) {
- var browserify = require('browserify')
- var source = require('vinyl-source-stream')
- var buffer = require('vinyl-buffer')
-
- pipe = browserify({
- entries: 'build/' + options.targetName,
- debug: options.debug
- }).bundle()
- .pipe(source(options.targetName))
- .pipe(buffer())
- } else {
- pipe = gulp.src('build/' + options.targetName)
- }
- return createDist(pipe)
- })
gulp.task('dist', function () {
var browserify = require('browserify')
@@ -99,7 +50,7 @@ module.exports = function (gulp, helperOptions) {
.pipe(source(options.targetName))
.pipe(buffer())
.pipe($.if(options.debug, $.sourcemaps.init({loadMaps: true})))
- .pipe($.concat(options.targetName))
+ .pipe($.if(!options.debug && options.regenerator, $.babel(babelOptions)))
.pipe($.if(!options.debug && options.regenerator, $.uglify()))
.pipe($.if(options.debug, $.sourcemaps.write('.')))
.pipe(gulp.dest('./dist/'))
@@ -108,11 +59,46 @@ module.exports = function (gulp, helperOptions) {
gulp.task('watch:dist', function (cb) {
options.debug = true
runSequence('dist', function () {
- gulp.watch(files.dist, ['dist'])
+ gulp.watch(files.src, ['dist'])
cb()
})
})
+ gulp.task('dev:node', ['test'], function () {
+ gulp.watch(files.src, ['test'])
+ })
+
+ gulp.task('spec-build', function () {
+ var browserify = require('browserify')
+ var source = require('vinyl-source-stream')
+ var buffer = require('vinyl-buffer')
+
+ return browserify({
+ entries: files.specs,
+ debug: options.debug
+ }).bundle()
+ .pipe(source('specs.js'))
+ .pipe(buffer())
+ .pipe($.sourcemaps.init({loadMaps: true}))
+ .pipe($.sourcemaps.write())
+ .pipe(gulp.dest('./build/'))
+ })
+
+ gulp.task('dev:browser', ['spec-build'], function () {
+ gulp.watch(files.src, ['spec-build'])
+ return gulp.src('./build/specs.js')
+ .pipe($.jasmineBrowser.specRunner())
+ .pipe($.jasmineBrowser.server({port: options.testport}))
+ })
+
+ gulp.task('test', function () {
+ return gulp.src(files.specs)
+ .pipe($.jasmine({
+ verbose: true,
+ includeStuckTrace: true
+ }))
+ })
+
gulp.task('updateSubmodule', function () {
return gulp.src('./package.json', {read: false})
.pipe($.shell([
@@ -168,24 +154,4 @@ module.exports = function (gulp, helperOptions) {
}))
})
})
-
- gulp.task('dev:node', ['test'], function () {
- gulp.watch(files.dist, ['test'])
- })
-
- gulp.task('dev:browser', ['watch:build'], function () {
- return gulp.src(files.test)
- .pipe($.watch(['build/**/*']))
- .pipe($.jasmineBrowser.specRunner())
- .pipe($.jasmineBrowser.server({port: options.testport}))
- })
-
- gulp.task('test', function () {
- console.log(files.test)
- return gulp.src('./dist/y.js')
- .pipe($.jasmine({
- verbose: true,
- includeStuckTrace: true
- }))
- })
}
diff --git a/gulpfile.js b/gulpfile.js
index 5813a05c..e2416b32 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -49,23 +49,15 @@ var runSequence = require('run-sequence').use(gulp)
require('./gulpfile.helper.js')(gulp, {
polyfills: [],
- files: [
- 'y.js',
- 'Connector.js',
- 'Database.js',
- 'Transaction.js',
- 'Struct.js',
- 'Utils.js',
- 'Databases/RedBlackTree.js',
- 'Databases/Memory.js',
- 'Databases/IndexedDB.js',
- 'Connectors/Test.js',
- 'Types/Array.js',
- 'Types/Map.js',
- 'Types/TextBind.js'
- ],
+ entry: './src/y.js',
targetName: 'y.js',
- moduleName: 'yjs'
+ moduleName: 'yjs',
+ specs: [
+ './src/Databases/RedBlackTree.spec.js',
+ './src/Types/Array.spec.js',
+ './src/Types/Map.spec.js',
+ './src/Database.spec.js'
+ ]
})
gulp.task('dev:examples', ['updateSubmodule', 'watch:dist'], function () {
diff --git a/package.json b/package.json
index 6cc53b78..4abb616e 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"run-sequence": "^1.1.4",
"standard": "^5.2.2",
"vinyl-buffer": "^1.0.0",
- "vinyl-source-stream": "^1.1.0"
+ "vinyl-source-stream": "^1.1.0",
+ "watchify": "^3.6.0"
}
}
diff --git a/src/Connector.js b/src/Connector.js
index 1784712f..42119593 100644
--- a/src/Connector.js
+++ b/src/Connector.js
@@ -1,328 +1,329 @@
-/* globals Y */
'use strict'
-class AbstractConnector {
- /*
- 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
- if (opts == null) {
- opts = {}
- }
- if (opts.role == null || opts.role === 'master') {
- this.role = 'master'
- } else if (opts.role === 'slave') {
- this.role = 'slave'
- } else {
- throw new Error("Role must be either 'master' or 'slave'!")
- }
- this.role = opts.role
- this.connections = {}
- this.isSynced = false
- this.userEventListeners = []
- this.whenSyncedListeners = []
- this.currentSyncTarget = null
- this.syncingClients = []
- this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
- this.debug = opts.debug === true
- this.broadcastedHB = false
- this.syncStep2 = Promise.resolve()
- }
- reconnect () {
- }
- disconnect () {
- this.connections = {}
- this.isSynced = false
- this.currentSyncTarget = null
- this.broadcastedHB = false
- this.syncingClients = []
- this.whenSyncedListeners = []
- return this.y.db.stopGarbageCollector()
- }
- setUserId (userId) {
- this.userId = userId
- return this.y.db.setUserId(userId)
- }
- onUserEvent (f) {
- this.userEventListeners.push(f)
- }
- userLeft (user) {
- delete this.connections[user]
- if (user === this.currentSyncTarget) {
- this.currentSyncTarget = null
- this.findNextSyncTarget()
- }
- this.syncingClients = this.syncingClients.filter(function (cli) {
- return cli !== user
- })
- for (var f of this.userEventListeners) {
- f({
- action: 'userLeft',
- user: user
- })
- }
- }
- userJoined (user, role) {
- if (role == null) {
- throw new Error('You must specify the role of the joined user!')
- }
- if (this.connections[user] != null) {
- throw new Error('This user already joined!')
- }
- this.connections[user] = {
- isSynced: false,
- role: role
- }
- for (var f of this.userEventListeners) {
- f({
- action: 'userJoined',
- user: user,
- role: role
- })
- }
- if (this.currentSyncTarget == null) {
- this.findNextSyncTarget()
- }
- }
- // Execute a function _when_ we are connected.
- // If not connected, wait until connected
- whenSynced (f) {
- if (this.isSynced) {
- f()
- } else {
- this.whenSyncedListeners.push(f)
- }
- }
- /*
-
- returns false, if there is no sync target
- true otherwise
- */
- findNextSyncTarget () {
- if (this.currentSyncTarget != null || this.isSynced) {
- return // "The current sync has not finished!"
- }
-
- var syncUser = null
- for (var uid in this.connections) {
- if (!this.connections[uid].isSynced) {
- syncUser = uid
- break
+module.exports = function (Y) {
+ class AbstractConnector {
+ /*
+ 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
+ if (opts == null) {
+ opts = {}
}
- }
- if (syncUser != null) {
- var conn = this
- this.currentSyncTarget = syncUser
- this.y.db.requestTransaction(function *() {
- conn.send(syncUser, {
- type: 'sync step 1',
- stateSet: yield* this.getStateSet(),
- deleteSet: yield* this.getDeleteSet()
- })
- })
- } else {
- this.isSynced = true
- // call when synced listeners
- for (var f of this.whenSyncedListeners) {
- f()
+ if (opts.role == null || opts.role === 'master') {
+ this.role = 'master'
+ } else if (opts.role === 'slave') {
+ this.role = 'slave'
+ } else {
+ throw new Error("Role must be either 'master' or 'slave'!")
}
+ this.role = opts.role
+ this.connections = {}
+ this.isSynced = false
+ this.userEventListeners = []
this.whenSyncedListeners = []
- this.y.db.requestTransaction(function *() {
- yield* this.garbageCollectAfterSync()
+ this.currentSyncTarget = null
+ this.syncingClients = []
+ this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
+ this.debug = opts.debug === true
+ this.broadcastedHB = false
+ this.syncStep2 = Promise.resolve()
+ }
+ reconnect () {
+ }
+ disconnect () {
+ this.connections = {}
+ this.isSynced = false
+ this.currentSyncTarget = null
+ this.broadcastedHB = false
+ this.syncingClients = []
+ this.whenSyncedListeners = []
+ return this.y.db.stopGarbageCollector()
+ }
+ setUserId (userId) {
+ this.userId = userId
+ return this.y.db.setUserId(userId)
+ }
+ onUserEvent (f) {
+ this.userEventListeners.push(f)
+ }
+ userLeft (user) {
+ delete this.connections[user]
+ if (user === this.currentSyncTarget) {
+ this.currentSyncTarget = null
+ this.findNextSyncTarget()
+ }
+ this.syncingClients = this.syncingClients.filter(function (cli) {
+ return cli !== user
})
- }
- }
- send (uid, message) {
- if (this.debug) {
- console.log(`send ${this.userId} -> ${uid}: ${message.type}`, m) // eslint-disable-line
- }
- }
- /*
- 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
- }
- if (this.debug) {
- console.log(`receive ${sender} -> ${this.userId}: ${m.type}`, JSON.parse(JSON.stringify(m))) // eslint-disable-line
- }
- if (m.type === 'sync step 1') {
- // TODO: make transaction, stream the ops
- let conn = this
- this.y.db.requestTransaction(function *() {
- var currentStateSet = yield* this.getStateSet()
- yield* this.applyDeleteSet(m.deleteSet)
-
- var ds = yield* this.getDeleteSet()
- var ops = yield* this.getOperations(m.stateSet)
- conn.send(sender, {
- type: 'sync step 2',
- os: ops,
- stateSet: currentStateSet,
- deleteSet: ds
+ for (var f of this.userEventListeners) {
+ f({
+ action: 'userLeft',
+ user: user
})
- if (this.forwardToSyncingClients) {
- conn.syncingClients.push(sender)
- setTimeout(function () {
- conn.syncingClients = conn.syncingClients.filter(function (cli) {
- return cli !== sender
- })
+ }
+ }
+ userJoined (user, role) {
+ if (role == null) {
+ throw new Error('You must specify the role of the joined user!')
+ }
+ if (this.connections[user] != null) {
+ throw new Error('This user already joined!')
+ }
+ this.connections[user] = {
+ isSynced: false,
+ role: role
+ }
+ for (var f of this.userEventListeners) {
+ f({
+ action: 'userJoined',
+ user: user,
+ role: role
+ })
+ }
+ if (this.currentSyncTarget == null) {
+ this.findNextSyncTarget()
+ }
+ }
+ // Execute a function _when_ we are connected.
+ // If not connected, wait until connected
+ whenSynced (f) {
+ if (this.isSynced) {
+ f()
+ } else {
+ this.whenSyncedListeners.push(f)
+ }
+ }
+ /*
+
+ returns false, if there is no sync target
+ true otherwise
+ */
+ findNextSyncTarget () {
+ if (this.currentSyncTarget != null || this.isSynced) {
+ return // "The current sync has not finished!"
+ }
+
+ var syncUser = null
+ for (var uid in this.connections) {
+ if (!this.connections[uid].isSynced) {
+ syncUser = uid
+ break
+ }
+ }
+ if (syncUser != null) {
+ var conn = this
+ this.currentSyncTarget = syncUser
+ this.y.db.requestTransaction(function *() {
+ conn.send(syncUser, {
+ type: 'sync step 1',
+ stateSet: yield* this.getStateSet(),
+ deleteSet: yield* this.getDeleteSet()
+ })
+ })
+ } else {
+ this.isSynced = true
+ // call when synced listeners
+ for (var f of this.whenSyncedListeners) {
+ f()
+ }
+ this.whenSyncedListeners = []
+ this.y.db.requestTransaction(function *() {
+ yield* this.garbageCollectAfterSync()
+ })
+ }
+ }
+ send (uid, message) {
+ if (this.debug) {
+ console.log(`send ${this.userId} -> ${uid}: ${message.type}`, m) // eslint-disable-line
+ }
+ }
+ /*
+ 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
+ }
+ if (this.debug) {
+ console.log(`receive ${sender} -> ${this.userId}: ${m.type}`, JSON.parse(JSON.stringify(m))) // eslint-disable-line
+ }
+ if (m.type === 'sync step 1') {
+ // TODO: make transaction, stream the ops
+ let conn = this
+ this.y.db.requestTransaction(function *() {
+ var currentStateSet = yield* this.getStateSet()
+ yield* this.applyDeleteSet(m.deleteSet)
+
+ var ds = yield* this.getDeleteSet()
+ var ops = yield* this.getOperations(m.stateSet)
+ conn.send(sender, {
+ type: 'sync step 2',
+ os: ops,
+ stateSet: currentStateSet,
+ deleteSet: ds
+ })
+ if (this.forwardToSyncingClients) {
+ conn.syncingClients.push(sender)
+ setTimeout(function () {
+ conn.syncingClients = conn.syncingClients.filter(function (cli) {
+ return cli !== sender
+ })
+ conn.send(sender, {
+ type: 'sync done'
+ })
+ }, conn.syncingClientDuration)
+ } else {
conn.send(sender, {
type: 'sync done'
})
- }, conn.syncingClientDuration)
- } else {
- conn.send(sender, {
- type: 'sync done'
- })
- }
- conn._setSyncedWith(sender)
- })
- } else if (m.type === 'sync step 2') {
- let conn = this
- var broadcastHB = !this.broadcastedHB
- this.broadcastedHB = true
- var db = this.y.db
- this.syncStep2 = new Promise(function (resolve) {
- db.requestTransaction(function * () {
- yield* this.applyDeleteSet(m.deleteSet)
- this.store.apply(m.os)
+ }
+ conn._setSyncedWith(sender)
+ })
+ } else if (m.type === 'sync step 2') {
+ let conn = this
+ var broadcastHB = !this.broadcastedHB
+ this.broadcastedHB = true
+ var db = this.y.db
+ this.syncStep2 = new Promise(function (resolve) {
db.requestTransaction(function * () {
- var ops = yield* this.getOperations(m.stateSet)
- if (ops.length > 0) {
- m = {
- type: 'update',
- ops: ops
+ yield* this.applyDeleteSet(m.deleteSet)
+ this.store.apply(m.os)
+ db.requestTransaction(function * () {
+ var ops = yield* this.getOperations(m.stateSet)
+ if (ops.length > 0) {
+ m = {
+ type: 'update',
+ ops: ops
+ }
+ if (!broadcastHB) { // TODO: consider to broadcast here..
+ conn.send(sender, m)
+ } else {
+ // broadcast only once!
+ conn.broadcast(m)
+ }
}
- if (!broadcastHB) { // TODO: consider to broadcast here..
- conn.send(sender, m)
- } else {
- // broadcast only once!
- conn.broadcast(m)
- }
- }
- resolve()
+ resolve()
+ })
})
})
- })
- } else if (m.type === 'sync done') {
- var self = this
- this.syncStep2.then(function () {
- self._setSyncedWith(sender)
- })
- } else if (m.type === 'update') {
- if (this.forwardToSyncingClients) {
- for (var client of this.syncingClients) {
- this.send(client, m)
+ } else if (m.type === 'sync done') {
+ var self = this
+ this.syncStep2.then(function () {
+ self._setSyncedWith(sender)
+ })
+ } else if (m.type === 'update') {
+ if (this.forwardToSyncingClients) {
+ for (var client of this.syncingClients) {
+ this.send(client, m)
+ }
}
+ this.y.db.apply(m.ops)
}
- this.y.db.apply(m.ops)
}
- }
- _setSyncedWith (user) {
- var conn = this.connections[user]
- if (conn != null) {
- conn.isSynced = true
+ _setSyncedWith (user) {
+ var conn = this.connections[user]
+ if (conn != null) {
+ conn.isSynced = true
+ }
+ if (user === this.currentSyncTarget) {
+ this.currentSyncTarget = null
+ this.findNextSyncTarget()
+ }
}
- if (user === this.currentSyncTarget) {
- this.currentSyncTarget = null
- this.findNextSyncTarget()
- }
- }
- /*
- Currently, the HB encodes operations as JSON. For the moment I want to keep it
- that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
- too much overhead. Y is very likely to get changed a lot in the future
+ /*
+ Currently, the HB encodes operations as JSON. For the moment I want to keep it
+ that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
+ too much overhead. Y is very likely to get changed a lot in the future
- Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
- we encode the JSON as XML.
+ Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
+ we encode the JSON as XML.
- When the HB support encoding as XML, the format should look pretty much like this.
+ When the HB support encoding as XML, the format should look pretty much like this.
- does not support primitive values as array elements
- expects an ltx (less than xml) object
- */
- parseMessageFromXml (m) {
- function parseArray (node) {
- for (var n of node.children) {
- if (n.getAttribute('isArray') === 'true') {
- return parseArray(n)
- } else {
- return parseObject(n)
+ does not support primitive values as array elements
+ expects an ltx (less than xml) object
+ */
+ parseMessageFromXml (m) {
+ function parseArray (node) {
+ for (var n of node.children) {
+ if (n.getAttribute('isArray') === 'true') {
+ return parseArray(n)
+ } else {
+ return parseObject(n)
+ }
}
}
+ function parseObject (node) {
+ var json = {}
+ for (var attrName in node.attrs) {
+ var value = node.attrs[attrName]
+ var int = parseInt(value, 10)
+ if (isNaN(int) || ('' + int) !== value) {
+ json[attrName] = value
+ } else {
+ json[attrName] = int
+ }
+ }
+ for (var n in node.children) {
+ var name = n.name
+ if (n.getAttribute('isArray') === 'true') {
+ json[name] = parseArray(n)
+ } else {
+ json[name] = parseObject(n)
+ }
+ }
+ return json
+ }
+ parseObject(m)
}
- function parseObject (node) {
- var json = {}
- for (var attrName in node.attrs) {
- var value = node.attrs[attrName]
- var int = parseInt(value, 10)
- if (isNaN(int) || ('' + int) !== value) {
- json[attrName] = value
- } else {
- json[attrName] = int
+ /*
+ encode message in xml
+ we use string because Strophe only accepts an "xml-string"..
+ So {a:4,b:{c:5}} will look like
+
+
+
+ m - ltx element
+ json - Object
+ */
+ encodeMessageToXml (msg, obj) {
+ // attributes is optional
+ function encodeObject (m, json) {
+ for (var name in json) {
+ var value = json[name]
+ if (name == null) {
+ // nop
+ } else if (value.constructor === Object) {
+ encodeObject(m.c(name), value)
+ } else if (value.constructor === Array) {
+ encodeArray(m.c(name), value)
+ } else {
+ m.setAttribute(name, value)
+ }
}
}
- for (var n in node.children) {
- var name = n.name
- if (n.getAttribute('isArray') === 'true') {
- json[name] = parseArray(n)
- } else {
- json[name] = parseObject(n)
+ function encodeArray (m, array) {
+ m.setAttribute('isArray', 'true')
+ for (var e of array) {
+ if (e.constructor === Object) {
+ encodeObject(m.c('array-element'), e)
+ } else {
+ encodeArray(m.c('array-element'), e)
+ }
}
}
- return json
- }
- parseObject(m)
- }
- /*
- encode message in xml
- we use string because Strophe only accepts an "xml-string"..
- So {a:4,b:{c:5}} will look like
-
-
-
- m - ltx element
- json - Object
- */
- encodeMessageToXml (msg, obj) {
- // attributes is optional
- function encodeObject (m, json) {
- for (var name in json) {
- var value = json[name]
- if (name == null) {
- // nop
- } else if (value.constructor === Object) {
- encodeObject(m.c(name), value)
- } else if (value.constructor === Array) {
- encodeArray(m.c(name), value)
- } else {
- m.setAttribute(name, value)
- }
+ if (obj.constructor === Object) {
+ encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
+ } else if (obj.constructor === Array) {
+ encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
+ } else {
+ throw new Error("I can't encode this json!")
}
}
- function encodeArray (m, array) {
- m.setAttribute('isArray', 'true')
- for (var e of array) {
- if (e.constructor === Object) {
- encodeObject(m.c('array-element'), e)
- } else {
- encodeArray(m.c('array-element'), e)
- }
- }
- }
- if (obj.constructor === Object) {
- encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
- } else if (obj.constructor === Array) {
- encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
- } else {
- throw new Error("I can't encode this json!")
- }
}
+ Y.AbstractConnector = AbstractConnector
}
-Y.AbstractConnector = AbstractConnector
diff --git a/src/Connectors/Test.js b/src/Connectors/Test.js
index e02c2a4a..f2ee9814 100644
--- a/src/Connectors/Test.js
+++ b/src/Connectors/Test.js
@@ -1,136 +1,138 @@
-/* global getRandom, Y, wait, async */
+/* global getRandom, wait, async */
'use strict'
-var globalRoom = {
- users: {},
- buffers: {},
- removeUser: function (user) {
- for (var i in this.users) {
- this.users[i].userLeft(user)
- }
- delete this.users[user]
- delete this.buffers[user]
- },
- addUser: function (connector) {
- this.users[connector.userId] = connector
- this.buffers[connector.userId] = []
- for (var uname in this.users) {
- if (uname !== connector.userId) {
- var u = this.users[uname]
- u.userJoined(connector.userId, 'master')
- connector.userJoined(u.userId, 'master')
+module.exports = function (Y) {
+ var globalRoom = {
+ users: {},
+ buffers: {},
+ removeUser: function (user) {
+ for (var i in this.users) {
+ this.users[i].userLeft(user)
}
- }
- }
-}
-Y.utils.globalRoom = globalRoom
-
-function flushOne () {
- var bufs = []
- for (var i in globalRoom.buffers) {
- if (globalRoom.buffers[i].length > 0) {
- bufs.push(i)
- }
- }
- if (bufs.length > 0) {
- var userId = getRandom(bufs)
- var m = globalRoom.buffers[userId].shift()
- var user = globalRoom.users[userId]
- user.receiveMessage(m[0], m[1])
- return true
- } else {
- return false
- }
-}
-
-// setInterval(flushOne, 10)
-
-var userIdCounter = 0
-
-class Test extends Y.AbstractConnector {
- constructor (y, options) {
- if (options === undefined) {
- throw new Error('Options must not be undefined!')
- }
- options.role = 'master'
- options.forwardToSyncingClients = false
- super(y, options)
- this.setUserId((userIdCounter++) + '').then(() => {
- globalRoom.addUser(this)
- })
- this.globalRoom = globalRoom
- this.syncingClientDuration = 0
- }
- receiveMessage (sender, m) {
- super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
- }
- send (userId, message) {
- var buffer = globalRoom.buffers[userId]
- if (buffer != null) {
- buffer.push(JSON.parse(JSON.stringify([this.userId, message])))
- }
- }
- broadcast (message) {
- for (var key in globalRoom.buffers) {
- globalRoom.buffers[key].push(JSON.parse(JSON.stringify([this.userId, message])))
- }
- }
- isDisconnected () {
- return globalRoom.users[this.userId] == null
- }
- reconnect () {
- if (this.isDisconnected()) {
- globalRoom.addUser(this)
- super.reconnect()
- }
- return this.flushAll()
- }
- disconnect () {
- if (!this.isDisconnected()) {
- globalRoom.removeUser(this.userId)
- super.disconnect()
- }
- return wait()
- }
- flush () {
- var self = this
- return async(function * () {
- yield wait()
- while (globalRoom.buffers[self.userId].length > 0) {
- var m = globalRoom.buffers[self.userId].shift()
- this.receiveMessage(m[0], m[1])
- yield wait()
- }
- })
- }
- flushAll () {
- 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()
- })
+ delete this.users[user]
+ delete this.buffers[user]
+ },
+ addUser: function (connector) {
+ this.users[connector.userId] = connector
+ this.buffers[connector.userId] = []
+ for (var uname in this.users) {
+ if (uname !== connector.userId) {
+ var u = this.users[uname]
+ u.userJoined(connector.userId, 'master')
+ connector.userJoined(u.userId, 'master')
}
}
- // in the case that there are
- // still actions that want to be performed
- wait().then(nextFlush)
- })
+ }
}
- /*
- Flushes an operation for some user..
- */
- flushOne () {
- flushOne()
- }
-}
+ Y.utils.globalRoom = globalRoom
-Y.Test = Test
+ function flushOne () {
+ var bufs = []
+ for (var i in globalRoom.buffers) {
+ if (globalRoom.buffers[i].length > 0) {
+ bufs.push(i)
+ }
+ }
+ if (bufs.length > 0) {
+ var userId = getRandom(bufs)
+ var m = globalRoom.buffers[userId].shift()
+ var user = globalRoom.users[userId]
+ user.receiveMessage(m[0], m[1])
+ return true
+ } else {
+ return false
+ }
+ }
+
+ // setInterval(flushOne, 10)
+
+ var userIdCounter = 0
+
+ class Test extends Y.AbstractConnector {
+ constructor (y, options) {
+ if (options === undefined) {
+ throw new Error('Options must not be undefined!')
+ }
+ options.role = 'master'
+ options.forwardToSyncingClients = false
+ super(y, options)
+ this.setUserId((userIdCounter++) + '').then(() => {
+ globalRoom.addUser(this)
+ })
+ this.globalRoom = globalRoom
+ this.syncingClientDuration = 0
+ }
+ receiveMessage (sender, m) {
+ super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
+ }
+ send (userId, message) {
+ var buffer = globalRoom.buffers[userId]
+ if (buffer != null) {
+ buffer.push(JSON.parse(JSON.stringify([this.userId, message])))
+ }
+ }
+ broadcast (message) {
+ for (var key in globalRoom.buffers) {
+ globalRoom.buffers[key].push(JSON.parse(JSON.stringify([this.userId, message])))
+ }
+ }
+ isDisconnected () {
+ return globalRoom.users[this.userId] == null
+ }
+ reconnect () {
+ if (this.isDisconnected()) {
+ globalRoom.addUser(this)
+ super.reconnect()
+ }
+ return this.flushAll()
+ }
+ disconnect () {
+ if (!this.isDisconnected()) {
+ globalRoom.removeUser(this.userId)
+ super.disconnect()
+ }
+ return wait()
+ }
+ flush () {
+ var self = this
+ return async(function * () {
+ yield wait()
+ while (globalRoom.buffers[self.userId].length > 0) {
+ var m = globalRoom.buffers[self.userId].shift()
+ this.receiveMessage(m[0], m[1])
+ yield wait()
+ }
+ })
+ }
+ flushAll () {
+ 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()
+ })
+ }
+ }
+ // in the case that there are
+ // still actions that want to be performed
+ wait().then(nextFlush)
+ })
+ }
+ /*
+ Flushes an operation for some user..
+ */
+ flushOne () {
+ flushOne()
+ }
+ }
+
+ Y.Test = Test
+}
diff --git a/src/Database.js b/src/Database.js
index c5e3e194..051a0148 100644
--- a/src/Database.js
+++ b/src/Database.js
@@ -1,341 +1,342 @@
-/* global Y */
'use strict'
-/*
- Partial definition of an OperationStore.
- TODO: name it Database, operation store only holds operations.
+module.exports = function (Y) {
+ /*
+ 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 AbstractDatabase {
- constructor (y, opts) {
- this.y = y
- // E.g. this.listenersById[id] : Array
- this.listenersById = {}
- // Execute the next time a transaction is requested
- this.listenersByIdExecuteNow = []
- // A transaction is requested
- this.listenersByIdRequestPending = false
- /* To make things more clear, the following naming conventions:
- * ls : we put this.listenersById on ls
- * l : Array
- * id : Id (can't use as property name)
- * sid : String (converted from id via JSON.stringify
- so we can use it as a property name)
+ 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 AbstractDatabase {
+ constructor (y, opts) {
+ this.y = y
+ // E.g. this.listenersById[id] : Array
+ this.listenersById = {}
+ // Execute the next time a transaction is requested
+ this.listenersByIdExecuteNow = []
+ // A transaction is requested
+ this.listenersByIdRequestPending = false
+ /* To make things more clear, the following naming conventions:
+ * ls : we put this.listenersById on ls
+ * l : Array
+ * id : Id (can't use as property name)
+ * sid : String (converted from id via JSON.stringify
+ so we can use it as a property name)
- Always remember to first overwrite
- a property before you iterate over it!
- */
- // TODO: Use ES7 Weak Maps. This way types that are no longer user,
- // wont be kept in memory.
- this.initializedTypes = {}
- this.whenUserIdSetListener = null
- this.waitingTransactions = []
- this.transactionInProgress = false
- if (typeof YConcurrency_TestingMode !== 'undefined') {
- this.executeOrder = []
- }
- this.gc1 = [] // first stage
- this.gc2 = [] // second stage -> after that, remove the op
- this.gcTimeout = opts.gcTimeout || 5000
- var os = this
- function garbageCollect () {
- return new Promise((resolve) => {
- os.requestTransaction(function * () {
- if (os.y.connector != null && os.y.connector.isSynced) {
- for (var i in os.gc2) {
- var oid = os.gc2[i]
- yield* this.garbageCollectOperation(oid)
+ Always remember to first overwrite
+ a property before you iterate over it!
+ */
+ // TODO: Use ES7 Weak Maps. This way types that are no longer user,
+ // wont be kept in memory.
+ this.initializedTypes = {}
+ this.whenUserIdSetListener = null
+ this.waitingTransactions = []
+ this.transactionInProgress = false
+ if (typeof YConcurrency_TestingMode !== 'undefined') {
+ this.executeOrder = []
+ }
+ this.gc1 = [] // first stage
+ this.gc2 = [] // second stage -> after that, remove the op
+ this.gcTimeout = opts.gcTimeout || 5000
+ var os = this
+ function garbageCollect () {
+ return new Promise((resolve) => {
+ os.requestTransaction(function * () {
+ if (os.y.connector != null && os.y.connector.isSynced) {
+ for (var i in os.gc2) {
+ var oid = os.gc2[i]
+ yield* this.garbageCollectOperation(oid)
+ }
+ os.gc2 = os.gc1
+ os.gc1 = []
}
- os.gc2 = os.gc1
- os.gc1 = []
+ if (os.gcTimeout > 0) {
+ os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
+ }
+ resolve()
+ })
+ })
+ }
+ this.garbageCollect = garbageCollect
+ if (this.gcTimeout > 0) {
+ garbageCollect()
+ }
+ }
+ addToDebug () {
+ if (typeof YConcurrency_TestingMode !== 'undefined') {
+ var command = Array.prototype.map.call(arguments, function (s) {
+ if (typeof s === 'string') {
+ return s
+ } else {
+ return JSON.stringify(s)
}
- if (os.gcTimeout > 0) {
- os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
+ }).join('').replace(/"/g, "'").replace(/,/g, ', ').replace(/:/g, ': ')
+ this.executeOrder.push(command)
+ }
+ }
+ getDebugData () {
+ console.log(this.executeOrder.join('\n'))
+ }
+ stopGarbageCollector () {
+ var self = this
+ return new Promise(function (resolve) {
+ self.requestTransaction(function * () {
+ var ungc = self.gc1.concat(self.gc2)
+ self.gc1 = []
+ self.gc2 = []
+ for (var i in ungc) {
+ var op = yield* this.getOperation(ungc[i])
+ delete op.gc
+ yield* this.setOperation(op)
}
resolve()
})
})
}
- this.garbageCollect = garbageCollect
- if (this.gcTimeout > 0) {
- garbageCollect()
- }
- }
- addToDebug () {
- if (typeof YConcurrency_TestingMode !== 'undefined') {
- var command = Array.prototype.map.call(arguments, function (s) {
- if (typeof s === 'string') {
- return s
- } else {
- return JSON.stringify(s)
- }
- }).join('').replace(/"/g, "'").replace(/,/g, ', ').replace(/:/g, ': ')
- this.executeOrder.push(command)
- }
- }
- getDebugData () {
- console.log(this.executeOrder.join('\n'))
- }
- stopGarbageCollector () {
- var self = this
- return new Promise(function (resolve) {
- self.requestTransaction(function * () {
- var ungc = self.gc1.concat(self.gc2)
- self.gc1 = []
- self.gc2 = []
- for (var i in ungc) {
- var op = yield* this.getOperation(ungc[i])
- delete op.gc
- yield* this.setOperation(op)
- }
- resolve()
- })
- })
- }
- /*
- Try to add to GC.
+ /*
+ Try to add to GC.
- TODO: rename this function
+ TODO: rename this function
- Rulez:
- * Only gc if this user is online
- * The most left element in a list must not be gc'd.
- => There is at least one element in the list
+ Rulez:
+ * Only gc if this user is online
+ * The most left element in a list must not be gc'd.
+ => There is at least one element in the list
- returns true iff op was added to GC
- */
- addToGarbageCollector (op, left) {
- if (
- op.gc == null &&
- op.deleted === true &&
- this.y.connector.isSynced &&
- left != null &&
- left.deleted === true
- ) {
- op.gc = true
- this.gc1.push(op.id)
- return true
- } else {
- return false
- }
- }
- removeFromGarbageCollector (op) {
- function filter (o) {
- return !Y.utils.compareIds(o, op.id)
- }
- this.gc1 = this.gc1.filter(filter)
- this.gc2 = this.gc2.filter(filter)
- delete op.gc
- }
- destroy () {
- clearInterval(this.gcInterval)
- this.gcInterval = null
- }
- setUserId (userId) {
- var self = this
- return new Promise(function (resolve) {
- self.requestTransaction(function * () {
- self.userId = userId
- self.opClock = (yield* this.getState(userId)).clock
- if (self.whenUserIdSetListener != null) {
- self.whenUserIdSetListener()
- self.whenUserIdSetListener = null
- }
- resolve()
- })
- })
- }
- whenUserIdSet (f) {
- if (this.userId != null) {
- f()
- } else {
- this.whenUserIdSetListener = f
- }
- }
- getNextOpId () {
- if (this.userId == null) {
- throw new Error('OperationStore not yet initialized!')
- }
- return [this.userId, this.opClock++]
- }
- /*
- Apply a list of operations.
-
- * get a transaction
- * check whether all Struct.*.requiredOps are in the OS
- * check if it is an expected op (otherwise wait for it)
- * check if was deleted, apply a delete operation after op was applied
- */
- apply (ops) {
- for (var key in ops) {
- var o = ops[key]
- var required = Y.Struct[o.struct].requiredOps(o)
- this.whenOperationsExist(required, o)
- }
- }
- /*
- op is executed as soon as every operation requested is available.
- Note that Transaction can (and should) buffer requests.
- */
- whenOperationsExist (ids, op) {
- if (ids.length > 0) {
- let listener = {
- op: op,
- missing: ids.length
+ returns true iff op was added to GC
+ */
+ addToGarbageCollector (op, left) {
+ if (
+ op.gc == null &&
+ op.deleted === true &&
+ this.y.connector.isSynced &&
+ left != null &&
+ left.deleted === true
+ ) {
+ op.gc = true
+ this.gc1.push(op.id)
+ return true
+ } else {
+ return false
}
-
- for (let key in ids) {
- let id = ids[key]
- let sid = JSON.stringify(id)
- let l = this.listenersById[sid]
- if (l == null) {
- l = []
- this.listenersById[sid] = l
- }
- l.push(listener)
+ }
+ removeFromGarbageCollector (op) {
+ function filter (o) {
+ return !Y.utils.compareIds(o, op.id)
}
- } else {
- this.listenersByIdExecuteNow.push({
- op: op
+ this.gc1 = this.gc1.filter(filter)
+ this.gc2 = this.gc2.filter(filter)
+ delete op.gc
+ }
+ destroy () {
+ clearInterval(this.gcInterval)
+ this.gcInterval = null
+ }
+ setUserId (userId) {
+ var self = this
+ return new Promise(function (resolve) {
+ self.requestTransaction(function * () {
+ self.userId = userId
+ self.opClock = (yield* this.getState(userId)).clock
+ if (self.whenUserIdSetListener != null) {
+ self.whenUserIdSetListener()
+ self.whenUserIdSetListener = null
+ }
+ resolve()
+ })
})
}
-
- if (this.listenersByIdRequestPending) {
- return
+ whenUserIdSet (f) {
+ if (this.userId != null) {
+ f()
+ } else {
+ this.whenUserIdSetListener = f
+ }
}
+ getNextOpId () {
+ if (this.userId == null) {
+ throw new Error('OperationStore not yet initialized!')
+ }
+ return [this.userId, this.opClock++]
+ }
+ /*
+ Apply a list of operations.
- this.listenersByIdRequestPending = true
- var store = this
+ * get a transaction
+ * check whether all Struct.*.requiredOps are in the OS
+ * check if it is an expected op (otherwise wait for it)
+ * check if was deleted, apply a delete operation after op was applied
+ */
+ apply (ops) {
+ for (var key in ops) {
+ var o = ops[key]
+ var required = Y.Struct[o.struct].requiredOps(o)
+ this.whenOperationsExist(required, o)
+ }
+ }
+ /*
+ op is executed as soon as every operation requested is available.
+ Note that Transaction can (and should) buffer requests.
+ */
+ whenOperationsExist (ids, op) {
+ if (ids.length > 0) {
+ let listener = {
+ op: op,
+ missing: ids.length
+ }
- this.requestTransaction(function * () {
- var exeNow = store.listenersByIdExecuteNow
- store.listenersByIdExecuteNow = []
-
- var ls = store.listenersById
- store.listenersById = {}
-
- store.listenersByIdRequestPending = false
-
- for (let key in exeNow) {
- let o = exeNow[key].op
- yield* store.tryExecute.call(this, o)
+ for (let key in ids) {
+ let id = ids[key]
+ let sid = JSON.stringify(id)
+ let l = this.listenersById[sid]
+ if (l == null) {
+ l = []
+ this.listenersById[sid] = l
+ }
+ l.push(listener)
+ }
+ } else {
+ this.listenersByIdExecuteNow.push({
+ op: op
+ })
}
- for (var sid in ls) {
- var l = ls[sid]
- var id = JSON.parse(sid)
- if ((yield* this.getOperation(id)) == null) {
- store.listenersById[sid] = l
- } else {
- for (let key in l) {
- let listener = l[key]
- let o = listener.op
- if (--listener.missing === 0) {
- yield* store.tryExecute.call(this, o)
+ if (this.listenersByIdRequestPending) {
+ return
+ }
+
+ this.listenersByIdRequestPending = true
+ var store = this
+
+ this.requestTransaction(function * () {
+ var exeNow = store.listenersByIdExecuteNow
+ store.listenersByIdExecuteNow = []
+
+ var ls = store.listenersById
+ store.listenersById = {}
+
+ store.listenersByIdRequestPending = false
+
+ for (let key in exeNow) {
+ let o = exeNow[key].op
+ yield* store.tryExecute.call(this, o)
+ }
+
+ for (var sid in ls) {
+ var l = ls[sid]
+ var id = JSON.parse(sid)
+ if ((yield* this.getOperation(id)) == null) {
+ store.listenersById[sid] = l
+ } else {
+ for (let key in l) {
+ let listener = l[key]
+ let o = listener.op
+ if (--listener.missing === 0) {
+ yield* store.tryExecute.call(this, o)
+ }
}
}
}
- }
- })
- }
- /*
- Actually execute an operation, when all expected operations are available.
- */
- * tryExecute (op) {
- this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
- if (op.struct === 'Delete') {
- yield* Y.Struct.Delete.execute.call(this, op)
- yield* this.store.operationAdded(this, op)
- } else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) {
- yield* Y.Struct[op.struct].execute.call(this, op)
- yield* this.addOperation(op)
- yield* this.store.operationAdded(this, op)
+ })
}
- }
- // called by a transaction when an operation is added
- * operationAdded (transaction, op) {
- if (op.struct === 'Delete') {
- var target = yield* transaction.getOperation(op.target)
- if (target != null) {
- var type = transaction.store.initializedTypes[JSON.stringify(target.parent)]
- if (type != null) {
- yield* type._changed(transaction, {
- struct: 'Delete',
- target: op.target
- })
+ /*
+ Actually execute an operation, when all expected operations are available.
+ */
+ * tryExecute (op) {
+ this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
+ if (op.struct === 'Delete') {
+ yield* Y.Struct.Delete.execute.call(this, op)
+ yield* this.store.operationAdded(this, op)
+ } else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) {
+ yield* Y.Struct[op.struct].execute.call(this, op)
+ yield* this.addOperation(op)
+ yield* this.store.operationAdded(this, op)
+ }
+ }
+ // called by a transaction when an operation is added
+ * operationAdded (transaction, op) {
+ if (op.struct === 'Delete') {
+ var target = yield* transaction.getOperation(op.target)
+ if (target != null) {
+ var type = transaction.store.initializedTypes[JSON.stringify(target.parent)]
+ if (type != null) {
+ yield* type._changed(transaction, {
+ struct: 'Delete',
+ target: op.target
+ })
+ }
}
- }
- } else {
- // increase SS
- var o = op
- var state = yield* transaction.getState(op.id[0])
- while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) {
- // either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
- state.clock++
- yield* transaction.checkDeleteStoreForState(state)
- o = yield* transaction.os.findNext(o.id)
- }
- yield* transaction.setState(state)
+ } else {
+ // increase SS
+ var o = op
+ var state = yield* transaction.getState(op.id[0])
+ while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) {
+ // either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
+ state.clock++
+ yield* transaction.checkDeleteStoreForState(state)
+ o = yield* transaction.os.findNext(o.id)
+ }
+ yield* transaction.setState(state)
- // notify whenOperation listeners (by id)
- var sid = JSON.stringify(op.id)
- var l = this.listenersById[sid]
- delete this.listenersById[sid]
+ // notify whenOperation listeners (by id)
+ var sid = JSON.stringify(op.id)
+ var l = this.listenersById[sid]
+ delete this.listenersById[sid]
- if (l != null) {
- for (var key in l) {
- var listener = l[key]
- if (--listener.missing === 0) {
- this.whenOperationsExist([], listener.op)
+ if (l != null) {
+ for (var key in l) {
+ var listener = l[key]
+ if (--listener.missing === 0) {
+ this.whenOperationsExist([], listener.op)
+ }
+ }
+ }
+ var t = this.initializedTypes[JSON.stringify(op.parent)]
+ // notify parent, if it has been initialized as a custom type
+ if (t != null) {
+ yield* t._changed(transaction, Y.utils.copyObject(op))
+ }
+
+ // Delete if DS says this is actually deleted
+ if (!op.deleted && (yield* transaction.isDeleted(op.id))) {
+ var delop = {
+ struct: 'Delete',
+ target: op.id
+ }
+ yield* Y.Struct['Delete'].execute.call(transaction, delop)
+ if (t != null) {
+ yield* t._changed(transaction, delop)
}
}
}
- var t = this.initializedTypes[JSON.stringify(op.parent)]
- // notify parent, if it has been initialized as a custom type
- if (t != null) {
- yield* t._changed(transaction, Y.utils.copyObject(op))
+ }
+ getNextRequest () {
+ if (this.waitingTransactions.length === 0) {
+ this.transactionInProgress = false
+ return null
+ } else {
+ return this.waitingTransactions.shift()
}
-
- // Delete if DS says this is actually deleted
- if (!op.deleted && (yield* transaction.isDeleted(op.id))) {
- var delop = {
- struct: 'Delete',
- target: op.id
- }
- yield* Y.Struct['Delete'].execute.call(transaction, delop)
- if (t != null) {
- yield* t._changed(transaction, delop)
- }
+ }
+ requestTransaction (makeGen, callImmediately) {
+ if (callImmediately) {
+ this.transact(makeGen)
+ } else if (!this.transactionInProgress) {
+ this.transactionInProgress = true
+ var self = this
+ setTimeout(function () {
+ self.transact(makeGen)
+ }, 0)
+ } else {
+ this.waitingTransactions.push(makeGen)
}
}
}
- getNextRequest () {
- if (this.waitingTransactions.length === 0) {
- this.transactionInProgress = false
- return null
- } else {
- return this.waitingTransactions.shift()
- }
- }
- requestTransaction (makeGen, callImmediately) {
- if (callImmediately) {
- this.transact(makeGen)
- } else if (!this.transactionInProgress) {
- this.transactionInProgress = true
- var self = this
- setTimeout(function () {
- self.transact(makeGen)
- }, 0)
- } else {
- this.waitingTransactions.push(makeGen)
- }
- }
+ Y.AbstractDatabase = AbstractDatabase
}
-Y.AbstractDatabase = AbstractDatabase
diff --git a/src/Database.spec.js b/src/Database.spec.js
index 2305bf21..d8ee4922 100644
--- a/src/Database.spec.js
+++ b/src/Database.spec.js
@@ -1,5 +1,8 @@
-/* global Y, async, databases */
+/* global async, databases */
/* eslint-env browser,jasmine,console */
+'use strict'
+
+var Y = require('./SpecHelper.js')
for (let database of databases) {
describe(`Database (${database})`, function () {
diff --git a/src/Databases/IndexedDB.js b/src/Databases/IndexedDB.js
index 93b9584f..060cebec 100644
--- a/src/Databases/IndexedDB.js
+++ b/src/Databases/IndexedDB.js
@@ -1,8 +1,6 @@
-/* global Y */
-
'use strict'
-Y.IndexedDB = (function () {
+module.exports = function (Y) {
class Store {
constructor (transaction, name) {
this.store = transaction.objectStore(name)
@@ -177,5 +175,5 @@ Y.IndexedDB = (function () {
yield window.indexedDB.deleteDatabase(this.namespace)
}
}
- return OperationStore
-})()
+ Y.IndexedDB = OperationStore
+}
diff --git a/src/Databases/IndexedDB.spec.js b/src/Databases/IndexedDB.spec.js
deleted file mode 100644
index 0b4e256c..00000000
--- a/src/Databases/IndexedDB.spec.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* global Y */
-/* eslint-env browser,jasmine */
-
-if (typeof window !== 'undefined' && false) {
- describe('IndexedDB', function () {
- var ob
- beforeAll(function () {
- ob = new Y.IndexedDB(null, {namespace: 'Test', gcTimeout: -1})
- })
-
- afterAll(function (done) {
- ob.requestTransaction(function *() {
- yield* ob.removeDatabase()
- ob = null
- done()
- })
- })
- })
-}
diff --git a/src/Databases/Memory.js b/src/Databases/Memory.js
index 1dcfffa8..69b2e830 100644
--- a/src/Databases/Memory.js
+++ b/src/Databases/Memory.js
@@ -1,7 +1,6 @@
-/* global Y */
'use strict'
-Y.Memory = (function () {
+module.exports = function (Y) {
class Transaction extends Y.Transaction {
constructor (store) {
super(store)
@@ -59,5 +58,5 @@ Y.Memory = (function () {
delete this.ds
}
}
- return Database
-})()
+ Y.Memory = Database
+}
diff --git a/src/Databases/RedBlackTree.js b/src/Databases/RedBlackTree.js
index a595cf03..5c06cc17 100644
--- a/src/Databases/RedBlackTree.js
+++ b/src/Databases/RedBlackTree.js
@@ -1,489 +1,490 @@
-/* global Y */
'use strict'
/*
This file contains a not so fancy implemantion of a Red Black Tree.
*/
-class N {
- // A created node is always red!
- constructor (val) {
- this.val = val
- this.color = true
- this._left = null
- this._right = null
- this._parent = null
- if (val.id === null) {
- throw new Error('You must define id!')
- }
- }
- isRed () { return this.color }
- isBlack () { return !this.color }
- redden () { this.color = true; return this }
- blacken () { this.color = false; return this }
- get grandparent () {
- return this.parent.parent
- }
- get parent () {
- return this._parent
- }
- get sibling () {
- return (this === this.parent.left)
- ? this.parent.right : this.parent.left
- }
- get left () {
- return this._left
- }
- get right () {
- return this._right
- }
- set left (n) {
- if (n !== null) {
- n._parent = this
- }
- this._left = n
- }
- set right (n) {
- if (n !== null) {
- n._parent = this
- }
- this._right = n
- }
- rotateLeft (tree) {
- var parent = this.parent
- var newParent = this.right
- var newRight = this.right.left
- newParent.left = this
- this.right = newRight
- if (parent === null) {
- tree.root = newParent
- newParent._parent = null
- } else if (parent.left === this) {
- parent.left = newParent
- } else if (parent.right === this) {
- parent.right = newParent
- } else {
- throw new Error('The elements are wrongly connected!')
- }
- }
- next () {
- if (this.right !== null) {
- // search the most left node in the right tree
- var o = this.right
- while (o.left !== null) {
- o = o.left
+module.exports = function (Y) {
+ class N {
+ // A created node is always red!
+ constructor (val) {
+ this.val = val
+ this.color = true
+ this._left = null
+ this._right = null
+ this._parent = null
+ if (val.id === null) {
+ throw new Error('You must define id!')
}
- return o
- } else {
- var p = this
- while (p.parent !== null && p !== p.parent.left) {
- p = p.parent
+ }
+ isRed () { return this.color }
+ isBlack () { return !this.color }
+ redden () { this.color = true; return this }
+ blacken () { this.color = false; return this }
+ get grandparent () {
+ return this.parent.parent
+ }
+ get parent () {
+ return this._parent
+ }
+ get sibling () {
+ return (this === this.parent.left)
+ ? this.parent.right : this.parent.left
+ }
+ get left () {
+ return this._left
+ }
+ get right () {
+ return this._right
+ }
+ set left (n) {
+ if (n !== null) {
+ n._parent = this
}
- return p.parent
+ this._left = n
}
- }
- prev () {
- if (this.left !== null) {
- // search the most right node in the left tree
- var o = this.left
- while (o.right !== null) {
- o = o.right
+ set right (n) {
+ if (n !== null) {
+ n._parent = this
}
- return o
- } else {
- var p = this
- while (p.parent !== null && p !== p.parent.right) {
- p = p.parent
+ this._right = n
+ }
+ rotateLeft (tree) {
+ var parent = this.parent
+ var newParent = this.right
+ var newRight = this.right.left
+ newParent.left = this
+ this.right = newRight
+ if (parent === null) {
+ tree.root = newParent
+ newParent._parent = null
+ } else if (parent.left === this) {
+ parent.left = newParent
+ } else if (parent.right === this) {
+ parent.right = newParent
+ } else {
+ throw new Error('The elements are wrongly connected!')
}
- return p.parent
}
- }
- rotateRight (tree) {
- var parent = this.parent
- var newParent = this.left
- var newLeft = this.left.right
- newParent.right = this
- this.left = newLeft
- if (parent === null) {
- tree.root = newParent
- newParent._parent = null
- } else if (parent.left === this) {
- parent.left = newParent
- } else if (parent.right === this) {
- parent.right = newParent
- } else {
- throw new Error('The elements are wrongly connected!')
- }
- }
- getUncle () {
- // we can assume that grandparent exists when this is called!
- if (this.parent === this.parent.parent.left) {
- return this.parent.parent.right
- } else {
- return this.parent.parent.left
- }
- }
-}
-
-class RBTree {
- constructor () {
- this.root = null
- this.length = 0
- }
- * findNext (id) {
- return yield* this.findWithLowerBound([id[0], id[1] + 1])
- }
- * findPrev (id) {
- return yield* this.findWithUpperBound([id[0], id[1] - 1])
- }
- findNodeWithLowerBound (from) {
- if (from === void 0) {
- throw new Error('You must define from!')
- }
- var o = this.root
- if (o === null) {
- return null
- } else {
- while (true) {
- if ((from === null || Y.utils.smaller(from, o.val.id)) && o.left !== null) {
- // o is included in the bound
- // try to find an element that is closer to the bound
+ next () {
+ if (this.right !== null) {
+ // search the most left node in the right tree
+ var o = this.right
+ while (o.left !== null) {
o = o.left
- } 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) {
+ }
+ return o
+ } else {
+ var p = this
+ while (p.parent !== null && p !== p.parent.left) {
+ p = p.parent
+ }
+ return p.parent
+ }
+ }
+ prev () {
+ if (this.left !== null) {
+ // search the most right node in the left tree
+ var o = this.left
+ while (o.right !== null) {
+ o = o.right
+ }
+ return o
+ } else {
+ var p = this
+ while (p.parent !== null && p !== p.parent.right) {
+ p = p.parent
+ }
+ return p.parent
+ }
+ }
+ rotateRight (tree) {
+ var parent = this.parent
+ var newParent = this.left
+ var newLeft = this.left.right
+ newParent.right = this
+ this.left = newLeft
+ if (parent === null) {
+ tree.root = newParent
+ newParent._parent = null
+ } else if (parent.left === this) {
+ parent.left = newParent
+ } else if (parent.right === this) {
+ parent.right = newParent
+ } else {
+ throw new Error('The elements are wrongly connected!')
+ }
+ }
+ getUncle () {
+ // we can assume that grandparent exists when this is called!
+ if (this.parent === this.parent.parent.left) {
+ return this.parent.parent.right
+ } else {
+ return this.parent.parent.left
+ }
+ }
+ }
+
+ class RBTree {
+ constructor () {
+ this.root = null
+ this.length = 0
+ }
+ * findNext (id) {
+ return yield* this.findWithLowerBound([id[0], id[1] + 1])
+ }
+ * findPrev (id) {
+ return yield* this.findWithUpperBound([id[0], id[1] - 1])
+ }
+ findNodeWithLowerBound (from) {
+ if (from === void 0) {
+ throw new Error('You must define from!')
+ }
+ var o = this.root
+ if (o === null) {
+ return null
+ } else {
+ while (true) {
+ if ((from === null || Y.utils.smaller(from, o.val.id)) && o.left !== null) {
+ // o is included in the bound
+ // try to find an element that is closer to the bound
+ o = o.left
+ } else if (from !== null && Y.utils.smaller(o.val.id, from)) {
+ // o is not within the bound, maybe one of the right elements is..
+ if (o.right !== null) {
+ o = o.right
+ } else {
+ // there is no right element. Search for the next bigger element,
+ // this should be within the bounds
+ return o.next()
+ }
+ } else {
+ return o
+ }
+ }
+ }
+ }
+ findNodeWithUpperBound (to) {
+ if (to === void 0) {
+ throw new Error('You must define from!')
+ }
+ var o = this.root
+ if (o === null) {
+ return null
+ } else {
+ while (true) {
+ if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) {
+ // o is included in the bound
+ // try to find an element that is closer to the bound
+ o = o.right
+ } else if (to !== null && Y.utils.smaller(to, o.val.id)) {
+ // o is not within the bound, maybe one of the left elements is..
+ if (o.left !== null) {
+ o = o.left
+ } else {
+ // there is no left element. Search for the prev smaller element,
+ // this should be within the bounds
+ return o.prev()
+ }
+ } else {
+ return o
+ }
+ }
+ }
+ }
+ * findWithLowerBound (from) {
+ var n = this.findNodeWithLowerBound(from)
+ return n == null ? null : n.val
+ }
+ * findWithUpperBound (to) {
+ var n = this.findNodeWithUpperBound(to)
+ return n == null ? null : n.val
+ }
+ * iterate (t, from, to, f) {
+ var o = this.findNodeWithLowerBound(from)
+ while (o !== null && (to === null || Y.utils.smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) {
+ yield* f.call(t, o.val)
+ o = o.next()
+ }
+ return true
+ }
+ * logTable (from, to, filter) {
+ if (filter == null) {
+ filter = function () {
+ return true
+ }
+ }
+ if (from == null) { from = null }
+ if (to == null) { to = null }
+ var os = []
+ yield* this.iterate(this, from, to, function * (o) {
+ if (filter(o)) {
+ var o_ = {}
+ for (var key in o) {
+ if (typeof o[key] === 'object') {
+ o_[key] = JSON.stringify(o[key])
+ } else {
+ o_[key] = o[key]
+ }
+ }
+ os.push(o_)
+ }
+ })
+ if (console.table != null) {
+ console.table(os)
+ }
+ }
+ * find (id) {
+ var n
+ return (n = this.findNode(id)) ? n.val : null
+ }
+ findNode (id) {
+ if (id == null || id.constructor !== Array) {
+ throw new Error('Expect id to be an array!')
+ }
+ var o = this.root
+ if (o === null) {
+ return false
+ } else {
+ while (true) {
+ if (o === null) {
+ return false
+ }
+ if (Y.utils.smaller(id, o.val.id)) {
+ o = o.left
+ } else if (Y.utils.smaller(o.val.id, id)) {
o = o.right
} else {
- // there is no right element. Search for the next bigger element,
- // this should be within the bounds
- return o.next()
+ return o
}
- } else {
- return o
}
}
}
- }
- findNodeWithUpperBound (to) {
- if (to === void 0) {
- throw new Error('You must define from!')
- }
- var o = this.root
- if (o === null) {
- return null
- } else {
- while (true) {
- if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) {
- // o is included in the bound
- // try to find an element that is closer to the bound
+ * delete (id) {
+ if (id == null || id.constructor !== Array) {
+ throw new Error('id is expected to be an Array!')
+ }
+ var d = this.findNode(id)
+ if (d == null) {
+ throw new Error('Element does not exist!')
+ }
+ this.length--
+ if (d.left !== null && d.right !== null) {
+ // switch d with the greates element in the left subtree.
+ // o should have at most one child.
+ var o = d.left
+ // find
+ while (o.right !== null) {
o = o.right
- } else if (to !== null && Y.utils.smaller(to, o.val.id)) {
- // o is not within the bound, maybe one of the left elements is..
- if (o.left !== null) {
- o = o.left
- } else {
- // there is no left element. Search for the prev smaller element,
- // this should be within the bounds
- return o.prev()
- }
- } else {
- return o
}
+ // switch
+ d.val = o.val
+ d = o
}
- }
- }
- * findWithLowerBound (from) {
- var n = this.findNodeWithLowerBound(from)
- return n == null ? null : n.val
- }
- * findWithUpperBound (to) {
- var n = this.findNodeWithUpperBound(to)
- return n == null ? null : n.val
- }
- * iterate (t, from, to, f) {
- var o = this.findNodeWithLowerBound(from)
- while (o !== null && (to === null || Y.utils.smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) {
- yield* f.call(t, o.val)
- o = o.next()
- }
- return true
- }
- * logTable (from, to, filter) {
- if (filter == null) {
- filter = function () {
- return true
+ // d has at most one child
+ // let n be the node that replaces d
+ var isFakeChild
+ var child = d.left || d.right
+ if (child === null) {
+ isFakeChild = true
+ child = new N({id: 0})
+ child.blacken()
+ d.right = child
+ } else {
+ isFakeChild = false
}
- }
- if (from == null) { from = null }
- if (to == null) { to = null }
- var os = []
- yield* this.iterate(this, from, to, function * (o) {
- if (filter(o)) {
- var o_ = {}
- for (var key in o) {
- if (typeof o[key] === 'object') {
- o_[key] = JSON.stringify(o[key])
- } else {
- o_[key] = o[key]
- }
- }
- os.push(o_)
- }
- })
- if (console.table != null) {
- console.table(os)
- }
- }
- * find (id) {
- var n
- return (n = this.findNode(id)) ? n.val : null
- }
- findNode (id) {
- if (id == null || id.constructor !== Array) {
- throw new Error('Expect id to be an array!')
- }
- var o = this.root
- if (o === null) {
- return false
- } else {
- while (true) {
- if (o === null) {
- return false
- }
- if (Y.utils.smaller(id, o.val.id)) {
- o = o.left
- } else if (Y.utils.smaller(o.val.id, id)) {
- o = o.right
- } else {
- return o
- }
- }
- }
- }
- * delete (id) {
- if (id == null || id.constructor !== Array) {
- throw new Error('id is expected to be an Array!')
- }
- var d = this.findNode(id)
- if (d == null) {
- throw new Error('Element does not exist!')
- }
- this.length--
- if (d.left !== null && d.right !== null) {
- // switch d with the greates element in the left subtree.
- // o should have at most one child.
- var o = d.left
- // find
- while (o.right !== null) {
- o = o.right
- }
- // switch
- d.val = o.val
- d = o
- }
- // d has at most one child
- // let n be the node that replaces d
- var isFakeChild
- var child = d.left || d.right
- if (child === null) {
- isFakeChild = true
- child = new N({id: 0})
- child.blacken()
- d.right = child
- } else {
- isFakeChild = false
- }
- if (d.parent === null) {
- if (!isFakeChild) {
- this.root = child
- child.blacken()
- child._parent = null
+ if (d.parent === null) {
+ if (!isFakeChild) {
+ this.root = child
+ child.blacken()
+ child._parent = null
+ } else {
+ this.root = null
+ }
+ return
+ } else if (d.parent.left === d) {
+ d.parent.left = child
+ } else if (d.parent.right === d) {
+ d.parent.right = child
} else {
- this.root = null
+ throw new Error('Impossible!')
}
- return
- } else if (d.parent.left === d) {
- d.parent.left = child
- } else if (d.parent.right === d) {
- d.parent.right = child
- } else {
- throw new Error('Impossible!')
- }
- if (d.isBlack()) {
- if (child.isRed()) {
- child.blacken()
- } else {
- this._fixDelete(child)
+ if (d.isBlack()) {
+ if (child.isRed()) {
+ child.blacken()
+ } else {
+ this._fixDelete(child)
+ }
+ }
+ this.root.blacken()
+ if (isFakeChild) {
+ if (child.parent.left === child) {
+ child.parent.left = null
+ } else if (child.parent.right === child) {
+ child.parent.right = null
+ } else {
+ throw new Error('Impossible #3')
+ }
}
}
- this.root.blacken()
- if (isFakeChild) {
- if (child.parent.left === child) {
- child.parent.left = null
- } else if (child.parent.right === child) {
- child.parent.right = null
- } else {
- throw new Error('Impossible #3')
+ _fixDelete (n) {
+ function isBlack (node) {
+ return node !== null ? node.isBlack() : true
}
- }
- }
- _fixDelete (n) {
- function isBlack (node) {
- return node !== null ? node.isBlack() : true
- }
- function isRed (node) {
- return node !== null ? node.isRed() : false
- }
- if (n.parent === null) {
- // this can only be called after the first iteration of fixDelete.
- return
- }
- // d was already replaced by the child
- // d is not the root
- // d and child are black
- var sibling = n.sibling
- if (isRed(sibling)) {
- // make sibling the grandfather
- n.parent.redden()
- sibling.blacken()
- if (n === n.parent.left) {
- n.parent.rotateLeft(this)
- } else if (n === n.parent.right) {
- n.parent.rotateRight(this)
- } else {
- throw new Error('Impossible #2')
+ function isRed (node) {
+ return node !== null ? node.isRed() : false
}
- sibling = n.sibling
- }
- // parent, sibling, and children of n are black
- if (n.parent.isBlack() &&
- sibling.isBlack() &&
- isBlack(sibling.left) &&
- isBlack(sibling.right)
- ) {
- sibling.redden()
- this._fixDelete(n.parent)
- } else if (n.parent.isRed() &&
- sibling.isBlack() &&
- isBlack(sibling.left) &&
- isBlack(sibling.right)
- ) {
- sibling.redden()
- n.parent.blacken()
- } else {
- if (n === n.parent.left &&
+ if (n.parent === null) {
+ // this can only be called after the first iteration of fixDelete.
+ return
+ }
+ // d was already replaced by the child
+ // d is not the root
+ // d and child are black
+ var sibling = n.sibling
+ if (isRed(sibling)) {
+ // make sibling the grandfather
+ n.parent.redden()
+ sibling.blacken()
+ if (n === n.parent.left) {
+ n.parent.rotateLeft(this)
+ } else if (n === n.parent.right) {
+ n.parent.rotateRight(this)
+ } else {
+ throw new Error('Impossible #2')
+ }
+ sibling = n.sibling
+ }
+ // parent, sibling, and children of n are black
+ if (n.parent.isBlack() &&
sibling.isBlack() &&
- isRed(sibling.left) &&
+ isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
- sibling.left.blacken()
- sibling.rotateRight(this)
- sibling = n.sibling
- } else if (n === n.parent.right &&
+ this._fixDelete(n.parent)
+ } else if (n.parent.isRed() &&
sibling.isBlack() &&
- isRed(sibling.right) &&
- isBlack(sibling.left)
+ isBlack(sibling.left) &&
+ isBlack(sibling.right)
) {
sibling.redden()
- sibling.right.blacken()
- sibling.rotateLeft(this)
- sibling = n.sibling
- }
- sibling.color = n.parent.color
- n.parent.blacken()
- if (n === n.parent.left) {
- sibling.right.blacken()
- n.parent.rotateLeft(this)
+ n.parent.blacken()
} else {
- sibling.left.blacken()
- n.parent.rotateRight(this)
- }
- }
- }
- * put (v) {
- if (v == null || v.id == null || v.id.constructor !== Array) {
- throw new Error('v is expected to have an id property which is an Array!')
- }
- var node = new N(v)
- if (this.root !== null) {
- var p = this.root // p abbrev. parent
- while (true) {
- if (Y.utils.smaller(node.val.id, p.val.id)) {
- if (p.left === null) {
- p.left = node
- break
- } else {
- p = p.left
- }
- } else if (Y.utils.smaller(p.val.id, node.val.id)) {
- if (p.right === null) {
- p.right = node
- break
- } else {
- p = p.right
- }
+ if (n === n.parent.left &&
+ sibling.isBlack() &&
+ isRed(sibling.left) &&
+ isBlack(sibling.right)
+ ) {
+ sibling.redden()
+ sibling.left.blacken()
+ sibling.rotateRight(this)
+ sibling = n.sibling
+ } else if (n === n.parent.right &&
+ sibling.isBlack() &&
+ isRed(sibling.right) &&
+ isBlack(sibling.left)
+ ) {
+ sibling.redden()
+ sibling.right.blacken()
+ sibling.rotateLeft(this)
+ sibling = n.sibling
+ }
+ sibling.color = n.parent.color
+ n.parent.blacken()
+ if (n === n.parent.left) {
+ sibling.right.blacken()
+ n.parent.rotateLeft(this)
} else {
- p.val = node.val
- return p
+ sibling.left.blacken()
+ n.parent.rotateRight(this)
}
}
- this._fixInsert(node)
- } else {
- this.root = node
}
- this.length++
- this.root.blacken()
- return node
- }
- _fixInsert (n) {
- if (n.parent === null) {
- n.blacken()
- return
- } else if (n.parent.isBlack()) {
- return
- }
- var uncle = n.getUncle()
- if (uncle !== null && uncle.isRed()) {
- // Note: parent: red, uncle: red
- n.parent.blacken()
- uncle.blacken()
- n.grandparent.redden()
- this._fixInsert(n.grandparent)
- } else {
- // Note: parent: red, uncle: black or null
- // Now we transform the tree in such a way that
- // either of these holds:
- // 1) grandparent.left.isRed
- // and grandparent.left.left.isRed
- // 2) grandparent.right.isRed
- // and grandparent.right.right.isRed
- if (n === n.parent.right && n.parent === n.grandparent.left) {
- n.parent.rotateLeft(this)
- // Since we rotated and want to use the previous
- // cases, we need to set n in such a way that
- // n.parent.isRed again
- n = n.left
- } else if (n === n.parent.left && n.parent === n.grandparent.right) {
- n.parent.rotateRight(this)
- // see above
- n = n.right
+ * put (v) {
+ if (v == null || v.id == null || v.id.constructor !== Array) {
+ throw new Error('v is expected to have an id property which is an Array!')
}
- // Case 1) or 2) hold from here on.
- // Now traverse grandparent, make parent a black node
- // on the highest level which holds two red nodes.
- n.parent.blacken()
- n.grandparent.redden()
- if (n === n.parent.left) {
- // Case 1
- n.grandparent.rotateRight(this)
+ var node = new N(v)
+ if (this.root !== null) {
+ var p = this.root // p abbrev. parent
+ while (true) {
+ if (Y.utils.smaller(node.val.id, p.val.id)) {
+ if (p.left === null) {
+ p.left = node
+ break
+ } else {
+ p = p.left
+ }
+ } else if (Y.utils.smaller(p.val.id, node.val.id)) {
+ if (p.right === null) {
+ p.right = node
+ break
+ } else {
+ p = p.right
+ }
+ } else {
+ p.val = node.val
+ return p
+ }
+ }
+ this._fixInsert(node)
} else {
- // Case 2
- n.grandparent.rotateLeft(this)
+ this.root = node
+ }
+ this.length++
+ this.root.blacken()
+ return node
+ }
+ _fixInsert (n) {
+ if (n.parent === null) {
+ n.blacken()
+ return
+ } else if (n.parent.isBlack()) {
+ return
+ }
+ var uncle = n.getUncle()
+ if (uncle !== null && uncle.isRed()) {
+ // Note: parent: red, uncle: red
+ n.parent.blacken()
+ uncle.blacken()
+ n.grandparent.redden()
+ this._fixInsert(n.grandparent)
+ } else {
+ // Note: parent: red, uncle: black or null
+ // Now we transform the tree in such a way that
+ // either of these holds:
+ // 1) grandparent.left.isRed
+ // and grandparent.left.left.isRed
+ // 2) grandparent.right.isRed
+ // and grandparent.right.right.isRed
+ if (n === n.parent.right && n.parent === n.grandparent.left) {
+ n.parent.rotateLeft(this)
+ // Since we rotated and want to use the previous
+ // cases, we need to set n in such a way that
+ // n.parent.isRed again
+ n = n.left
+ } else if (n === n.parent.left && n.parent === n.grandparent.right) {
+ n.parent.rotateRight(this)
+ // see above
+ n = n.right
+ }
+ // Case 1) or 2) hold from here on.
+ // Now traverse grandparent, make parent a black node
+ // on the highest level which holds two red nodes.
+ n.parent.blacken()
+ n.grandparent.redden()
+ if (n === n.parent.left) {
+ // Case 1
+ n.grandparent.rotateRight(this)
+ } else {
+ // Case 2
+ n.grandparent.rotateLeft(this)
+ }
}
}
}
-}
-Y.utils.RBTree = RBTree
+ Y.utils.RBTree = RBTree
+}
diff --git a/src/Databases/RedBlackTree.spec.js b/src/Databases/RedBlackTree.spec.js
index 905f7bec..62a9d9b8 100644
--- a/src/Databases/RedBlackTree.spec.js
+++ b/src/Databases/RedBlackTree.spec.js
@@ -1,6 +1,7 @@
-/* global Y */
/* eslint-env browser,jasmine,console */
+'use strict'
+var Y = require('../SpecHelper.js')
var numberOfRBTreeTests = 1000
function itRedNodesDoNotHaveBlackChildren () {
diff --git a/src/Helper.spec.js b/src/Helper.spec.js
deleted file mode 100644
index 6ac70ec7..00000000
--- a/src/Helper.spec.js
+++ /dev/null
@@ -1,288 +0,0 @@
-/* global Y */
-/* eslint-env browser, jasmine */
-
-/*
- 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
-} else if (typeof window !== 'undefined') {
- g = window
-} else {
- throw new Error('No global object?')
-}
-g.g = g
-
-g.YConcurrency_TestingMode = true
-
-jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000
-
-g.describeManyTimes = function describeManyTimes (times, name, f) {
- for (var i = 0; i < times; i++) {
- describe(name, f)
- }
-}
-
-/*
- Wait for a specified amount of time (in ms). defaults to 5ms
-*/
-function wait (t) {
- if (t == null) {
- t = 5
- }
- return new Promise(function (resolve) {
- setTimeout(function () {
- resolve()
- }, t * 2)
- })
-}
-g.wait = wait
-
-g.databases = ['Memory']
-if (typeof window !== 'undefined') {
- g.databases.push('IndexedDB')
-}
-
-/*
- 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)]
- } else if (o.constructor === Object) {
- var ks = []
- for (var key in o) {
- ks.push(key)
- }
- return o[getRandom(ks)]
- }
-}
-g.getRandom = getRandom
-
-function getRandomNumber (n) {
- if (n == null) {
- n = 9999
- }
- return Math.floor(Math.random() * n)
-}
-g.getRandomNumber = getRandomNumber
-
-function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions) {
- function randomTransaction (root) {
- var f = getRandom(transactions)
- f(root)
- }
- for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) {
- var r = Math.random()
- if (r >= 0.5) {
- // 50% chance to flush
- users[0].connector.flushOne() // flushes for some user.. (not necessarily 0)
- } else if (r >= 0.05) {
- // 45% chance to create operation
- randomTransaction(getRandom(objects))
- } else {
- // 5% chance to disconnect/reconnect
- var u = getRandom(users)
- if (u.connector.isDisconnected()) {
- yield u.reconnect()
- } else {
- yield u.disconnect()
- }
- }
- yield wait()
- }
-}
-
-g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
- yield* applyTransactions(1, numberOfTransactions, objects, users, transactions)
- yield users[0].connector.flushAll()
- yield wait()
- for (var u in users) {
- yield users[u].reconnect()
- }
- yield wait(100)
- yield users[0].connector.flushAll()
- yield g.garbageCollectAllUsers(users)
-})
-
-g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
- yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
- yield users[0].connector.flushAll()
- yield g.garbageCollectAllUsers(users)
- yield wait(100)
- for (var u in users) {
- // TODO: here, we enforce that two users never sync at the same time with u[0]
- // enforce that in the connector itself!
- yield users[u].reconnect()
- }
- yield wait(100)
- yield users[0].connector.flushAll()
- yield wait(100)
- yield g.garbageCollectAllUsers(users)
-})
-
-g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
- // gc two times because of the two gc phases (really collect everything)
- yield wait(100)
- for (var i in users) {
- yield users[i].db.garbageCollect()
- yield users[i].db.garbageCollect()
- }
- yield wait(100)
-})
-
-g.compareAllUsers = async(function * compareAllUsers (users) {
- 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()
- allDels1 = []
- yield* this.ds.iterate(this, null, null, function * (d) {
- allDels1.push(d)
- })
- }
- function * t2 () {
- s2 = yield* this.getStateSet()
- ds2 = yield* this.getDeleteSet()
- allDels2 = []
- yield* this.ds.iterate(this, null, null, function * (d) {
- allDels2.push(d)
- })
- }
- yield users[0].connector.flushAll()
- yield wait()
- yield g.garbageCollectAllUsers(users)
-
- for (var uid = 0; uid < users.length; uid++) {
- var u = users[uid]
- u.db.requestTransaction(function * () {
- // compare deleted ops against deleteStore
- yield* this.os.iterate(this, null, null, function * (o) {
- if (o.deleted === true) {
- expect(yield* this.isDeleted(o.id)).toBeTruthy()
- }
- })
- // compare deleteStore against deleted ops
- var ds = []
- yield* this.ds.iterate(this, null, null, function * (d) {
- ds.push(d)
- })
- for (var j in ds) {
- var d = ds[j]
- for (var i = 0; i < d.len; i++) {
- var o = yield* this.getOperation([d.id[0], d.id[1] + i])
- // gc'd or deleted
- if (d.gc) {
- expect(o).toBeFalsy()
- } else {
- expect(o.deleted).toBeTruthy()
- }
- }
- }
- })
- // compare allDels tree
- yield wait()
- if (s1 == null) {
- u.db.requestTransaction(function * () {
- yield* t1.call(this)
- yield* this.os.iterate(this, null, null, function * (o) {
- o = Y.utils.copyObject(o)
- delete o.origin
- db1.push(o)
- })
- })
- yield wait()
- } else {
- // TODO: make requestTransaction return a promise..
- u.db.requestTransaction(function * () {
- yield* t2.call(this)
- expect(s1).toEqual(s2)
- expect(allDels1).toEqual(allDels2) // inner structure
- expect(ds1).toEqual(ds2) // exported structure
- var count = 0
- yield* this.os.iterate(this, null, null, function * (o) {
- o = Y.utils.copyObject(o)
- delete o.origin
- expect(db1[count++]).toEqual(o)
- })
- })
- yield wait()
- }
- }
-})
-
-g.createUsers = async(function * createUsers (self, numberOfUsers, database) {
- 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) {
- Y.utils.globalRoom.users[u].y.destroy()
- }
- self.users = null
-
- var promises = []
- for (var i = 0; i < numberOfUsers; i++) {
- promises.push(Y({
- db: {
- name: database,
- namespace: 'User ' + i,
- cleanStart: true,
- gcTimeout: -1
- },
- connector: {
- name: 'Test',
- debug: false
- }
- }))
- }
- 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) {
- if (result.done) return Promise.resolve(result.value)
-
- return Promise.resolve(result.value).then(function (res) {
- return handle(generator.next(res))
- }, function (err) {
- return handle(generator.throw(err))
- })
- }
- try {
- return handle(generator.next())
- } catch (ex) {
- generator.throw(ex)
- // return Promise.reject(ex)
- }
- }
-}
-g.async = async
-
-function logUsers (self) {
- if (self.constructor === Array) {
- self = {users: self}
- }
- self.users[0].db.logTable()
- self.users[1].db.logTable()
- self.users[2].db.logTable()
-}
-
-g.logUsers = logUsers
diff --git a/src/Struct.js b/src/Struct.js
index fb1f72e1..f8cc40ba 100644
--- a/src/Struct.js
+++ b/src/Struct.js
@@ -1,4 +1,3 @@
-/* global Y */
'use strict'
/*
@@ -19,318 +18,319 @@
* requiredOps
- Operations that are required to execute this operation.
*/
+module.exports = function (Y) {
+ var Struct = {
+ /* 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
-var Struct = {
- /* 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: {
- encode: function (op) {
- return op
- },
- requiredOps: function (op) {
- return [] // [op.target]
- },
- execute: function * (op) {
- return yield* this.deleteOperation(op.target)
- }
- },
- Insert: {
- /* {
- content: any,
- id: Id,
- left: Id,
- origin: Id,
- right: Id,
- parent: Id,
- parentSub: string (optional), // child of Map type
- }
- */
- encode: function (op) {
- // TODO: you could not send the "left" property, then you also have to
- // "op.left = null" in $execute or $decode
- var e = {
- id: op.id,
- left: op.left,
- right: op.right,
- origin: op.origin,
- parent: op.parent,
- struct: op.struct
- }
- if (op.parentSub != null) {
- e.parentSub = op.parentSub
- }
- if (op.opContent != null) {
- e.opContent = op.opContent
- } else {
- e.content = op.content
- }
-
- return e
- },
- requiredOps: function (op) {
- var ids = []
- if (op.left != null) {
- ids.push(op.left)
- }
- if (op.right != null) {
- ids.push(op.right)
- }
- if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
- ids.push(op.origin)
- }
- // if (op.right == null && op.left == null) {
- ids.push(op.parent)
-
- if (op.opContent != null) {
- ids.push(op.opContent)
- }
- return ids
- },
- getDistanceToOrigin: function * (op) {
- if (op.left == null) {
- return 0
- } else {
- var d = 0
- var o = yield* this.getOperation(op.left)
- while (!Y.utils.compareIds(op.origin, (o ? o.id : null))) {
- d++
- if (o.left == null) {
- break
- } else {
- o = yield* this.getOperation(o.left)
- }
- }
- return d
- }
- },
- /*
- # $this has to find a unique position between origin and the next known character
- # case 1: $origin equals $o.origin: the $creator parameter decides if left or right
- # let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
- # o2,o3 and o4 origin is 1 (the position of o2)
- # there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
- # then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
- # therefore $this would be always to the right of o3
- # case 2: $origin < $o.origin
- # if current $this insert_position > $o origin: $this ins
- # else $insert_position will not change
- # (maybe we encounter case 1 later, then this will be to the right of $o)
- # case 3: $origin > $o.origin
- # $this insert_position is to the left of $o (forever!)
- */
- execute: function *(op) {
- var i // loop counter
- var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
- var o
- var parent
- var start
-
- // find o. o is the first conflicting operation
- if (op.left != null) {
- o = yield* this.getOperation(op.left)
- o = (o.right == null) ? null : yield* this.getOperation(o.right)
- } else { // left == null
- parent = yield* this.getOperation(op.parent)
- let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
- start = startId == null ? null : yield* this.getOperation(startId)
- o = start
- }
-
- // handle conflicts
- while (true) {
- if (o != null && !Y.utils.compareIds(o.id, op.right)) {
- var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
- if (oOriginDistance === i) {
- // case 1
- if (o.id[0] < op.id[0]) {
- op.left = o.id
- distanceToOrigin = i + 1
- }
- } else if (oOriginDistance < i) {
- // case 2
- if (i - distanceToOrigin <= oOriginDistance) {
- op.left = o.id
- distanceToOrigin = i + 1
- }
- } else {
- break
- }
- i++
- o = o.right ? yield* this.getOperation(o.right) : null
- } else {
- break
- }
- }
-
- // reconnect..
- var left = null
- var right = null
- parent = parent || (yield* this.getOperation(op.parent))
-
- // reconnect left and set right of op
- if (op.left != null) {
- left = yield* this.getOperation(op.left)
- op.right = left.right
- left.right = op.id
-
- yield* this.setOperation(left)
- } else {
- op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
- }
- // reconnect right
- if (op.right != null) {
- right = yield* this.getOperation(op.right)
- right.left = op.id
-
- // if right exists, and it is supposed to be gc'd. Remove it from the gc
- if (right.gc != null) {
- this.store.removeFromGarbageCollector(right)
- }
- yield* this.setOperation(right)
- }
-
- // update parents .map/start/end properties
- if (op.parentSub != null) {
- if (left == null) {
- parent.map[op.parentSub] = op.id
- yield* this.setOperation(parent)
- }
- // is a child of a map struct.
- // Then also make sure that only the most left element is not deleted
- if (op.right != null) {
- yield* this.deleteOperation(op.right, true)
- }
- if (op.left != null) {
- yield* this.deleteOperation(op.id, true)
- }
- } else {
- if (right == null || left == null) {
- if (right == null) {
- parent.end = op.id
- }
- if (left == null) {
- parent.start = op.id
- }
- yield* this.setOperation(parent)
- }
- }
- }
- },
- List: {
- /*
- {
- start: null,
- end: null,
- struct: "List",
- type: "",
- id: this.os.getNextOpId()
+ op = {
+ target: Id
}
*/
- encode: function (op) {
- return {
- struct: 'List',
- id: op.id,
- type: op.type
+ Delete: {
+ encode: function (op) {
+ return op
+ },
+ requiredOps: function (op) {
+ return [] // [op.target]
+ },
+ execute: function * (op) {
+ return yield* this.deleteOperation(op.target)
}
},
- requiredOps: function () {
- /*
- var ids = []
- if (op.start != null) {
- ids.push(op.start)
- }
- if (op.end != null){
- ids.push(op.end)
- }
- return ids
+ Insert: {
+ /* {
+ content: any,
+ id: Id,
+ left: Id,
+ origin: Id,
+ right: Id,
+ parent: Id,
+ parentSub: string (optional), // child of Map type
+ }
*/
- return []
- },
- execute: function * (op) {
- op.start = null
- op.end = null
- },
- ref: function * (op, pos) {
- if (op.start == null) {
- return null
- }
- var res = null
- var o = yield* this.getOperation(op.start)
-
- while (true) {
- if (!o.deleted) {
- res = o
- pos--
+ encode: function (op) {
+ // TODO: you could not send the "left" property, then you also have to
+ // "op.left = null" in $execute or $decode
+ var e = {
+ id: op.id,
+ left: op.left,
+ right: op.right,
+ origin: op.origin,
+ parent: op.parent,
+ struct: op.struct
}
- if (pos >= 0 && o.right != null) {
- o = (yield* this.getOperation(o.right))
+ if (op.parentSub != null) {
+ e.parentSub = op.parentSub
+ }
+ if (op.opContent != null) {
+ e.opContent = op.opContent
} else {
- break
+ e.content = op.content
+ }
+
+ return e
+ },
+ requiredOps: function (op) {
+ var ids = []
+ if (op.left != null) {
+ ids.push(op.left)
+ }
+ if (op.right != null) {
+ ids.push(op.right)
+ }
+ if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
+ ids.push(op.origin)
+ }
+ // if (op.right == null && op.left == null) {
+ ids.push(op.parent)
+
+ if (op.opContent != null) {
+ ids.push(op.opContent)
+ }
+ return ids
+ },
+ getDistanceToOrigin: function * (op) {
+ if (op.left == null) {
+ return 0
+ } else {
+ var d = 0
+ var o = yield* this.getOperation(op.left)
+ while (!Y.utils.compareIds(op.origin, (o ? o.id : null))) {
+ d++
+ if (o.left == null) {
+ break
+ } else {
+ o = yield* this.getOperation(o.left)
+ }
+ }
+ return d
+ }
+ },
+ /*
+ # $this has to find a unique position between origin and the next known character
+ # case 1: $origin equals $o.origin: the $creator parameter decides if left or right
+ # let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
+ # o2,o3 and o4 origin is 1 (the position of o2)
+ # there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
+ # then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
+ # therefore $this would be always to the right of o3
+ # case 2: $origin < $o.origin
+ # if current $this insert_position > $o origin: $this ins
+ # else $insert_position will not change
+ # (maybe we encounter case 1 later, then this will be to the right of $o)
+ # case 3: $origin > $o.origin
+ # $this insert_position is to the left of $o (forever!)
+ */
+ execute: function *(op) {
+ var i // loop counter
+ var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
+ var o
+ var parent
+ var start
+
+ // find o. o is the first conflicting operation
+ if (op.left != null) {
+ o = yield* this.getOperation(op.left)
+ o = (o.right == null) ? null : yield* this.getOperation(o.right)
+ } else { // left == null
+ parent = yield* this.getOperation(op.parent)
+ let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
+ start = startId == null ? null : yield* this.getOperation(startId)
+ o = start
+ }
+
+ // handle conflicts
+ while (true) {
+ if (o != null && !Y.utils.compareIds(o.id, op.right)) {
+ var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
+ if (oOriginDistance === i) {
+ // case 1
+ if (o.id[0] < op.id[0]) {
+ op.left = o.id
+ distanceToOrigin = i + 1
+ }
+ } else if (oOriginDistance < i) {
+ // case 2
+ if (i - distanceToOrigin <= oOriginDistance) {
+ op.left = o.id
+ distanceToOrigin = i + 1
+ }
+ } else {
+ break
+ }
+ i++
+ o = o.right ? yield* this.getOperation(o.right) : null
+ } else {
+ break
+ }
+ }
+
+ // reconnect..
+ var left = null
+ var right = null
+ parent = parent || (yield* this.getOperation(op.parent))
+
+ // reconnect left and set right of op
+ if (op.left != null) {
+ left = yield* this.getOperation(op.left)
+ op.right = left.right
+ left.right = op.id
+
+ yield* this.setOperation(left)
+ } else {
+ op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
+ }
+ // reconnect right
+ if (op.right != null) {
+ right = yield* this.getOperation(op.right)
+ right.left = op.id
+
+ // if right exists, and it is supposed to be gc'd. Remove it from the gc
+ if (right.gc != null) {
+ this.store.removeFromGarbageCollector(right)
+ }
+ yield* this.setOperation(right)
+ }
+
+ // update parents .map/start/end properties
+ if (op.parentSub != null) {
+ if (left == null) {
+ parent.map[op.parentSub] = op.id
+ yield* this.setOperation(parent)
+ }
+ // is a child of a map struct.
+ // Then also make sure that only the most left element is not deleted
+ if (op.right != null) {
+ yield* this.deleteOperation(op.right, true)
+ }
+ if (op.left != null) {
+ yield* this.deleteOperation(op.id, true)
+ }
+ } else {
+ if (right == null || left == null) {
+ if (right == null) {
+ parent.end = op.id
+ }
+ if (left == null) {
+ parent.start = op.id
+ }
+ yield* this.setOperation(parent)
+ }
}
}
- return res
},
- map: function * (o, f) {
- o = o.start
- var res = []
- while (o != null) { // TODO: change to != (at least some convention)
- var operation = yield* this.getOperation(o)
- if (!operation.deleted) {
- res.push(f(operation))
- }
- o = operation.right
- }
- return res
- }
- },
- Map: {
- /*
+ List: {
+ /*
{
- map: {},
- struct: "Map",
+ start: null,
+ end: null,
+ struct: "List",
type: "",
id: this.os.getNextOpId()
}
- */
- encode: function (op) {
- return {
- struct: 'Map',
- type: op.type,
- id: op.id,
- map: {} // overwrite map!!
+ */
+ encode: function (op) {
+ return {
+ struct: 'List',
+ id: op.id,
+ type: op.type
+ }
+ },
+ requiredOps: function () {
+ /*
+ var ids = []
+ if (op.start != null) {
+ ids.push(op.start)
+ }
+ if (op.end != null){
+ ids.push(op.end)
+ }
+ return ids
+ */
+ return []
+ },
+ execute: function * (op) {
+ op.start = null
+ op.end = null
+ },
+ ref: function * (op, pos) {
+ if (op.start == null) {
+ return null
+ }
+ var res = null
+ var o = yield* this.getOperation(op.start)
+
+ while (true) {
+ if (!o.deleted) {
+ res = o
+ pos--
+ }
+ if (pos >= 0 && o.right != null) {
+ o = (yield* this.getOperation(o.right))
+ } else {
+ break
+ }
+ }
+ return res
+ },
+ map: function * (o, f) {
+ o = o.start
+ var res = []
+ while (o != null) { // TODO: change to != (at least some convention)
+ var operation = yield* this.getOperation(o)
+ if (!operation.deleted) {
+ res.push(f(operation))
+ }
+ o = operation.right
+ }
+ return res
}
},
- requiredOps: function () {
- return []
- },
- execute: function * () {},
- /*
- Get a property by name
- */
- get: function * (op, name) {
- var oid = op.map[name]
- if (oid != null) {
- var res = yield* this.getOperation(oid)
- return (res == null || res.deleted) ? void 0 : (res.opContent == null
- ? 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) {
- yield* Struct.Delete.create.call(this, {
- target: v
- })
+ Map: {
+ /*
+ {
+ map: {},
+ struct: "Map",
+ type: "",
+ id: this.os.getNextOpId()
+ }
+ */
+ encode: function (op) {
+ return {
+ struct: 'Map',
+ type: op.type,
+ id: op.id,
+ map: {} // overwrite map!!
+ }
+ },
+ requiredOps: function () {
+ return []
+ },
+ execute: function * () {},
+ /*
+ Get a property by name
+ */
+ get: function * (op, name) {
+ var oid = op.map[name]
+ if (oid != null) {
+ var res = yield* this.getOperation(oid)
+ return (res == null || res.deleted) ? void 0 : (res.opContent == null
+ ? 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) {
+ yield* Struct.Delete.create.call(this, {
+ target: v
+ })
+ }
}
}
}
+ Y.Struct = Struct
}
-Y.Struct = Struct
diff --git a/src/Transaction.js b/src/Transaction.js
index 8084777c..c9d3d82e 100644
--- a/src/Transaction.js
+++ b/src/Transaction.js
@@ -1,4 +1,3 @@
-/* global Y */
'use strict'
/*
@@ -74,580 +73,582 @@
- this is called only by `getOperations(startSS)`. It makes an operation
applyable on a given SS.
*/
-class Transaction {
- /*
- Get a type based on the id of its model.
- If it does not exist yes, create it.
- TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
- */
- * getType (id) {
- var sid = JSON.stringify(id)
- var t = this.store.initializedTypes[sid]
- if (t == null) {
- var op = yield* this.getOperation(id)
- if (op != null) {
- t = yield* Y[op.type].initType.call(this, this.store, op)
- this.store.initializedTypes[sid] = t
+module.exports = function (Y) {
+ class Transaction {
+ /*
+ Get a type based on the id of its model.
+ If it does not exist yes, create it.
+ TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
+ */
+ * getType (id) {
+ var sid = JSON.stringify(id)
+ var t = this.store.initializedTypes[sid]
+ if (t == null) {
+ var op = yield* this.getOperation(id)
+ if (op != null) {
+ t = yield* Y[op.type].initType.call(this, this.store, op)
+ this.store.initializedTypes[sid] = t
+ }
+ }
+ return t
+ }
+ /*
+ Apply operations that this user created (no remote ones!)
+ * does not check for Struct.*.requiredOps()
+ * also broadcasts it through the connector
+ */
+ * applyCreatedOperations (ops) {
+ var send = []
+ for (var i = 0; i < ops.length; i++) {
+ var op = ops[i]
+ yield* this.store.tryExecute.call(this, op)
+ send.push(Y.Struct[op.struct].encode(op))
+ }
+ if (!this.store.y.connector.isDisconnected()) {
+ this.store.y.connector.broadcast({
+ type: 'update',
+ ops: send
+ })
}
}
- return t
- }
- /*
- Apply operations that this user created (no remote ones!)
- * does not check for Struct.*.requiredOps()
- * also broadcasts it through the connector
- */
- * applyCreatedOperations (ops) {
- var send = []
- for (var i = 0; i < ops.length; i++) {
- var op = ops[i]
- yield* this.store.tryExecute.call(this, op)
- send.push(Y.Struct[op.struct].encode(op))
- }
- if (!this.store.y.connector.isDisconnected()) {
- this.store.y.connector.broadcast({
- type: 'update',
- ops: send
- })
- }
- }
- * deleteList (start) {
- if (this.store.y.connector.isSynced) {
- while (start != null && this.store.y.connector.isSynced) {
- start = (yield* this.getOperation(start))
- start.gc = true
- yield* this.setOperation(start)
- // TODO: will always reset the parent..
- this.store.gc1.push(start.id)
- start = start.right
+ * deleteList (start) {
+ if (this.store.y.connector.isSynced) {
+ while (start != null && this.store.y.connector.isSynced) {
+ start = (yield* this.getOperation(start))
+ start.gc = true
+ yield* this.setOperation(start)
+ // TODO: will always reset the parent..
+ this.store.gc1.push(start.id)
+ start = start.right
+ }
+ } else {
+ // TODO: when not possible??? do later in (gcWhenSynced)
}
- } else {
- // TODO: when not possible??? do later in (gcWhenSynced)
- }
- }
-
- /*
- Mark an operation as deleted, and add it to the GC, if possible.
- */
- * deleteOperation (targetId, preventCallType) {
- var target = yield* this.getOperation(targetId)
- var callType = false
-
- if (target == null || !target.deleted) {
- yield* this.markDeleted(targetId)
}
- if (target != null && target.gc == null) {
- if (!target.deleted) {
- callType = true
- // set deleted & notify type
- target.deleted = true
+ /*
+ Mark an operation as deleted, and add it to the GC, if possible.
+ */
+ * deleteOperation (targetId, preventCallType) {
+ var target = yield* this.getOperation(targetId)
+ var callType = false
+
+ if (target == null || !target.deleted) {
+ yield* this.markDeleted(targetId)
+ }
+
+ if (target != null && target.gc == null) {
+ if (!target.deleted) {
+ callType = true
+ // set deleted & notify type
+ target.deleted = true
+ /*
+ if (!preventCallType) {
+ var type = this.store.initializedTypes[JSON.stringify(target.parent)]
+ if (type != null) {
+ yield* type._changed(this, {
+ struct: 'Delete',
+ target: targetId
+ })
+ }
+ }
+ */
+ // delete containing lists
+ if (target.start != null) {
+ // TODO: don't do it like this .. -.-
+ yield* this.deleteList(target.start)
+ yield* this.deleteList(target.id)
+ }
+ if (target.map != null) {
+ for (var name in target.map) {
+ yield* this.deleteList(target.map[name])
+ }
+ // TODO: here to.. (see above)
+ yield* this.deleteList(target.id)
+ }
+ if (target.opContent != null) {
+ yield* this.deleteOperation(target.opContent)
+ target.opContent = null
+ }
+ }
+ var left = target.left != null ? yield* this.getOperation(target.left) : null
+
+ this.store.addToGarbageCollector(target, left)
+
+ // set here because it was deleted and/or gc'd
+ yield* this.setOperation(target)
+
/*
- if (!preventCallType) {
- var type = this.store.initializedTypes[JSON.stringify(target.parent)]
- if (type != null) {
- yield* type._changed(this, {
- struct: 'Delete',
- target: targetId
- })
- }
- }
+ Check if it is possible to add right to the gc.
+ Because this delete can't be responsible for left being gc'd,
+ we don't have to add left to the gc..
*/
- // delete containing lists
- if (target.start != null) {
- // TODO: don't do it like this .. -.-
- yield* this.deleteList(target.start)
- yield* this.deleteList(target.id)
- }
- if (target.map != null) {
- for (var name in target.map) {
- yield* this.deleteList(target.map[name])
- }
- // TODO: here to.. (see above)
- yield* this.deleteList(target.id)
- }
- if (target.opContent != null) {
- yield* this.deleteOperation(target.opContent)
- target.opContent = null
+ var right = target.right != null ? yield* this.getOperation(target.right) : null
+ if (
+ right != null &&
+ this.store.addToGarbageCollector(right, target)
+ ) {
+ yield* this.setOperation(right)
}
+ return callType
}
- var left = target.left != null ? yield* this.getOperation(target.left) : null
-
- this.store.addToGarbageCollector(target, left)
-
- // set here because it was deleted and/or gc'd
- yield* this.setOperation(target)
-
- /*
- Check if it is possible to add right to the gc.
- Because this delete can't be responsible for left being gc'd,
- we don't have to add left to the gc..
- */
- var right = target.right != null ? yield* this.getOperation(target.right) : null
- if (
- right != null &&
- this.store.addToGarbageCollector(right, target)
- ) {
- yield* this.setOperation(right)
- }
- return callType
}
- }
- /*
- Mark an operation as deleted&gc'd
- */
- * markGarbageCollected (id) {
- // this.mem.push(["gc", id]);
- var n = yield* this.markDeleted(id)
- if (!n.gc) {
- if (n.id[1] < id[1]) {
- // un-extend left
- var newlen = n.len - (id[1] - n.id[1])
- n.len -= newlen
- yield* this.ds.put(n)
- n = {id: id, len: newlen, gc: false}
+ /*
+ Mark an operation as deleted&gc'd
+ */
+ * markGarbageCollected (id) {
+ // this.mem.push(["gc", id]);
+ var n = yield* this.markDeleted(id)
+ if (!n.gc) {
+ if (n.id[1] < id[1]) {
+ // un-extend left
+ var newlen = n.len - (id[1] - n.id[1])
+ n.len -= newlen
+ yield* this.ds.put(n)
+ n = {id: id, len: newlen, gc: false}
+ yield* this.ds.put(n)
+ }
+ // get prev&next before adding a new operation
+ var prev = yield* this.ds.findPrev(id)
+ var next = yield* this.ds.findNext(id)
+
+ if (id[1] < n.id[1] + n.len - 1) {
+ // un-extend right
+ yield* this.ds.put({id: [id[0], id[1] + 1], len: n.len - 1, gc: false})
+ n.len = 1
+ }
+ // set gc'd
+ n.gc = true
+ // can extend left?
+ if (
+ prev != null &&
+ prev.gc &&
+ Y.utils.compareIds([prev.id[0], prev.id[1] + prev.len], n.id)
+ ) {
+ prev.len += n.len
+ yield* this.ds.delete(n.id)
+ n = prev
+ // ds.put n here?
+ }
+ // can extend right?
+ if (
+ next != null &&
+ next.gc &&
+ Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id)
+ ) {
+ n.len += next.len
+ yield* this.ds.delete(next.id)
+ }
yield* this.ds.put(n)
}
- // get prev&next before adding a new operation
- var prev = yield* this.ds.findPrev(id)
- var next = yield* this.ds.findNext(id)
-
- if (id[1] < n.id[1] + n.len - 1) {
- // un-extend right
- yield* this.ds.put({id: [id[0], id[1] + 1], len: n.len - 1, gc: false})
- n.len = 1
- }
- // set gc'd
- n.gc = true
- // can extend left?
- if (
- prev != null &&
- prev.gc &&
- Y.utils.compareIds([prev.id[0], prev.id[1] + prev.len], n.id)
- ) {
- prev.len += n.len
- yield* this.ds.delete(n.id)
- n = prev
- // ds.put n here?
- }
- // can extend right?
- if (
- next != null &&
- next.gc &&
- Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id)
- ) {
- n.len += next.len
- yield* this.ds.delete(next.id)
- }
- yield* this.ds.put(n)
}
- }
- /*
- Mark an operation as deleted.
+ /*
+ Mark an operation as deleted.
- returns the delete node
- */
- * markDeleted (id) {
- // this.mem.push(["del", id]);
- var n = yield* this.ds.findWithUpperBound(id)
- if (n != null && n.id[0] === id[0]) {
- if (n.id[1] <= id[1] && id[1] < n.id[1] + n.len) {
- // already deleted
- return n
- } else if (n.id[1] + n.len === id[1] && !n.gc) {
- // can extend existing deletion
- n.len++
+ returns the delete node
+ */
+ * markDeleted (id) {
+ // this.mem.push(["del", id]);
+ var n = yield* this.ds.findWithUpperBound(id)
+ if (n != null && n.id[0] === id[0]) {
+ if (n.id[1] <= id[1] && id[1] < n.id[1] + n.len) {
+ // already deleted
+ return n
+ } else if (n.id[1] + n.len === id[1] && !n.gc) {
+ // can extend existing deletion
+ n.len++
+ } else {
+ // cannot extend left
+ n = {id: id, len: 1, gc: false}
+ yield* this.ds.put(n)
+ }
} else {
// cannot extend left
n = {id: id, len: 1, gc: false}
yield* this.ds.put(n)
}
- } else {
- // cannot extend left
- n = {id: id, len: 1, gc: false}
+ // can extend right?
+ var next = yield* this.ds.findNext(n.id)
+ if (
+ next != null &&
+ Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id) &&
+ !next.gc
+ ) {
+ n.len = n.len + next.len
+ yield* this.ds.delete(next.id)
+ }
yield* this.ds.put(n)
+ return n
}
- // can extend right?
- var next = yield* this.ds.findNext(n.id)
- if (
- next != null &&
- Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id) &&
- !next.gc
- ) {
- n.len = n.len + next.len
- yield* this.ds.delete(next.id)
+ /*
+ Call this method when the client is connected&synced with the
+ other clients (e.g. master). This will query the database for
+ operations that can be gc'd and add them to the garbage collector.
+ */
+ * garbageCollectAfterSync () {
+ yield* this.os.iterate(this, null, null, function * (op) {
+ if (op.deleted && op.left != null) {
+ var left = yield* this.getOperation(op.left)
+ this.store.addToGarbageCollector(op, left)
+ }
+ })
}
- yield* this.ds.put(n)
- return n
- }
- /*
- Call this method when the client is connected&synced with the
- other clients (e.g. master). This will query the database for
- operations that can be gc'd and add them to the garbage collector.
- */
- * garbageCollectAfterSync () {
- yield* this.os.iterate(this, null, null, function * (op) {
- if (op.deleted && op.left != null) {
- var left = yield* this.getOperation(op.left)
- this.store.addToGarbageCollector(op, left)
+ /*
+ Really remove an op and all its effects.
+ The complicated case here is the Insert operation:
+ * reset left
+ * reset right
+ * reset parent.start
+ * reset parent.end
+ * reset origins of all right ops
+ */
+ * garbageCollectOperation (id) {
+ this.store.addToDebug('yield* this.garbageCollectOperation(', id, ')')
+ // check to increase the state of the respective user
+ var state = yield* this.getState(id[0])
+ if (state.clock === id[1]) {
+ state.clock++
+ // also check if more expected operations were gc'd
+ yield* this.checkDeleteStoreForState(state)
+ // then set the state
+ yield* this.setState(state)
}
- })
- }
- /*
- Really remove an op and all its effects.
- The complicated case here is the Insert operation:
- * reset left
- * reset right
- * reset parent.start
- * reset parent.end
- * reset origins of all right ops
- */
- * garbageCollectOperation (id) {
- this.store.addToDebug('yield* this.garbageCollectOperation(', id, ')')
- // check to increase the state of the respective user
- var state = yield* this.getState(id[0])
- if (state.clock === id[1]) {
- state.clock++
- // also check if more expected operations were gc'd
- yield* this.checkDeleteStoreForState(state)
- // then set the state
- yield* this.setState(state)
- }
- yield* this.markGarbageCollected(id)
+ yield* this.markGarbageCollected(id)
- // if op exists, then clean that mess up..
- var o = yield* this.getOperation(id)
- if (o != null) {
- /*
- if (!o.deleted) {
- yield* this.deleteOperation(id)
- o = yield* this.getOperation(id)
- }
- */
+ // if op exists, then clean that mess up..
+ var o = yield* this.getOperation(id)
+ if (o != null) {
+ /*
+ if (!o.deleted) {
+ yield* this.deleteOperation(id)
+ o = yield* this.getOperation(id)
+ }
+ */
- // remove gc'd op from the left op, if it exists
- if (o.left != null) {
- var left = yield* this.getOperation(o.left)
- left.right = o.right
- yield* this.setOperation(left)
+ // remove gc'd op from the left op, if it exists
+ if (o.left != null) {
+ var left = yield* this.getOperation(o.left)
+ left.right = o.right
+ yield* this.setOperation(left)
+ }
+ // remove gc'd op from the right op, if it exists
+ // also reset origins of right ops
+ if (o.right != null) {
+ var right = yield* this.getOperation(o.right)
+ right.left = o.left
+ if (Y.utils.compareIds(right.origin, o.id)) { // rights origin is o
+ // find new origin of right ops
+ // origin is the first left deleted operation
+ var neworigin = o.left
+ while (neworigin != null) {
+ var neworigin_ = yield* this.getOperation(neworigin)
+ if (neworigin_.deleted) {
+ break
+ }
+ neworigin = neworigin_.left
+ }
+
+ // reset origin of right
+ right.origin = neworigin
+
+ // reset origin of all right ops (except first right - duh!),
+ // until you find origin pointer to the left of o
+ var i = right.right == null ? null : yield* this.getOperation(right.right)
+ var ids = [o.id, o.right]
+ while (i != null && ids.some(function (id) {
+ return Y.utils.compareIds(id, i.origin)
+ })) {
+ if (Y.utils.compareIds(i.origin, o.id)) {
+ // reset origin of i
+ i.origin = neworigin
+ yield* this.setOperation(i)
+ }
+ // get next i
+ i = i.right == null ? null : yield* this.getOperation(i.right)
+ }
+ } /* otherwise, rights origin is to the left of o,
+ then there is no right op (from o), that origins in o */
+ yield* this.setOperation(right)
+ }
+
+ if (o.parent != null) {
+ // remove gc'd op from parent, if it exists
+ var parent = yield* this.getOperation(o.parent)
+ var setParent = false // whether to save parent to the os
+ if (o.parentSub != null) {
+ if (Y.utils.compareIds(parent.map[o.parentSub], o.id)) {
+ setParent = true
+ parent.map[o.parentSub] = o.right
+ }
+ } else {
+ if (Y.utils.compareIds(parent.start, o.id)) {
+ // gc'd op is the start
+ setParent = true
+ parent.start = o.right
+ }
+ if (Y.utils.compareIds(parent.end, o.id)) {
+ // gc'd op is the end
+ setParent = true
+ parent.end = o.left
+ }
+ }
+ if (setParent) {
+ yield* this.setOperation(parent)
+ }
+ }
+ // finally remove it from the os
+ yield* this.removeOperation(o.id)
}
- // remove gc'd op from the right op, if it exists
- // also reset origins of right ops
- if (o.right != null) {
- var right = yield* this.getOperation(o.right)
- right.left = o.left
- if (Y.utils.compareIds(right.origin, o.id)) { // rights origin is o
- // find new origin of right ops
- // origin is the first left deleted operation
- var neworigin = o.left
- while (neworigin != null) {
- var neworigin_ = yield* this.getOperation(neworigin)
- if (neworigin_.deleted) {
+ }
+ * checkDeleteStoreForState (state) {
+ var n = yield* this.ds.findWithUpperBound([state.user, state.clock])
+ if (n != null && n.id[0] === state.user && n.gc) {
+ state.clock = Math.max(state.clock, n.id[1] + n.len)
+ }
+ }
+ /*
+ apply a delete set in order to get
+ the state of the supplied ds
+ */
+ * applyDeleteSet (ds) {
+ var deletions = []
+ function createDeletions (user, start, len, gc) {
+ for (var c = start; c < start + len; c++) {
+ deletions.push([user, c, gc])
+ }
+ }
+
+ for (var user in ds) {
+ var dv = ds[user]
+ var pos = 0
+ var d = dv[pos]
+ yield* this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
+ // cases:
+ // 1. d deletes something to the right of n
+ // => go to next n (break)
+ // 2. d deletes something to the left of n
+ // => create deletions
+ // => reset d accordingly
+ // *)=> if d doesn't delete anything anymore, go to next d (continue)
+ // 3. not 2) and d deletes something that also n deletes
+ // => reset d so that it doesn't contain n's deletion
+ // *)=> if d does not delete anything anymore, go to next d (continue)
+ while (d != null) {
+ var diff = 0 // describe the diff of length in 1) and 2)
+ if (n.id[1] + n.len <= d[0]) {
+ // 1)
break
+ } else if (d[0] < n.id[1]) {
+ // 2)
+ // delete maximum the len of d
+ // else delete as much as possible
+ diff = Math.min(n.id[1] - d[0], d[1])
+ createDeletions(user, d[0], diff, d[2])
+ } else {
+ // 3)
+ diff = n.id[1] + n.len - d[0] // never null (see 1)
+ if (d[2] && !n.gc) {
+ // d marks as gc'd but n does not
+ // then delete either way
+ createDeletions(user, d[0], Math.min(diff, d[1]), d[2])
+ }
}
- neworigin = neworigin_.left
- }
-
- // reset origin of right
- right.origin = neworigin
-
- // reset origin of all right ops (except first right - duh!),
- // until you find origin pointer to the left of o
- var i = right.right == null ? null : yield* this.getOperation(right.right)
- var ids = [o.id, o.right]
- while (i != null && ids.some(function (id) {
- return Y.utils.compareIds(id, i.origin)
- })) {
- if (Y.utils.compareIds(i.origin, o.id)) {
- // reset origin of i
- i.origin = neworigin
- yield* this.setOperation(i)
- }
- // get next i
- i = i.right == null ? null : yield* this.getOperation(i.right)
- }
- } /* otherwise, rights origin is to the left of o,
- then there is no right op (from o), that origins in o */
- yield* this.setOperation(right)
- }
-
- if (o.parent != null) {
- // remove gc'd op from parent, if it exists
- var parent = yield* this.getOperation(o.parent)
- var setParent = false // whether to save parent to the os
- if (o.parentSub != null) {
- if (Y.utils.compareIds(parent.map[o.parentSub], o.id)) {
- setParent = true
- parent.map[o.parentSub] = o.right
- }
- } else {
- if (Y.utils.compareIds(parent.start, o.id)) {
- // gc'd op is the start
- setParent = true
- parent.start = o.right
- }
- if (Y.utils.compareIds(parent.end, o.id)) {
- // gc'd op is the end
- setParent = true
- parent.end = o.left
- }
- }
- if (setParent) {
- yield* this.setOperation(parent)
- }
- }
- // finally remove it from the os
- yield* this.removeOperation(o.id)
- }
- }
- * checkDeleteStoreForState (state) {
- var n = yield* this.ds.findWithUpperBound([state.user, state.clock])
- if (n != null && n.id[0] === state.user && n.gc) {
- state.clock = Math.max(state.clock, n.id[1] + n.len)
- }
- }
- /*
- apply a delete set in order to get
- the state of the supplied ds
- */
- * applyDeleteSet (ds) {
- var deletions = []
- function createDeletions (user, start, len, gc) {
- for (var c = start; c < start + len; c++) {
- deletions.push([user, c, gc])
- }
- }
-
- for (var user in ds) {
- var dv = ds[user]
- var pos = 0
- var d = dv[pos]
- yield* this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
- // cases:
- // 1. d deletes something to the right of n
- // => go to next n (break)
- // 2. d deletes something to the left of n
- // => create deletions
- // => reset d accordingly
- // *)=> if d doesn't delete anything anymore, go to next d (continue)
- // 3. not 2) and d deletes something that also n deletes
- // => reset d so that it doesn't contain n's deletion
- // *)=> if d does not delete anything anymore, go to next d (continue)
- while (d != null) {
- var diff = 0 // describe the diff of length in 1) and 2)
- if (n.id[1] + n.len <= d[0]) {
- // 1)
- break
- } else if (d[0] < n.id[1]) {
- // 2)
- // delete maximum the len of d
- // else delete as much as possible
- diff = Math.min(n.id[1] - d[0], d[1])
- createDeletions(user, d[0], diff, d[2])
- } else {
- // 3)
- diff = n.id[1] + n.len - d[0] // never null (see 1)
- if (d[2] && !n.gc) {
- // d marks as gc'd but n does not
- // then delete either way
- createDeletions(user, d[0], Math.min(diff, d[1]), d[2])
+ if (d[1] <= diff) {
+ // d doesn't delete anything anymore
+ d = dv[++pos]
+ } else {
+ d[0] = d[0] + diff // reset pos
+ d[1] = d[1] - diff // reset length
}
}
- if (d[1] <= diff) {
- // d doesn't delete anything anymore
- d = dv[++pos]
- } else {
- d[0] = d[0] + diff // reset pos
- d[1] = d[1] - diff // reset length
- }
+ })
+ // for the rest.. just apply it
+ for (; pos < dv.length; pos++) {
+ d = dv[pos]
+ createDeletions(user, d[0], d[1], d[2])
}
+ }
+ for (var i in deletions) {
+ var del = deletions[i]
+ var id = [del[0], del[1]]
+ // always try to delete..
+ var addOperation = yield* this.deleteOperation(id)
+ if (addOperation) {
+ // TODO:.. really .. here? You could prevent calling all these functions in operationAdded
+ yield* this.store.operationAdded(this, {struct: 'Delete', target: id})
+ }
+ if (del[2]) {
+ // gc
+ yield* this.garbageCollectOperation(id)
+ }
+ }
+ }
+ * isGarbageCollected (id) {
+ var n = yield* this.ds.findWithUpperBound(id)
+ return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len && n.gc
+ }
+ /*
+ A DeleteSet (ds) describes all the deleted ops in the OS
+ */
+ * getDeleteSet () {
+ var ds = {}
+ yield* this.ds.iterate(this, null, null, function * (n) {
+ var user = n.id[0]
+ var counter = n.id[1]
+ var len = n.len
+ var gc = n.gc
+ var dv = ds[user]
+ if (dv === void 0) {
+ dv = []
+ ds[user] = dv
+ }
+ dv.push([counter, len, gc])
})
- // for the rest.. just apply it
- for (; pos < dv.length; pos++) {
- d = dv[pos]
- createDeletions(user, d[0], d[1], d[2])
+ return ds
+ }
+ * isDeleted (id) {
+ var n = yield* this.ds.findWithUpperBound(id)
+ return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len
+ }
+ * setOperation (op) {
+ yield* this.os.put(op)
+ return op
+ }
+ * addOperation (op) {
+ yield* this.os.put(op)
+ }
+ * getOperation (id) {
+ return yield* this.os.find(id)
+ }
+ * removeOperation (id) {
+ yield* this.os.delete(id)
+ }
+ * setState (state) {
+ var val = {
+ id: [state.user],
+ clock: state.clock
+ }
+ // TODO: find a way to skip this step.. (after implementing some dbs..)
+ if (yield* this.ss.find([state.user])) {
+ yield* this.ss.put(val)
+ } else {
+ yield* this.ss.put(val)
}
}
- for (var i in deletions) {
- var del = deletions[i]
- var id = [del[0], del[1]]
- // always try to delete..
- var addOperation = yield* this.deleteOperation(id)
- if (addOperation) {
- // TODO:.. really .. here? You could prevent calling all these functions in operationAdded
- yield* this.store.operationAdded(this, {struct: 'Delete', target: id})
+ * getState (user) {
+ var n
+ var clock = (n = yield* this.ss.find([user])) == null ? null : n.clock
+ if (clock == null) {
+ clock = 0
}
- if (del[2]) {
- // gc
- yield* this.garbageCollectOperation(id)
+ return {
+ user: user,
+ clock: clock
}
}
- }
- * isGarbageCollected (id) {
- var n = yield* this.ds.findWithUpperBound(id)
- return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len && n.gc
- }
- /*
- A DeleteSet (ds) describes all the deleted ops in the OS
- */
- * getDeleteSet () {
- var ds = {}
- yield* this.ds.iterate(this, null, null, function * (n) {
- var user = n.id[0]
- var counter = n.id[1]
- var len = n.len
- var gc = n.gc
- var dv = ds[user]
- if (dv === void 0) {
- dv = []
- ds[user] = dv
- }
- dv.push([counter, len, gc])
- })
- return ds
- }
- * isDeleted (id) {
- var n = yield* this.ds.findWithUpperBound(id)
- return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len
- }
- * setOperation (op) {
- yield* this.os.put(op)
- return op
- }
- * addOperation (op) {
- yield* this.os.put(op)
- }
- * getOperation (id) {
- return yield* this.os.find(id)
- }
- * removeOperation (id) {
- yield* this.os.delete(id)
- }
- * setState (state) {
- var val = {
- id: [state.user],
- clock: state.clock
- }
- // TODO: find a way to skip this step.. (after implementing some dbs..)
- if (yield* this.ss.find([state.user])) {
- yield* this.ss.put(val)
- } else {
- yield* this.ss.put(val)
- }
- }
- * getState (user) {
- var n
- var clock = (n = yield* this.ss.find([user])) == null ? null : n.clock
- if (clock == null) {
- clock = 0
- }
- return {
- user: user,
- clock: clock
- }
- }
- * getStateVector () {
- var stateVector = []
- yield* this.ss.iterate(this, null, null, function * (n) {
- stateVector.push({
- user: n.id[0],
- clock: n.clock
+ * getStateVector () {
+ var stateVector = []
+ yield* this.ss.iterate(this, null, null, function * (n) {
+ stateVector.push({
+ user: n.id[0],
+ clock: n.clock
+ })
})
- })
- return stateVector
- }
- * getStateSet () {
- var ss = {}
- yield* this.ss.iterate(this, null, null, function * (n) {
- ss[n.id[0]] = n.clock
- })
- return ss
- }
- * getOperations (startSS) {
- // TODO: use bounds here!
- if (startSS == null) {
- startSS = {}
+ return stateVector
}
- var ops = []
-
- var endSV = yield* this.getStateVector()
- for (var endState of endSV) {
- var user = endState.user
- if (user === '_') {
- continue
- }
- var startPos = startSS[user] || 0
-
- yield* this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
- ops.push(op)
+ * getStateSet () {
+ var ss = {}
+ yield* this.ss.iterate(this, null, null, function * (n) {
+ ss[n.id[0]] = n.clock
})
+ return ss
}
- var res = []
- for (var op of ops) {
- res.push(yield* this.makeOperationReady(startSS, op))
- }
- return res
- }
- /*
- Here, we make op executable for the receiving user.
-
- Notes:
- startSS: denotes to the SV that the remote user sent
- currSS: denotes to the state vector that the user should have if he
- applies all already sent operations (increases is each step)
-
- We face several problems:
- * Execute op as is won't work because ops depend on each other
- -> find a way so that they do not anymore
- * When changing left, must not go more to the left than the origin
- * When changing right, you have to consider that other ops may have op
- as their origin, this means that you must not set one of these ops
- as the new right (interdependencies of ops)
- * can't just go to the right until you find the first known operation,
- With currSS
- -> interdependency of ops is a problem
- With startSS
- -> leads to inconsistencies when two users join at the same time.
- Then the position depends on the order of execution -> error!
-
- Solution:
- -> re-create originial situation
- -> set op.left = op.origin (which never changes)
- -> set op.right
- to the first operation that is known (according to startSS)
- or to the first operation that has an origin that is not to the
- right of op.
- -> Enforces unique execution order -> happy user
-
- Improvements: TODO
- * Could set left to origin, or the first known operation
- (startSS or currSS.. ?)
- -> Could be necessary when I turn GC again.
- -> Is a bad(ish) idea because it requires more computation
- */
- * makeOperationReady (startSS, op) {
- op = Y.Struct[op.struct].encode(op)
- op = Y.utils.copyObject(op)
- var o = op
- var ids = [op.id]
- // search for the new op.right
- // it is either the first known op (according to startSS)
- // or the o that has no origin to the right of op
- // (this is why we use the ids array)
- while (o.right != null) {
- var right = yield* this.getOperation(o.right)
- if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) {
- return Y.utils.compareIds(id, right.origin)
- })) {
- break
+ * getOperations (startSS) {
+ // TODO: use bounds here!
+ if (startSS == null) {
+ startSS = {}
}
- ids.push(o.right)
- o = right
+ var ops = []
+
+ var endSV = yield* this.getStateVector()
+ for (var endState of endSV) {
+ var user = endState.user
+ if (user === '_') {
+ continue
+ }
+ var startPos = startSS[user] || 0
+
+ yield* this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
+ ops.push(op)
+ })
+ }
+ var res = []
+ for (var op of ops) {
+ res.push(yield* this.makeOperationReady(startSS, op))
+ }
+ return res
+ }
+ /*
+ Here, we make op executable for the receiving user.
+
+ Notes:
+ startSS: denotes to the SV that the remote user sent
+ currSS: denotes to the state vector that the user should have if he
+ applies all already sent operations (increases is each step)
+
+ We face several problems:
+ * Execute op as is won't work because ops depend on each other
+ -> find a way so that they do not anymore
+ * When changing left, must not go more to the left than the origin
+ * When changing right, you have to consider that other ops may have op
+ as their origin, this means that you must not set one of these ops
+ as the new right (interdependencies of ops)
+ * can't just go to the right until you find the first known operation,
+ With currSS
+ -> interdependency of ops is a problem
+ With startSS
+ -> leads to inconsistencies when two users join at the same time.
+ Then the position depends on the order of execution -> error!
+
+ Solution:
+ -> re-create originial situation
+ -> set op.left = op.origin (which never changes)
+ -> set op.right
+ to the first operation that is known (according to startSS)
+ or to the first operation that has an origin that is not to the
+ right of op.
+ -> Enforces unique execution order -> happy user
+
+ Improvements: TODO
+ * Could set left to origin, or the first known operation
+ (startSS or currSS.. ?)
+ -> Could be necessary when I turn GC again.
+ -> Is a bad(ish) idea because it requires more computation
+ */
+ * makeOperationReady (startSS, op) {
+ op = Y.Struct[op.struct].encode(op)
+ op = Y.utils.copyObject(op)
+ var o = op
+ var ids = [op.id]
+ // search for the new op.right
+ // it is either the first known op (according to startSS)
+ // or the o that has no origin to the right of op
+ // (this is why we use the ids array)
+ while (o.right != null) {
+ var right = yield* this.getOperation(o.right)
+ if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) {
+ return Y.utils.compareIds(id, right.origin)
+ })) {
+ break
+ }
+ ids.push(o.right)
+ o = right
+ }
+ op.right = o.right
+ op.left = op.origin
+ return op
}
- op.right = o.right
- op.left = op.origin
- return op
}
+ Y.Transaction = Transaction
}
-Y.Transaction = Transaction
diff --git a/src/Types/Array.js b/src/Types/Array.js
index 4445fcf1..7fe2030f 100644
--- a/src/Types/Array.js
+++ b/src/Types/Array.js
@@ -1,7 +1,6 @@
-/* global Y */
'use strict'
-;(function () {
+function extend (Y) {
class YArray {
constructor (os, _model, idArray, valArray) {
this.os = os
@@ -166,7 +165,7 @@
}
}
- Y.Array = new Y.utils.CustomType({
+ Y.extend('Array', new Y.utils.CustomType({
class: YArray,
createType: function * YArrayCreator () {
var modelid = this.store.getNextOpId()
@@ -188,5 +187,11 @@
})
return new YArray(os, model.id, idArray, valArray)
}
- })
-})()
+ }))
+}
+
+if (typeof Y !== 'undefined') {
+ extend(Y)
+} else {
+ module.exports = extend
+}
diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js
index e81952b3..f385e52c 100644
--- a/src/Types/Array.spec.js
+++ b/src/Types/Array.spec.js
@@ -1,6 +1,8 @@
-/* global createUsers, databases, wait, Y, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, garbageCollectAllUsers, describeManyTimes */
+/* global createUsers, databases, wait, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, garbageCollectAllUsers, describeManyTimes */
/* eslint-env browser,jasmine */
+'use strict'
+var Y = require('../SpecHelper.js')
var numberOfYArrayTests = 10
var repeatArrayTests = 2
diff --git a/src/Types/Map.js b/src/Types/Map.js
index fbafbc6f..efca38d9 100644
--- a/src/Types/Map.js
+++ b/src/Types/Map.js
@@ -1,7 +1,6 @@
-/* global Y */
'use strict'
-;(function () {
+module.exports = function (Y) {
class YMap {
constructor (os, model, contents, opContents) {
this._model = model.id
@@ -292,4 +291,4 @@
return new YMap(os, model, contents, opContents)
}
})
-})()
+}
diff --git a/src/Types/Map.spec.js b/src/Types/Map.spec.js
index 4f8e8d39..7cd44d04 100644
--- a/src/Types/Map.spec.js
+++ b/src/Types/Map.spec.js
@@ -1,6 +1,8 @@
-/* global createUsers, Y, databases, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, describeManyTimes */
+/* global createUsers, databases, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, describeManyTimes */
/* eslint-env browser,jasmine */
+'use strict'
+var Y = require('../SpecHelper.js')
var numberOfYMapTests = 10
var repeatMapTeasts = 1
diff --git a/src/Types/TextBind.js b/src/Types/TextBind.js
index 44a78529..86e7a587 100644
--- a/src/Types/TextBind.js
+++ b/src/Types/TextBind.js
@@ -1,7 +1,6 @@
-/* global Y */
'use strict'
-;(function () {
+module.exports = function (Y) {
class YTextBind extends Y.Array['class'] {
constructor (os, _model, idArray, valArray) {
super(os, _model, idArray, valArray)
@@ -287,4 +286,4 @@
return new YTextBind(os, model.id, idArray, valArray)
}
})
-})()
+}
diff --git a/src/Utils.js b/src/Utils.js
index 50325a29..513fafc3 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1,4 +1,3 @@
-/* global Y */
'use strict'
/*
@@ -21,178 +20,182 @@
database request to finish). EventHandler will help you to make your type
synchronously.
*/
-class EventHandler {
- /*
- onevent: is called when the structure changes.
+module.exports = function (Y) {
+ Y.utils = {}
- 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.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])
- } else {
- this.waiting.push(Y.utils.copyObject(op))
+ 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.eventListeners = []
}
- }
- /*
- 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)
- }
- /*
- Basic event listener boilerplate...
- TODO: maybe put this in a different type..
- */
- addEventListener (f) {
- this.eventListeners.push(f)
- }
- removeEventListener (f) {
- this.eventListeners = this.eventListeners.filter(function (g) {
- return f !== g
- })
- }
- removeAllEventListeners () {
- this.eventListeners = []
- }
- callEventListeners (event) {
- for (var i in this.eventListeners) {
- try {
- this.eventListeners[i](event)
- } catch (e) {
- console.log('User events must not throw Errors!') // eslint-disable-line
+ /*
+ 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])
+ } else {
+ this.waiting.push(Y.utils.copyObject(op))
}
}
- }
- /*
- 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]
- for (var i = this.waiting.length - 1; i >= 0; i--) {
- let w = this.waiting[i]
- if (Y.utils.compareIds(op.left, w.id)) {
- // include the effect of op in w
- w.right = op.id
- // exclude the effect of w in op
- op.left = w.left
- } else if (Y.utils.compareIds(op.right, w.id)) {
- // similar..
- w.left = op.id
- op.right = w.right
+ /*
+ 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)
+ }
+ /*
+ Basic event listener boilerplate...
+ TODO: maybe put this in a different type..
+ */
+ addEventListener (f) {
+ this.eventListeners.push(f)
+ }
+ removeEventListener (f) {
+ this.eventListeners = this.eventListeners.filter(function (g) {
+ return f !== g
+ })
+ }
+ removeAllEventListeners () {
+ this.eventListeners = []
+ }
+ callEventListeners (event) {
+ for (var i in this.eventListeners) {
+ try {
+ this.eventListeners[i](event)
+ } catch (e) {
+ console.log('User events must not throw Errors!') // eslint-disable-line
}
}
}
- this._tryCallEvents()
- }
- /*
- 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]
- if (newLeft != null) {
- for (var i in this.waiting) {
+ /*
+ 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]
+ for (var i = this.waiting.length - 1; i >= 0; i--) {
let w = this.waiting[i]
- // We will just care about w.left
- if (Y.utils.compareIds(del.target, w.left)) {
- del.left = newLeft
+ if (Y.utils.compareIds(op.left, w.id)) {
+ // include the effect of op in w
+ w.right = op.id
+ // exclude the effect of w in op
+ op.left = w.left
+ } else if (Y.utils.compareIds(op.right, w.id)) {
+ // similar..
+ w.left = op.id
+ op.right = w.right
}
}
}
+ this._tryCallEvents()
+ }
+ /*
+ 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]
+ if (newLeft != null) {
+ for (var i in this.waiting) {
+ let w = this.waiting[i]
+ // We will just care about w.left
+ if (Y.utils.compareIds(del.target, w.left)) {
+ del.left = newLeft
+ }
+ }
+ }
+ }
+ this._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
+ this.waiting = []
+ this.onevent(events)
+ }
}
- this._tryCallEvents()
}
- /* (private)
- Try to execute the events for the waiting operations
+ 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)
*/
- _tryCallEvents () {
- this.awaiting--
- if (this.awaiting <= 0 && this.waiting.length > 0) {
- var events = this.waiting
- this.waiting = []
- this.onevent(events)
+ class CustomType { // eslint-disable-line
+ constructor (def) {
+ if (def.createType == null ||
+ def.initType == null ||
+ def.class == null
+ ) {
+ throw new Error('Custom type was not initialized correctly!')
+ }
+ this.createType = def.createType
+ this.initType = def.initType
+ this.class = def.class
}
}
-}
-Y.utils.EventHandler = EventHandler
+ Y.utils.CustomType = CustomType
-/*
- 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 ||
- def.initType == null ||
- def.class == null
- ) {
- throw new Error('Custom type was not initialized correctly!')
+ /*
+ Make a flat copy of an object
+ (just copy properties)
+ */
+ function copyObject (o) {
+ var c = {}
+ for (var key in o) {
+ c[key] = o[key]
}
- this.createType = def.createType
- this.initType = def.initType
- this.class = def.class
+ return c
}
-}
-Y.utils.CustomType = CustomType
+ Y.utils.copyObject = copyObject
-/*
- Make a flat copy of an object
- (just copy properties)
-*/
-function copyObject (o) {
- var c = {}
- for (var key in o) {
- c[key] = o[key]
+ /*
+ Defines a smaller relation on Id's
+ */
+ function smaller (a, b) {
+ return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1])
}
- return c
-}
-Y.utils.copyObject = copyObject
+ Y.utils.smaller = smaller
-/*
- 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) {
+ 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
}
- return false
- }
- if (id1[0] === id2[0] && id1[1] === id2[1]) {
- return true
- } else {
- return false
}
+ Y.utils.compareIds = compareIds
}
-Y.utils.compareIds = compareIds
diff --git a/src/y.js b/src/y.js
index a599db18..421fbcfa 100644
--- a/src/y.js
+++ b/src/y.js
@@ -1,11 +1,68 @@
/* @flow */
'use strict'
+require('./Connector.js')(Y)
+require('./Database.js')(Y)
+require('./Transaction.js')(Y)
+require('./Struct.js')(Y)
+require('./Utils.js')(Y)
+require('./Databases/RedBlackTree.js')(Y)
+require('./Databases/Memory.js')(Y)
+require('./Databases/IndexedDB.js')(Y)
+require('./Connectors/Test.js')(Y)
+
+var requiringModules = {}
+
+module.exports = Y
+
+Y.extend = function (name, value) {
+ Y[name] = value
+ var resolves = requiringModules[name]
+ if (requiringModules[name] != null) {
+ for (var i = 0; i < resolves.length; i++) {
+ resolves[i]()
+ }
+ delete requiringModules[name]
+ }
+}
+
+require('./Types/Array.js')(Y)
+require('./Types/Map.js')(Y)
+require('./Types/TextBind.js')(Y)
+
function Y (opts) {
- return new Promise(function (resolve) {
- var yconfig = new YConfig(opts, function () {
- yconfig.db.whenUserIdSet(function () {
- resolve(yconfig)
+ opts.types = opts.types != null ? opts.types : []
+ var modules = [opts.db.name, opts.connector.name].concat(opts.types)
+ var promises = []
+ for (var i = 0; i < modules.length; i++) {
+ if (Y[modules[i]] == null) {
+ try {
+ require(modules[i])(Y)
+ } catch (e) {
+ // module does not exist
+ if (window != null) {
+ if (requiringModules[modules[i]] == null) {
+ var imported = document.createElement('script')
+ var name = modules[i].toLowerCase()
+ imported.src = opts.sourceDir + '/y-' + name + '/y-' + name + '.js'
+ document.head.appendChild(imported)
+ requiringModules[modules[i]] = []
+ }
+ promises.push(new Promise(function (resolve) {
+ requiringModules[modules[i]].push(resolve)
+ }))
+ } else {
+ throw e
+ }
+ }
+ }
+ }
+ return Promise.all(promises).then(function () {
+ return new Promise(function (resolve) {
+ var yconfig = new YConfig(opts, function () {
+ yconfig.db.whenUserIdSet(function () {
+ resolve(yconfig)
+ })
})
})
})
@@ -49,9 +106,3 @@ class YConfig {
if (typeof window !== 'undefined') {
window.Y = Y
}
-
-if (typeof YConcurrency_TestingMode !== 'undefined') {
- g.Y = Y //eslint-disable-line
- // debugger //eslint-disable-line
-}
-Y.utils = {}