implemented module loader for yjs
This commit is contained in:
		
							parent
							
								
									138afe39dc
								
							
						
					
					
						commit
						6dc347642b
					
				@ -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
 | 
			
		||||
      }))
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								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 () {
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										593
									
								
								src/Connector.js
									
									
									
									
									
								
							
							
						
						
									
										593
									
								
								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
 | 
			
		||||
      <y a="4">
 | 
			
		||||
        <b c="5"></b>
 | 
			
		||||
      </y>
 | 
			
		||||
      m - ltx element
 | 
			
		||||
      json - Object
 | 
			
		||||
    */
 | 
			
		||||
    encodeMessageToXml (msg, obj) {
 | 
			
		||||
      // attributes is optional
 | 
			
		||||
      function encodeObject (m, json) {
 | 
			
		||||
        for (var name in json) {
 | 
			
		||||
          var value = json[name]
 | 
			
		||||
          if (name == null) {
 | 
			
		||||
            // nop
 | 
			
		||||
          } else if (value.constructor === Object) {
 | 
			
		||||
            encodeObject(m.c(name), value)
 | 
			
		||||
          } else if (value.constructor === Array) {
 | 
			
		||||
            encodeArray(m.c(name), value)
 | 
			
		||||
          } else {
 | 
			
		||||
            m.setAttribute(name, value)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      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
 | 
			
		||||
    <y a="4">
 | 
			
		||||
      <b c="5"></b>
 | 
			
		||||
    </y>
 | 
			
		||||
    m - ltx element
 | 
			
		||||
    json - Object
 | 
			
		||||
  */
 | 
			
		||||
  encodeMessageToXml (msg, obj) {
 | 
			
		||||
    // attributes is optional
 | 
			
		||||
    function encodeObject (m, json) {
 | 
			
		||||
      for (var name in json) {
 | 
			
		||||
        var value = json[name]
 | 
			
		||||
        if (name == null) {
 | 
			
		||||
          // nop
 | 
			
		||||
        } else if (value.constructor === Object) {
 | 
			
		||||
          encodeObject(m.c(name), value)
 | 
			
		||||
        } else if (value.constructor === Array) {
 | 
			
		||||
          encodeArray(m.c(name), value)
 | 
			
		||||
        } else {
 | 
			
		||||
          m.setAttribute(name, value)
 | 
			
		||||
        }
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										607
									
								
								src/Database.js
									
									
									
									
									
								
							
							
						
						
									
										607
									
								
								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<Listener>
 | 
			
		||||
    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<Listener>
 | 
			
		||||
       * 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<Listener>
 | 
			
		||||
      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<Listener>
 | 
			
		||||
         * 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
 | 
			
		||||
 | 
			
		||||
@ -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 () {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
/* global Y */
 | 
			
		||||
/* eslint-env browser,jasmine,console */
 | 
			
		||||
'use strict'
 | 
			
		||||
 | 
			
		||||
var Y = require('../SpecHelper.js')
 | 
			
		||||
var numberOfRBTreeTests = 1000
 | 
			
		||||
 | 
			
		||||
function itRedNodesDoNotHaveBlackChildren () {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
							
								
								
									
										590
									
								
								src/Struct.js
									
									
									
									
									
								
							
							
						
						
									
										590
									
								
								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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1065
									
								
								src/Transaction.js
									
									
									
									
									
								
							
							
						
						
									
										1065
									
								
								src/Transaction.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
})()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
})()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										303
									
								
								src/Utils.js
									
									
									
									
									
								
							
							
						
						
									
										303
									
								
								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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										71
									
								
								src/y.js
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								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 = {}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user