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