implemented module loader for yjs
This commit is contained in:
parent
138afe39dc
commit
6dc347642b
@ -21,31 +21,10 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
options.regenerator = false
|
options.regenerator = false
|
||||||
// TODO: include './node_modules/gulp-babel/node_modules/babel-core/node_modules/regenerator/runtime.js'
|
// 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 = {
|
var files = {
|
||||||
dist: helperOptions.polyfills.concat(helperOptions.files.map(function (f) {
|
dist: helperOptions.entry,
|
||||||
return 'src/' + f
|
specs: helperOptions.specs,
|
||||||
})),
|
src: './src/**/*.js'
|
||||||
test: ['../yjs/src/Helper.spec.js'].concat(yjsfiles).concat(helperOptions.files.map(function (f) {
|
|
||||||
return 'src/' + f
|
|
||||||
}).concat(['src/' + options.testfiles]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var babelOptions = {
|
var babelOptions = {
|
||||||
@ -54,38 +33,10 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
experimental: true
|
experimental: true
|
||||||
}
|
}
|
||||||
if (options.regenerator) {
|
if (options.regenerator) {
|
||||||
files.test = helperOptions.polyfills.concat(files.test)
|
files.specs = helperOptions.polyfills.concat(files.specs)
|
||||||
} else {
|
} else {
|
||||||
babelOptions.blacklist = 'regenerator'
|
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 () {
|
gulp.task('dist', function () {
|
||||||
var browserify = require('browserify')
|
var browserify = require('browserify')
|
||||||
@ -99,7 +50,7 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
.pipe(source(options.targetName))
|
.pipe(source(options.targetName))
|
||||||
.pipe(buffer())
|
.pipe(buffer())
|
||||||
.pipe($.if(options.debug, $.sourcemaps.init({loadMaps: true})))
|
.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 && options.regenerator, $.uglify()))
|
||||||
.pipe($.if(options.debug, $.sourcemaps.write('.')))
|
.pipe($.if(options.debug, $.sourcemaps.write('.')))
|
||||||
.pipe(gulp.dest('./dist/'))
|
.pipe(gulp.dest('./dist/'))
|
||||||
@ -108,11 +59,46 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
gulp.task('watch:dist', function (cb) {
|
gulp.task('watch:dist', function (cb) {
|
||||||
options.debug = true
|
options.debug = true
|
||||||
runSequence('dist', function () {
|
runSequence('dist', function () {
|
||||||
gulp.watch(files.dist, ['dist'])
|
gulp.watch(files.src, ['dist'])
|
||||||
cb()
|
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 () {
|
gulp.task('updateSubmodule', function () {
|
||||||
return gulp.src('./package.json', {read: false})
|
return gulp.src('./package.json', {read: false})
|
||||||
.pipe($.shell([
|
.pipe($.shell([
|
||||||
@ -168,24 +154,4 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
gulp.task('dev:node', ['test'], function () {
|
|
||||||
gulp.watch(files.dist, ['test'])
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('dev:browser', ['watch:build'], function () {
|
|
||||||
return gulp.src(files.test)
|
|
||||||
.pipe($.watch(['build/**/*']))
|
|
||||||
.pipe($.jasmineBrowser.specRunner())
|
|
||||||
.pipe($.jasmineBrowser.server({port: options.testport}))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('test', function () {
|
|
||||||
console.log(files.test)
|
|
||||||
return gulp.src('./dist/y.js')
|
|
||||||
.pipe($.jasmine({
|
|
||||||
verbose: true,
|
|
||||||
includeStuckTrace: true
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
24
gulpfile.js
24
gulpfile.js
@ -49,23 +49,15 @@ var runSequence = require('run-sequence').use(gulp)
|
|||||||
|
|
||||||
require('./gulpfile.helper.js')(gulp, {
|
require('./gulpfile.helper.js')(gulp, {
|
||||||
polyfills: [],
|
polyfills: [],
|
||||||
files: [
|
entry: './src/y.js',
|
||||||
'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'
|
|
||||||
],
|
|
||||||
targetName: '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 () {
|
gulp.task('dev:examples', ['updateSubmodule', 'watch:dist'], function () {
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
"run-sequence": "^1.1.4",
|
"run-sequence": "^1.1.4",
|
||||||
"standard": "^5.2.2",
|
"standard": "^5.2.2",
|
||||||
"vinyl-buffer": "^1.0.0",
|
"vinyl-buffer": "^1.0.0",
|
||||||
"vinyl-source-stream": "^1.1.0"
|
"vinyl-source-stream": "^1.1.0",
|
||||||
|
"watchify": "^3.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
593
src/Connector.js
593
src/Connector.js
@ -1,328 +1,329 @@
|
|||||||
/* globals Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
class AbstractConnector {
|
module.exports = function (Y) {
|
||||||
/*
|
class AbstractConnector {
|
||||||
opts contains the following information:
|
/*
|
||||||
role : String Role of this client ("master" or "slave")
|
opts contains the following information:
|
||||||
userId : String Uniquely defines the user.
|
role : String Role of this client ("master" or "slave")
|
||||||
debug: Boolean Whether to print debug messages (optional)
|
userId : String Uniquely defines the user.
|
||||||
*/
|
debug: Boolean Whether to print debug messages (optional)
|
||||||
constructor (y, opts) {
|
*/
|
||||||
this.y = y
|
constructor (y, opts) {
|
||||||
if (opts == null) {
|
this.y = y
|
||||||
opts = {}
|
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
|
|
||||||
}
|
}
|
||||||
}
|
if (opts.role == null || opts.role === 'master') {
|
||||||
if (syncUser != null) {
|
this.role = 'master'
|
||||||
var conn = this
|
} else if (opts.role === 'slave') {
|
||||||
this.currentSyncTarget = syncUser
|
this.role = 'slave'
|
||||||
this.y.db.requestTransaction(function *() {
|
} else {
|
||||||
conn.send(syncUser, {
|
throw new Error("Role must be either 'master' or 'slave'!")
|
||||||
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.role = opts.role
|
||||||
|
this.connections = {}
|
||||||
|
this.isSynced = false
|
||||||
|
this.userEventListeners = []
|
||||||
this.whenSyncedListeners = []
|
this.whenSyncedListeners = []
|
||||||
this.y.db.requestTransaction(function *() {
|
this.currentSyncTarget = null
|
||||||
yield* this.garbageCollectAfterSync()
|
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({
|
||||||
send (uid, message) {
|
action: 'userLeft',
|
||||||
if (this.debug) {
|
user: user
|
||||||
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 () {
|
userJoined (user, role) {
|
||||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
if (role == null) {
|
||||||
return cli !== sender
|
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, {
|
conn.send(sender, {
|
||||||
type: 'sync done'
|
type: 'sync done'
|
||||||
})
|
})
|
||||||
}, conn.syncingClientDuration)
|
}
|
||||||
} else {
|
conn._setSyncedWith(sender)
|
||||||
conn.send(sender, {
|
})
|
||||||
type: 'sync done'
|
} else if (m.type === 'sync step 2') {
|
||||||
})
|
let conn = this
|
||||||
}
|
var broadcastHB = !this.broadcastedHB
|
||||||
conn._setSyncedWith(sender)
|
this.broadcastedHB = true
|
||||||
})
|
var db = this.y.db
|
||||||
} else if (m.type === 'sync step 2') {
|
this.syncStep2 = new Promise(function (resolve) {
|
||||||
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)
|
|
||||||
db.requestTransaction(function * () {
|
db.requestTransaction(function * () {
|
||||||
var ops = yield* this.getOperations(m.stateSet)
|
yield* this.applyDeleteSet(m.deleteSet)
|
||||||
if (ops.length > 0) {
|
this.store.apply(m.os)
|
||||||
m = {
|
db.requestTransaction(function * () {
|
||||||
type: 'update',
|
var ops = yield* this.getOperations(m.stateSet)
|
||||||
ops: ops
|
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..
|
resolve()
|
||||||
conn.send(sender, m)
|
})
|
||||||
} else {
|
|
||||||
// broadcast only once!
|
|
||||||
conn.broadcast(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
} else if (m.type === 'sync done') {
|
||||||
} else if (m.type === 'sync done') {
|
var self = this
|
||||||
var self = this
|
this.syncStep2.then(function () {
|
||||||
this.syncStep2.then(function () {
|
self._setSyncedWith(sender)
|
||||||
self._setSyncedWith(sender)
|
})
|
||||||
})
|
} else if (m.type === 'update') {
|
||||||
} else if (m.type === 'update') {
|
if (this.forwardToSyncingClients) {
|
||||||
if (this.forwardToSyncingClients) {
|
for (var client of this.syncingClients) {
|
||||||
for (var client of this.syncingClients) {
|
this.send(client, m)
|
||||||
this.send(client, m)
|
}
|
||||||
}
|
}
|
||||||
|
this.y.db.apply(m.ops)
|
||||||
}
|
}
|
||||||
this.y.db.apply(m.ops)
|
|
||||||
}
|
}
|
||||||
}
|
_setSyncedWith (user) {
|
||||||
_setSyncedWith (user) {
|
var conn = this.connections[user]
|
||||||
var conn = this.connections[user]
|
if (conn != null) {
|
||||||
if (conn != null) {
|
conn.isSynced = true
|
||||||
conn.isSynced = true
|
}
|
||||||
|
if (user === this.currentSyncTarget) {
|
||||||
|
this.currentSyncTarget = null
|
||||||
|
this.findNextSyncTarget()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (user === this.currentSyncTarget) {
|
/*
|
||||||
this.currentSyncTarget = null
|
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||||
this.findNextSyncTarget()
|
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)
|
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.
|
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
|
does not support primitive values as array elements
|
||||||
expects an ltx (less than xml) object
|
expects an ltx (less than xml) object
|
||||||
*/
|
*/
|
||||||
parseMessageFromXml (m) {
|
parseMessageFromXml (m) {
|
||||||
function parseArray (node) {
|
function parseArray (node) {
|
||||||
for (var n of node.children) {
|
for (var n of node.children) {
|
||||||
if (n.getAttribute('isArray') === 'true') {
|
if (n.getAttribute('isArray') === 'true') {
|
||||||
return parseArray(n)
|
return parseArray(n)
|
||||||
} else {
|
} else {
|
||||||
return parseObject(n)
|
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 = {}
|
encode message in xml
|
||||||
for (var attrName in node.attrs) {
|
we use string because Strophe only accepts an "xml-string"..
|
||||||
var value = node.attrs[attrName]
|
So {a:4,b:{c:5}} will look like
|
||||||
var int = parseInt(value, 10)
|
<y a="4">
|
||||||
if (isNaN(int) || ('' + int) !== value) {
|
<b c="5"></b>
|
||||||
json[attrName] = value
|
</y>
|
||||||
} else {
|
m - ltx element
|
||||||
json[attrName] = int
|
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) {
|
function encodeArray (m, array) {
|
||||||
var name = n.name
|
m.setAttribute('isArray', 'true')
|
||||||
if (n.getAttribute('isArray') === 'true') {
|
for (var e of array) {
|
||||||
json[name] = parseArray(n)
|
if (e.constructor === Object) {
|
||||||
} else {
|
encodeObject(m.c('array-element'), e)
|
||||||
json[name] = parseObject(n)
|
} else {
|
||||||
|
encodeArray(m.c('array-element'), e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json
|
if (obj.constructor === Object) {
|
||||||
}
|
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
||||||
parseObject(m)
|
} else if (obj.constructor === Array) {
|
||||||
}
|
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
||||||
/*
|
} else {
|
||||||
encode message in xml
|
throw new Error("I can't encode this json!")
|
||||||
we use string because Strophe only accepts an "xml-string"..
|
|
||||||
So {a:4,b:{c:5}} will look like
|
|
||||||
<y a="4">
|
|
||||||
<b c="5"></b>
|
|
||||||
</y>
|
|
||||||
m - ltx element
|
|
||||||
json - Object
|
|
||||||
*/
|
|
||||||
encodeMessageToXml (msg, obj) {
|
|
||||||
// attributes is optional
|
|
||||||
function encodeObject (m, json) {
|
|
||||||
for (var name in json) {
|
|
||||||
var value = json[name]
|
|
||||||
if (name == null) {
|
|
||||||
// nop
|
|
||||||
} else if (value.constructor === Object) {
|
|
||||||
encodeObject(m.c(name), value)
|
|
||||||
} else if (value.constructor === Array) {
|
|
||||||
encodeArray(m.c(name), value)
|
|
||||||
} else {
|
|
||||||
m.setAttribute(name, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function encodeArray (m, array) {
|
|
||||||
m.setAttribute('isArray', 'true')
|
|
||||||
for (var e of array) {
|
|
||||||
if (e.constructor === Object) {
|
|
||||||
encodeObject(m.c('array-element'), e)
|
|
||||||
} else {
|
|
||||||
encodeArray(m.c('array-element'), e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (obj.constructor === Object) {
|
|
||||||
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
|
||||||
} else if (obj.constructor === Array) {
|
|
||||||
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
|
||||||
} else {
|
|
||||||
throw new Error("I can't encode this json!")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Y.AbstractConnector = AbstractConnector
|
||||||
}
|
}
|
||||||
Y.AbstractConnector = AbstractConnector
|
|
||||||
|
@ -1,136 +1,138 @@
|
|||||||
/* global getRandom, Y, wait, async */
|
/* global getRandom, wait, async */
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
var globalRoom = {
|
module.exports = function (Y) {
|
||||||
users: {},
|
var globalRoom = {
|
||||||
buffers: {},
|
users: {},
|
||||||
removeUser: function (user) {
|
buffers: {},
|
||||||
for (var i in this.users) {
|
removeUser: function (user) {
|
||||||
this.users[i].userLeft(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')
|
|
||||||
}
|
}
|
||||||
}
|
delete this.users[user]
|
||||||
}
|
delete this.buffers[user]
|
||||||
}
|
},
|
||||||
Y.utils.globalRoom = globalRoom
|
addUser: function (connector) {
|
||||||
|
this.users[connector.userId] = connector
|
||||||
function flushOne () {
|
this.buffers[connector.userId] = []
|
||||||
var bufs = []
|
for (var uname in this.users) {
|
||||||
for (var i in globalRoom.buffers) {
|
if (uname !== connector.userId) {
|
||||||
if (globalRoom.buffers[i].length > 0) {
|
var u = this.users[uname]
|
||||||
bufs.push(i)
|
u.userJoined(connector.userId, 'master')
|
||||||
}
|
connector.userJoined(u.userId, 'master')
|
||||||
}
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/*
|
Y.utils.globalRoom = globalRoom
|
||||||
Flushes an operation for some user..
|
|
||||||
*/
|
|
||||||
flushOne () {
|
|
||||||
flushOne()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.Test = Test
|
function flushOne () {
|
||||||
|
var bufs = []
|
||||||
|
for (var i in globalRoom.buffers) {
|
||||||
|
if (globalRoom.buffers[i].length > 0) {
|
||||||
|
bufs.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bufs.length > 0) {
|
||||||
|
var userId = getRandom(bufs)
|
||||||
|
var m = globalRoom.buffers[userId].shift()
|
||||||
|
var user = globalRoom.users[userId]
|
||||||
|
user.receiveMessage(m[0], m[1])
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setInterval(flushOne, 10)
|
||||||
|
|
||||||
|
var userIdCounter = 0
|
||||||
|
|
||||||
|
class Test extends Y.AbstractConnector {
|
||||||
|
constructor (y, options) {
|
||||||
|
if (options === undefined) {
|
||||||
|
throw new Error('Options must not be undefined!')
|
||||||
|
}
|
||||||
|
options.role = 'master'
|
||||||
|
options.forwardToSyncingClients = false
|
||||||
|
super(y, options)
|
||||||
|
this.setUserId((userIdCounter++) + '').then(() => {
|
||||||
|
globalRoom.addUser(this)
|
||||||
|
})
|
||||||
|
this.globalRoom = globalRoom
|
||||||
|
this.syncingClientDuration = 0
|
||||||
|
}
|
||||||
|
receiveMessage (sender, m) {
|
||||||
|
super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
|
||||||
|
}
|
||||||
|
send (userId, message) {
|
||||||
|
var buffer = globalRoom.buffers[userId]
|
||||||
|
if (buffer != null) {
|
||||||
|
buffer.push(JSON.parse(JSON.stringify([this.userId, message])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
broadcast (message) {
|
||||||
|
for (var key in globalRoom.buffers) {
|
||||||
|
globalRoom.buffers[key].push(JSON.parse(JSON.stringify([this.userId, message])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDisconnected () {
|
||||||
|
return globalRoom.users[this.userId] == null
|
||||||
|
}
|
||||||
|
reconnect () {
|
||||||
|
if (this.isDisconnected()) {
|
||||||
|
globalRoom.addUser(this)
|
||||||
|
super.reconnect()
|
||||||
|
}
|
||||||
|
return this.flushAll()
|
||||||
|
}
|
||||||
|
disconnect () {
|
||||||
|
if (!this.isDisconnected()) {
|
||||||
|
globalRoom.removeUser(this.userId)
|
||||||
|
super.disconnect()
|
||||||
|
}
|
||||||
|
return wait()
|
||||||
|
}
|
||||||
|
flush () {
|
||||||
|
var self = this
|
||||||
|
return async(function * () {
|
||||||
|
yield wait()
|
||||||
|
while (globalRoom.buffers[self.userId].length > 0) {
|
||||||
|
var m = globalRoom.buffers[self.userId].shift()
|
||||||
|
this.receiveMessage(m[0], m[1])
|
||||||
|
yield wait()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
flushAll () {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
// flushes may result in more created operations,
|
||||||
|
// flush until there is nothing more to flush
|
||||||
|
function nextFlush () {
|
||||||
|
var c = flushOne()
|
||||||
|
if (c) {
|
||||||
|
while (flushOne()) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
wait().then(nextFlush)
|
||||||
|
} else {
|
||||||
|
wait().then(function () {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// in the case that there are
|
||||||
|
// still actions that want to be performed
|
||||||
|
wait().then(nextFlush)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Flushes an operation for some user..
|
||||||
|
*/
|
||||||
|
flushOne () {
|
||||||
|
flushOne()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Y.Test = Test
|
||||||
|
}
|
||||||
|
607
src/Database.js
607
src/Database.js
@ -1,341 +1,342 @@
|
|||||||
/* global Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/*
|
module.exports = function (Y) {
|
||||||
Partial definition of an OperationStore.
|
/*
|
||||||
TODO: name it Database, operation store only holds operations.
|
Partial definition of an OperationStore.
|
||||||
|
TODO: name it Database, operation store only holds operations.
|
||||||
|
|
||||||
A database definition must alse define the following methods:
|
A database definition must alse define the following methods:
|
||||||
* logTable() (optional)
|
* logTable() (optional)
|
||||||
- show relevant information information in a table
|
- show relevant information information in a table
|
||||||
* requestTransaction(makeGen)
|
* requestTransaction(makeGen)
|
||||||
- request a transaction
|
- request a transaction
|
||||||
* destroy()
|
* destroy()
|
||||||
- destroy the database
|
- destroy the database
|
||||||
*/
|
*/
|
||||||
class AbstractDatabase {
|
class AbstractDatabase {
|
||||||
constructor (y, opts) {
|
constructor (y, opts) {
|
||||||
this.y = y
|
this.y = y
|
||||||
// E.g. this.listenersById[id] : Array<Listener>
|
// E.g. this.listenersById[id] : Array<Listener>
|
||||||
this.listenersById = {}
|
this.listenersById = {}
|
||||||
// Execute the next time a transaction is requested
|
// Execute the next time a transaction is requested
|
||||||
this.listenersByIdExecuteNow = []
|
this.listenersByIdExecuteNow = []
|
||||||
// A transaction is requested
|
// A transaction is requested
|
||||||
this.listenersByIdRequestPending = false
|
this.listenersByIdRequestPending = false
|
||||||
/* To make things more clear, the following naming conventions:
|
/* To make things more clear, the following naming conventions:
|
||||||
* ls : we put this.listenersById on ls
|
* ls : we put this.listenersById on ls
|
||||||
* l : Array<Listener>
|
* l : Array<Listener>
|
||||||
* id : Id (can't use as property name)
|
* id : Id (can't use as property name)
|
||||||
* sid : String (converted from id via JSON.stringify
|
* sid : String (converted from id via JSON.stringify
|
||||||
so we can use it as a property name)
|
so we can use it as a property name)
|
||||||
|
|
||||||
Always remember to first overwrite
|
Always remember to first overwrite
|
||||||
a property before you iterate over it!
|
a property before you iterate over it!
|
||||||
*/
|
*/
|
||||||
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
|
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
|
||||||
// wont be kept in memory.
|
// wont be kept in memory.
|
||||||
this.initializedTypes = {}
|
this.initializedTypes = {}
|
||||||
this.whenUserIdSetListener = null
|
this.whenUserIdSetListener = null
|
||||||
this.waitingTransactions = []
|
this.waitingTransactions = []
|
||||||
this.transactionInProgress = false
|
this.transactionInProgress = false
|
||||||
if (typeof YConcurrency_TestingMode !== 'undefined') {
|
if (typeof YConcurrency_TestingMode !== 'undefined') {
|
||||||
this.executeOrder = []
|
this.executeOrder = []
|
||||||
}
|
}
|
||||||
this.gc1 = [] // first stage
|
this.gc1 = [] // first stage
|
||||||
this.gc2 = [] // second stage -> after that, remove the op
|
this.gc2 = [] // second stage -> after that, remove the op
|
||||||
this.gcTimeout = opts.gcTimeout || 5000
|
this.gcTimeout = opts.gcTimeout || 5000
|
||||||
var os = this
|
var os = this
|
||||||
function garbageCollect () {
|
function garbageCollect () {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
os.requestTransaction(function * () {
|
os.requestTransaction(function * () {
|
||||||
if (os.y.connector != null && os.y.connector.isSynced) {
|
if (os.y.connector != null && os.y.connector.isSynced) {
|
||||||
for (var i in os.gc2) {
|
for (var i in os.gc2) {
|
||||||
var oid = os.gc2[i]
|
var oid = os.gc2[i]
|
||||||
yield* this.garbageCollectOperation(oid)
|
yield* this.garbageCollectOperation(oid)
|
||||||
|
}
|
||||||
|
os.gc2 = os.gc1
|
||||||
|
os.gc1 = []
|
||||||
}
|
}
|
||||||
os.gc2 = os.gc1
|
if (os.gcTimeout > 0) {
|
||||||
os.gc1 = []
|
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) {
|
}).join('').replace(/"/g, "'").replace(/,/g, ', ').replace(/:/g, ': ')
|
||||||
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
|
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()
|
resolve()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.garbageCollect = garbageCollect
|
/*
|
||||||
if (this.gcTimeout > 0) {
|
Try to add to GC.
|
||||||
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.
|
|
||||||
|
|
||||||
TODO: rename this function
|
TODO: rename this function
|
||||||
|
|
||||||
Rulez:
|
Rulez:
|
||||||
* Only gc if this user is online
|
* Only gc if this user is online
|
||||||
* The most left element in a list must not be gc'd.
|
* The most left element in a list must not be gc'd.
|
||||||
=> There is at least one element in the list
|
=> There is at least one element in the list
|
||||||
|
|
||||||
returns true iff op was added to GC
|
returns true iff op was added to GC
|
||||||
*/
|
*/
|
||||||
addToGarbageCollector (op, left) {
|
addToGarbageCollector (op, left) {
|
||||||
if (
|
if (
|
||||||
op.gc == null &&
|
op.gc == null &&
|
||||||
op.deleted === true &&
|
op.deleted === true &&
|
||||||
this.y.connector.isSynced &&
|
this.y.connector.isSynced &&
|
||||||
left != null &&
|
left != null &&
|
||||||
left.deleted === true
|
left.deleted === true
|
||||||
) {
|
) {
|
||||||
op.gc = true
|
op.gc = true
|
||||||
this.gc1.push(op.id)
|
this.gc1.push(op.id)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (let key in ids) {
|
removeFromGarbageCollector (op) {
|
||||||
let id = ids[key]
|
function filter (o) {
|
||||||
let sid = JSON.stringify(id)
|
return !Y.utils.compareIds(o, op.id)
|
||||||
let l = this.listenersById[sid]
|
|
||||||
if (l == null) {
|
|
||||||
l = []
|
|
||||||
this.listenersById[sid] = l
|
|
||||||
}
|
|
||||||
l.push(listener)
|
|
||||||
}
|
}
|
||||||
} else {
|
this.gc1 = this.gc1.filter(filter)
|
||||||
this.listenersByIdExecuteNow.push({
|
this.gc2 = this.gc2.filter(filter)
|
||||||
op: op
|
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.listenersByIdRequestPending) {
|
if (this.userId != null) {
|
||||||
return
|
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
|
* get a transaction
|
||||||
var store = this
|
* 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 * () {
|
for (let key in ids) {
|
||||||
var exeNow = store.listenersByIdExecuteNow
|
let id = ids[key]
|
||||||
store.listenersByIdExecuteNow = []
|
let sid = JSON.stringify(id)
|
||||||
|
let l = this.listenersById[sid]
|
||||||
var ls = store.listenersById
|
if (l == null) {
|
||||||
store.listenersById = {}
|
l = []
|
||||||
|
this.listenersById[sid] = l
|
||||||
store.listenersByIdRequestPending = false
|
}
|
||||||
|
l.push(listener)
|
||||||
for (let key in exeNow) {
|
}
|
||||||
let o = exeNow[key].op
|
} else {
|
||||||
yield* store.tryExecute.call(this, o)
|
this.listenersByIdExecuteNow.push({
|
||||||
|
op: op
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var sid in ls) {
|
if (this.listenersByIdRequestPending) {
|
||||||
var l = ls[sid]
|
return
|
||||||
var id = JSON.parse(sid)
|
}
|
||||||
if ((yield* this.getOperation(id)) == null) {
|
|
||||||
store.listenersById[sid] = l
|
this.listenersByIdRequestPending = true
|
||||||
} else {
|
var store = this
|
||||||
for (let key in l) {
|
|
||||||
let listener = l[key]
|
this.requestTransaction(function * () {
|
||||||
let o = listener.op
|
var exeNow = store.listenersByIdExecuteNow
|
||||||
if (--listener.missing === 0) {
|
store.listenersByIdExecuteNow = []
|
||||||
yield* store.tryExecute.call(this, o)
|
|
||||||
|
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
|
Actually execute an operation, when all expected operations are available.
|
||||||
* operationAdded (transaction, op) {
|
*/
|
||||||
if (op.struct === 'Delete') {
|
* tryExecute (op) {
|
||||||
var target = yield* transaction.getOperation(op.target)
|
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
||||||
if (target != null) {
|
if (op.struct === 'Delete') {
|
||||||
var type = transaction.store.initializedTypes[JSON.stringify(target.parent)]
|
yield* Y.Struct.Delete.execute.call(this, op)
|
||||||
if (type != null) {
|
yield* this.store.operationAdded(this, op)
|
||||||
yield* type._changed(transaction, {
|
} else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) {
|
||||||
struct: 'Delete',
|
yield* Y.Struct[op.struct].execute.call(this, op)
|
||||||
target: op.target
|
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 {
|
||||||
} else {
|
// increase SS
|
||||||
// increase SS
|
var o = op
|
||||||
var o = op
|
var state = yield* transaction.getState(op.id[0])
|
||||||
var state = yield* transaction.getState(op.id[0])
|
while (o != null && o.id[1] === state.clock && op.id[0] === o.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
|
||||||
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
|
state.clock++
|
||||||
state.clock++
|
yield* transaction.checkDeleteStoreForState(state)
|
||||||
yield* transaction.checkDeleteStoreForState(state)
|
o = yield* transaction.os.findNext(o.id)
|
||||||
o = yield* transaction.os.findNext(o.id)
|
}
|
||||||
}
|
yield* transaction.setState(state)
|
||||||
yield* transaction.setState(state)
|
|
||||||
|
|
||||||
// notify whenOperation listeners (by id)
|
// notify whenOperation listeners (by id)
|
||||||
var sid = JSON.stringify(op.id)
|
var sid = JSON.stringify(op.id)
|
||||||
var l = this.listenersById[sid]
|
var l = this.listenersById[sid]
|
||||||
delete this.listenersById[sid]
|
delete this.listenersById[sid]
|
||||||
|
|
||||||
if (l != null) {
|
if (l != null) {
|
||||||
for (var key in l) {
|
for (var key in l) {
|
||||||
var listener = l[key]
|
var listener = l[key]
|
||||||
if (--listener.missing === 0) {
|
if (--listener.missing === 0) {
|
||||||
this.whenOperationsExist([], listener.op)
|
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
|
getNextRequest () {
|
||||||
if (t != null) {
|
if (this.waitingTransactions.length === 0) {
|
||||||
yield* t._changed(transaction, Y.utils.copyObject(op))
|
this.transactionInProgress = false
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return this.waitingTransactions.shift()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Delete if DS says this is actually deleted
|
requestTransaction (makeGen, callImmediately) {
|
||||||
if (!op.deleted && (yield* transaction.isDeleted(op.id))) {
|
if (callImmediately) {
|
||||||
var delop = {
|
this.transact(makeGen)
|
||||||
struct: 'Delete',
|
} else if (!this.transactionInProgress) {
|
||||||
target: op.id
|
this.transactionInProgress = true
|
||||||
}
|
var self = this
|
||||||
yield* Y.Struct['Delete'].execute.call(transaction, delop)
|
setTimeout(function () {
|
||||||
if (t != null) {
|
self.transact(makeGen)
|
||||||
yield* t._changed(transaction, delop)
|
}, 0)
|
||||||
}
|
} else {
|
||||||
|
this.waitingTransactions.push(makeGen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getNextRequest () {
|
Y.AbstractDatabase = AbstractDatabase
|
||||||
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
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
/* global Y, async, databases */
|
/* global async, databases */
|
||||||
/* eslint-env browser,jasmine,console */
|
/* eslint-env browser,jasmine,console */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var Y = require('./SpecHelper.js')
|
||||||
|
|
||||||
for (let database of databases) {
|
for (let database of databases) {
|
||||||
describe(`Database (${database})`, function () {
|
describe(`Database (${database})`, function () {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
/* global Y */
|
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
Y.IndexedDB = (function () {
|
module.exports = function (Y) {
|
||||||
class Store {
|
class Store {
|
||||||
constructor (transaction, name) {
|
constructor (transaction, name) {
|
||||||
this.store = transaction.objectStore(name)
|
this.store = transaction.objectStore(name)
|
||||||
@ -177,5 +175,5 @@ Y.IndexedDB = (function () {
|
|||||||
yield window.indexedDB.deleteDatabase(this.namespace)
|
yield window.indexedDB.deleteDatabase(this.namespace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return OperationStore
|
Y.IndexedDB = OperationStore
|
||||||
})()
|
}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
/* eslint-env browser,jasmine */
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && false) {
|
|
||||||
describe('IndexedDB', function () {
|
|
||||||
var ob
|
|
||||||
beforeAll(function () {
|
|
||||||
ob = new Y.IndexedDB(null, {namespace: 'Test', gcTimeout: -1})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(function (done) {
|
|
||||||
ob.requestTransaction(function *() {
|
|
||||||
yield* ob.removeDatabase()
|
|
||||||
ob = null
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
/* global Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
Y.Memory = (function () {
|
module.exports = function (Y) {
|
||||||
class Transaction extends Y.Transaction {
|
class Transaction extends Y.Transaction {
|
||||||
constructor (store) {
|
constructor (store) {
|
||||||
super(store)
|
super(store)
|
||||||
@ -59,5 +58,5 @@ Y.Memory = (function () {
|
|||||||
delete this.ds
|
delete this.ds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Database
|
Y.Memory = Database
|
||||||
})()
|
}
|
||||||
|
@ -1,489 +1,490 @@
|
|||||||
/* global Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This file contains a not so fancy implemantion of a Red Black Tree.
|
This file contains a not so fancy implemantion of a Red Black Tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class N {
|
module.exports = function (Y) {
|
||||||
// A created node is always red!
|
class N {
|
||||||
constructor (val) {
|
// A created node is always red!
|
||||||
this.val = val
|
constructor (val) {
|
||||||
this.color = true
|
this.val = val
|
||||||
this._left = null
|
this.color = true
|
||||||
this._right = null
|
this._left = null
|
||||||
this._parent = null
|
this._right = null
|
||||||
if (val.id === null) {
|
this._parent = null
|
||||||
throw new Error('You must define id!')
|
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
|
|
||||||
}
|
}
|
||||||
return o
|
}
|
||||||
} else {
|
isRed () { return this.color }
|
||||||
var p = this
|
isBlack () { return !this.color }
|
||||||
while (p.parent !== null && p !== p.parent.left) {
|
redden () { this.color = true; return this }
|
||||||
p = p.parent
|
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
|
||||||
}
|
}
|
||||||
}
|
set right (n) {
|
||||||
prev () {
|
if (n !== null) {
|
||||||
if (this.left !== null) {
|
n._parent = this
|
||||||
// search the most right node in the left tree
|
|
||||||
var o = this.left
|
|
||||||
while (o.right !== null) {
|
|
||||||
o = o.right
|
|
||||||
}
|
}
|
||||||
return o
|
this._right = n
|
||||||
} else {
|
}
|
||||||
var p = this
|
rotateLeft (tree) {
|
||||||
while (p.parent !== null && p !== p.parent.right) {
|
var parent = this.parent
|
||||||
p = p.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
|
|
||||||
}
|
}
|
||||||
}
|
next () {
|
||||||
rotateRight (tree) {
|
if (this.right !== null) {
|
||||||
var parent = this.parent
|
// search the most left node in the right tree
|
||||||
var newParent = this.left
|
var o = this.right
|
||||||
var newLeft = this.left.right
|
while (o.left !== null) {
|
||||||
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
|
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..
|
return o
|
||||||
if (o.right !== null) {
|
} 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
|
o = o.right
|
||||||
} else {
|
} else {
|
||||||
// there is no right element. Search for the next bigger element,
|
return o
|
||||||
// this should be within the bounds
|
|
||||||
return o.next()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return o
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
* delete (id) {
|
||||||
findNodeWithUpperBound (to) {
|
if (id == null || id.constructor !== Array) {
|
||||||
if (to === void 0) {
|
throw new Error('id is expected to be an Array!')
|
||||||
throw new Error('You must define from!')
|
}
|
||||||
}
|
var d = this.findNode(id)
|
||||||
var o = this.root
|
if (d == null) {
|
||||||
if (o === null) {
|
throw new Error('Element does not exist!')
|
||||||
return null
|
}
|
||||||
} else {
|
this.length--
|
||||||
while (true) {
|
if (d.left !== null && d.right !== null) {
|
||||||
if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) {
|
// switch d with the greates element in the left subtree.
|
||||||
// o is included in the bound
|
// o should have at most one child.
|
||||||
// try to find an element that is closer to the bound
|
var o = d.left
|
||||||
|
// find
|
||||||
|
while (o.right !== null) {
|
||||||
o = o.right
|
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
|
||||||
}
|
}
|
||||||
}
|
// d has at most one child
|
||||||
}
|
// let n be the node that replaces d
|
||||||
* findWithLowerBound (from) {
|
var isFakeChild
|
||||||
var n = this.findNodeWithLowerBound(from)
|
var child = d.left || d.right
|
||||||
return n == null ? null : n.val
|
if (child === null) {
|
||||||
}
|
isFakeChild = true
|
||||||
* findWithUpperBound (to) {
|
child = new N({id: 0})
|
||||||
var n = this.findNodeWithUpperBound(to)
|
child.blacken()
|
||||||
return n == null ? null : n.val
|
d.right = child
|
||||||
}
|
} else {
|
||||||
* iterate (t, from, to, f) {
|
isFakeChild = false
|
||||||
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 {
|
|
||||||
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 (d.parent === null) {
|
||||||
if (!isFakeChild) {
|
if (!isFakeChild) {
|
||||||
this.root = child
|
this.root = child
|
||||||
child.blacken()
|
child.blacken()
|
||||||
child._parent = null
|
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 {
|
} else {
|
||||||
this.root = null
|
throw new Error('Impossible!')
|
||||||
}
|
}
|
||||||
return
|
if (d.isBlack()) {
|
||||||
} else if (d.parent.left === d) {
|
if (child.isRed()) {
|
||||||
d.parent.left = child
|
child.blacken()
|
||||||
} else if (d.parent.right === d) {
|
} else {
|
||||||
d.parent.right = child
|
this._fixDelete(child)
|
||||||
} else {
|
}
|
||||||
throw new Error('Impossible!')
|
}
|
||||||
}
|
this.root.blacken()
|
||||||
if (d.isBlack()) {
|
if (isFakeChild) {
|
||||||
if (child.isRed()) {
|
if (child.parent.left === child) {
|
||||||
child.blacken()
|
child.parent.left = null
|
||||||
} else {
|
} else if (child.parent.right === child) {
|
||||||
this._fixDelete(child)
|
child.parent.right = null
|
||||||
|
} else {
|
||||||
|
throw new Error('Impossible #3')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.root.blacken()
|
_fixDelete (n) {
|
||||||
if (isFakeChild) {
|
function isBlack (node) {
|
||||||
if (child.parent.left === child) {
|
return node !== null ? node.isBlack() : true
|
||||||
child.parent.left = null
|
|
||||||
} else if (child.parent.right === child) {
|
|
||||||
child.parent.right = null
|
|
||||||
} else {
|
|
||||||
throw new Error('Impossible #3')
|
|
||||||
}
|
}
|
||||||
}
|
function isRed (node) {
|
||||||
}
|
return node !== null ? node.isRed() : false
|
||||||
_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')
|
|
||||||
}
|
}
|
||||||
sibling = n.sibling
|
if (n.parent === null) {
|
||||||
}
|
// this can only be called after the first iteration of fixDelete.
|
||||||
// parent, sibling, and children of n are black
|
return
|
||||||
if (n.parent.isBlack() &&
|
}
|
||||||
sibling.isBlack() &&
|
// d was already replaced by the child
|
||||||
isBlack(sibling.left) &&
|
// d is not the root
|
||||||
isBlack(sibling.right)
|
// d and child are black
|
||||||
) {
|
var sibling = n.sibling
|
||||||
sibling.redden()
|
if (isRed(sibling)) {
|
||||||
this._fixDelete(n.parent)
|
// make sibling the grandfather
|
||||||
} else if (n.parent.isRed() &&
|
n.parent.redden()
|
||||||
sibling.isBlack() &&
|
sibling.blacken()
|
||||||
isBlack(sibling.left) &&
|
if (n === n.parent.left) {
|
||||||
isBlack(sibling.right)
|
n.parent.rotateLeft(this)
|
||||||
) {
|
} else if (n === n.parent.right) {
|
||||||
sibling.redden()
|
n.parent.rotateRight(this)
|
||||||
n.parent.blacken()
|
} else {
|
||||||
} else {
|
throw new Error('Impossible #2')
|
||||||
if (n === n.parent.left &&
|
}
|
||||||
|
sibling = n.sibling
|
||||||
|
}
|
||||||
|
// parent, sibling, and children of n are black
|
||||||
|
if (n.parent.isBlack() &&
|
||||||
sibling.isBlack() &&
|
sibling.isBlack() &&
|
||||||
isRed(sibling.left) &&
|
isBlack(sibling.left) &&
|
||||||
isBlack(sibling.right)
|
isBlack(sibling.right)
|
||||||
) {
|
) {
|
||||||
sibling.redden()
|
sibling.redden()
|
||||||
sibling.left.blacken()
|
this._fixDelete(n.parent)
|
||||||
sibling.rotateRight(this)
|
} else if (n.parent.isRed() &&
|
||||||
sibling = n.sibling
|
|
||||||
} else if (n === n.parent.right &&
|
|
||||||
sibling.isBlack() &&
|
sibling.isBlack() &&
|
||||||
isRed(sibling.right) &&
|
isBlack(sibling.left) &&
|
||||||
isBlack(sibling.left)
|
isBlack(sibling.right)
|
||||||
) {
|
) {
|
||||||
sibling.redden()
|
sibling.redden()
|
||||||
sibling.right.blacken()
|
n.parent.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 {
|
} else {
|
||||||
sibling.left.blacken()
|
if (n === n.parent.left &&
|
||||||
n.parent.rotateRight(this)
|
sibling.isBlack() &&
|
||||||
}
|
isRed(sibling.left) &&
|
||||||
}
|
isBlack(sibling.right)
|
||||||
}
|
) {
|
||||||
* put (v) {
|
sibling.redden()
|
||||||
if (v == null || v.id == null || v.id.constructor !== Array) {
|
sibling.left.blacken()
|
||||||
throw new Error('v is expected to have an id property which is an Array!')
|
sibling.rotateRight(this)
|
||||||
}
|
sibling = n.sibling
|
||||||
var node = new N(v)
|
} else if (n === n.parent.right &&
|
||||||
if (this.root !== null) {
|
sibling.isBlack() &&
|
||||||
var p = this.root // p abbrev. parent
|
isRed(sibling.right) &&
|
||||||
while (true) {
|
isBlack(sibling.left)
|
||||||
if (Y.utils.smaller(node.val.id, p.val.id)) {
|
) {
|
||||||
if (p.left === null) {
|
sibling.redden()
|
||||||
p.left = node
|
sibling.right.blacken()
|
||||||
break
|
sibling.rotateLeft(this)
|
||||||
} else {
|
sibling = n.sibling
|
||||||
p = p.left
|
}
|
||||||
}
|
sibling.color = n.parent.color
|
||||||
} else if (Y.utils.smaller(p.val.id, node.val.id)) {
|
n.parent.blacken()
|
||||||
if (p.right === null) {
|
if (n === n.parent.left) {
|
||||||
p.right = node
|
sibling.right.blacken()
|
||||||
break
|
n.parent.rotateLeft(this)
|
||||||
} else {
|
|
||||||
p = p.right
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
p.val = node.val
|
sibling.left.blacken()
|
||||||
return p
|
n.parent.rotateRight(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._fixInsert(node)
|
|
||||||
} else {
|
|
||||||
this.root = node
|
|
||||||
}
|
}
|
||||||
this.length++
|
* put (v) {
|
||||||
this.root.blacken()
|
if (v == null || v.id == null || v.id.constructor !== Array) {
|
||||||
return node
|
throw new Error('v is expected to have an id property which is an Array!')
|
||||||
}
|
|
||||||
_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.
|
var node = new N(v)
|
||||||
// Now traverse grandparent, make parent a black node
|
if (this.root !== null) {
|
||||||
// on the highest level which holds two red nodes.
|
var p = this.root // p abbrev. parent
|
||||||
n.parent.blacken()
|
while (true) {
|
||||||
n.grandparent.redden()
|
if (Y.utils.smaller(node.val.id, p.val.id)) {
|
||||||
if (n === n.parent.left) {
|
if (p.left === null) {
|
||||||
// Case 1
|
p.left = node
|
||||||
n.grandparent.rotateRight(this)
|
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 {
|
} else {
|
||||||
// Case 2
|
this.root = node
|
||||||
n.grandparent.rotateLeft(this)
|
}
|
||||||
|
this.length++
|
||||||
|
this.root.blacken()
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
_fixInsert (n) {
|
||||||
|
if (n.parent === null) {
|
||||||
|
n.blacken()
|
||||||
|
return
|
||||||
|
} else if (n.parent.isBlack()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var uncle = n.getUncle()
|
||||||
|
if (uncle !== null && uncle.isRed()) {
|
||||||
|
// Note: parent: red, uncle: red
|
||||||
|
n.parent.blacken()
|
||||||
|
uncle.blacken()
|
||||||
|
n.grandparent.redden()
|
||||||
|
this._fixInsert(n.grandparent)
|
||||||
|
} else {
|
||||||
|
// Note: parent: red, uncle: black or null
|
||||||
|
// Now we transform the tree in such a way that
|
||||||
|
// either of these holds:
|
||||||
|
// 1) grandparent.left.isRed
|
||||||
|
// and grandparent.left.left.isRed
|
||||||
|
// 2) grandparent.right.isRed
|
||||||
|
// and grandparent.right.right.isRed
|
||||||
|
if (n === n.parent.right && n.parent === n.grandparent.left) {
|
||||||
|
n.parent.rotateLeft(this)
|
||||||
|
// Since we rotated and want to use the previous
|
||||||
|
// cases, we need to set n in such a way that
|
||||||
|
// n.parent.isRed again
|
||||||
|
n = n.left
|
||||||
|
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
|
||||||
|
n.parent.rotateRight(this)
|
||||||
|
// see above
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
// Case 1) or 2) hold from here on.
|
||||||
|
// Now traverse grandparent, make parent a black node
|
||||||
|
// on the highest level which holds two red nodes.
|
||||||
|
n.parent.blacken()
|
||||||
|
n.grandparent.redden()
|
||||||
|
if (n === n.parent.left) {
|
||||||
|
// Case 1
|
||||||
|
n.grandparent.rotateRight(this)
|
||||||
|
} else {
|
||||||
|
// Case 2
|
||||||
|
n.grandparent.rotateLeft(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Y.utils.RBTree = RBTree
|
Y.utils.RBTree = RBTree
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* global Y */
|
|
||||||
/* eslint-env browser,jasmine,console */
|
/* eslint-env browser,jasmine,console */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var Y = require('../SpecHelper.js')
|
||||||
var numberOfRBTreeTests = 1000
|
var numberOfRBTreeTests = 1000
|
||||||
|
|
||||||
function itRedNodesDoNotHaveBlackChildren () {
|
function itRedNodesDoNotHaveBlackChildren () {
|
||||||
|
@ -1,288 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
/* eslint-env browser, jasmine */
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is just a compilation of functions that help to test this library!
|
|
||||||
*/
|
|
||||||
|
|
||||||
// When testing, you store everything on the global object. We call it g
|
|
||||||
var g
|
|
||||||
if (typeof global !== 'undefined') {
|
|
||||||
g = global
|
|
||||||
} else if (typeof window !== 'undefined') {
|
|
||||||
g = window
|
|
||||||
} else {
|
|
||||||
throw new Error('No global object?')
|
|
||||||
}
|
|
||||||
g.g = g
|
|
||||||
|
|
||||||
g.YConcurrency_TestingMode = true
|
|
||||||
|
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000
|
|
||||||
|
|
||||||
g.describeManyTimes = function describeManyTimes (times, name, f) {
|
|
||||||
for (var i = 0; i < times; i++) {
|
|
||||||
describe(name, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Wait for a specified amount of time (in ms). defaults to 5ms
|
|
||||||
*/
|
|
||||||
function wait (t) {
|
|
||||||
if (t == null) {
|
|
||||||
t = 5
|
|
||||||
}
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
setTimeout(function () {
|
|
||||||
resolve()
|
|
||||||
}, t * 2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
g.wait = wait
|
|
||||||
|
|
||||||
g.databases = ['Memory']
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
g.databases.push('IndexedDB')
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
returns a random element of o.
|
|
||||||
works on Object, and Array
|
|
||||||
*/
|
|
||||||
function getRandom (o) {
|
|
||||||
if (o instanceof Array) {
|
|
||||||
return o[Math.floor(Math.random() * o.length)]
|
|
||||||
} else if (o.constructor === Object) {
|
|
||||||
var ks = []
|
|
||||||
for (var key in o) {
|
|
||||||
ks.push(key)
|
|
||||||
}
|
|
||||||
return o[getRandom(ks)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.getRandom = getRandom
|
|
||||||
|
|
||||||
function getRandomNumber (n) {
|
|
||||||
if (n == null) {
|
|
||||||
n = 9999
|
|
||||||
}
|
|
||||||
return Math.floor(Math.random() * n)
|
|
||||||
}
|
|
||||||
g.getRandomNumber = getRandomNumber
|
|
||||||
|
|
||||||
function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions) {
|
|
||||||
function randomTransaction (root) {
|
|
||||||
var f = getRandom(transactions)
|
|
||||||
f(root)
|
|
||||||
}
|
|
||||||
for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) {
|
|
||||||
var r = Math.random()
|
|
||||||
if (r >= 0.5) {
|
|
||||||
// 50% chance to flush
|
|
||||||
users[0].connector.flushOne() // flushes for some user.. (not necessarily 0)
|
|
||||||
} else if (r >= 0.05) {
|
|
||||||
// 45% chance to create operation
|
|
||||||
randomTransaction(getRandom(objects))
|
|
||||||
} else {
|
|
||||||
// 5% chance to disconnect/reconnect
|
|
||||||
var u = getRandom(users)
|
|
||||||
if (u.connector.isDisconnected()) {
|
|
||||||
yield u.reconnect()
|
|
||||||
} else {
|
|
||||||
yield u.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yield wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
|
||||||
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions)
|
|
||||||
yield users[0].connector.flushAll()
|
|
||||||
yield wait()
|
|
||||||
for (var u in users) {
|
|
||||||
yield users[u].reconnect()
|
|
||||||
}
|
|
||||||
yield wait(100)
|
|
||||||
yield users[0].connector.flushAll()
|
|
||||||
yield g.garbageCollectAllUsers(users)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
|
||||||
yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
|
|
||||||
yield users[0].connector.flushAll()
|
|
||||||
yield g.garbageCollectAllUsers(users)
|
|
||||||
yield wait(100)
|
|
||||||
for (var u in users) {
|
|
||||||
// TODO: here, we enforce that two users never sync at the same time with u[0]
|
|
||||||
// enforce that in the connector itself!
|
|
||||||
yield users[u].reconnect()
|
|
||||||
}
|
|
||||||
yield wait(100)
|
|
||||||
yield users[0].connector.flushAll()
|
|
||||||
yield wait(100)
|
|
||||||
yield g.garbageCollectAllUsers(users)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
|
|
||||||
// gc two times because of the two gc phases (really collect everything)
|
|
||||||
yield wait(100)
|
|
||||||
for (var i in users) {
|
|
||||||
yield users[i].db.garbageCollect()
|
|
||||||
yield users[i].db.garbageCollect()
|
|
||||||
}
|
|
||||||
yield wait(100)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.compareAllUsers = async(function * compareAllUsers (users) {
|
|
||||||
var s1, s2 // state sets
|
|
||||||
var ds1, ds2 // delete sets
|
|
||||||
var allDels1, allDels2 // all deletions
|
|
||||||
var db1 = [] // operation store of user1
|
|
||||||
|
|
||||||
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
|
|
||||||
function * t1 () {
|
|
||||||
s1 = yield* this.getStateSet()
|
|
||||||
ds1 = yield* this.getDeleteSet()
|
|
||||||
allDels1 = []
|
|
||||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
|
||||||
allDels1.push(d)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function * t2 () {
|
|
||||||
s2 = yield* this.getStateSet()
|
|
||||||
ds2 = yield* this.getDeleteSet()
|
|
||||||
allDels2 = []
|
|
||||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
|
||||||
allDels2.push(d)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
yield users[0].connector.flushAll()
|
|
||||||
yield wait()
|
|
||||||
yield g.garbageCollectAllUsers(users)
|
|
||||||
|
|
||||||
for (var uid = 0; uid < users.length; uid++) {
|
|
||||||
var u = users[uid]
|
|
||||||
u.db.requestTransaction(function * () {
|
|
||||||
// compare deleted ops against deleteStore
|
|
||||||
yield* this.os.iterate(this, null, null, function * (o) {
|
|
||||||
if (o.deleted === true) {
|
|
||||||
expect(yield* this.isDeleted(o.id)).toBeTruthy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// compare deleteStore against deleted ops
|
|
||||||
var ds = []
|
|
||||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
|
||||||
ds.push(d)
|
|
||||||
})
|
|
||||||
for (var j in ds) {
|
|
||||||
var d = ds[j]
|
|
||||||
for (var i = 0; i < d.len; i++) {
|
|
||||||
var o = yield* this.getOperation([d.id[0], d.id[1] + i])
|
|
||||||
// gc'd or deleted
|
|
||||||
if (d.gc) {
|
|
||||||
expect(o).toBeFalsy()
|
|
||||||
} else {
|
|
||||||
expect(o.deleted).toBeTruthy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// compare allDels tree
|
|
||||||
yield wait()
|
|
||||||
if (s1 == null) {
|
|
||||||
u.db.requestTransaction(function * () {
|
|
||||||
yield* t1.call(this)
|
|
||||||
yield* this.os.iterate(this, null, null, function * (o) {
|
|
||||||
o = Y.utils.copyObject(o)
|
|
||||||
delete o.origin
|
|
||||||
db1.push(o)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
yield wait()
|
|
||||||
} else {
|
|
||||||
// TODO: make requestTransaction return a promise..
|
|
||||||
u.db.requestTransaction(function * () {
|
|
||||||
yield* t2.call(this)
|
|
||||||
expect(s1).toEqual(s2)
|
|
||||||
expect(allDels1).toEqual(allDels2) // inner structure
|
|
||||||
expect(ds1).toEqual(ds2) // exported structure
|
|
||||||
var count = 0
|
|
||||||
yield* this.os.iterate(this, null, null, function * (o) {
|
|
||||||
o = Y.utils.copyObject(o)
|
|
||||||
delete o.origin
|
|
||||||
expect(db1[count++]).toEqual(o)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
yield wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
g.createUsers = async(function * createUsers (self, numberOfUsers, database) {
|
|
||||||
if (Y.utils.globalRoom.users[0] != null) {
|
|
||||||
yield Y.utils.globalRoom.users[0].flushAll()
|
|
||||||
}
|
|
||||||
// destroy old users
|
|
||||||
for (var u in Y.utils.globalRoom.users) {
|
|
||||||
Y.utils.globalRoom.users[u].y.destroy()
|
|
||||||
}
|
|
||||||
self.users = null
|
|
||||||
|
|
||||||
var promises = []
|
|
||||||
for (var i = 0; i < numberOfUsers; i++) {
|
|
||||||
promises.push(Y({
|
|
||||||
db: {
|
|
||||||
name: database,
|
|
||||||
namespace: 'User ' + i,
|
|
||||||
cleanStart: true,
|
|
||||||
gcTimeout: -1
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'Test',
|
|
||||||
debug: false
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
self.users = yield Promise.all(promises)
|
|
||||||
return self.users
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
Until async/await arrives in js, we use this function to wait for promises
|
|
||||||
by yielding them.
|
|
||||||
*/
|
|
||||||
function async (makeGenerator) {
|
|
||||||
return function (arg) {
|
|
||||||
var generator = makeGenerator.apply(this, arguments)
|
|
||||||
|
|
||||||
function handle (result) {
|
|
||||||
if (result.done) return Promise.resolve(result.value)
|
|
||||||
|
|
||||||
return Promise.resolve(result.value).then(function (res) {
|
|
||||||
return handle(generator.next(res))
|
|
||||||
}, function (err) {
|
|
||||||
return handle(generator.throw(err))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return handle(generator.next())
|
|
||||||
} catch (ex) {
|
|
||||||
generator.throw(ex)
|
|
||||||
// return Promise.reject(ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.async = async
|
|
||||||
|
|
||||||
function logUsers (self) {
|
|
||||||
if (self.constructor === Array) {
|
|
||||||
self = {users: self}
|
|
||||||
}
|
|
||||||
self.users[0].db.logTable()
|
|
||||||
self.users[1].db.logTable()
|
|
||||||
self.users[2].db.logTable()
|
|
||||||
}
|
|
||||||
|
|
||||||
g.logUsers = logUsers
|
|
590
src/Struct.js
590
src/Struct.js
@ -1,4 +1,3 @@
|
|||||||
/* global Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -19,318 +18,319 @@
|
|||||||
* requiredOps
|
* requiredOps
|
||||||
- Operations that are required to execute this operation.
|
- 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 = {
|
op = {
|
||||||
/* This is the only operation that is actually not a structure, because
|
target: Id
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
encode: function (op) {
|
Delete: {
|
||||||
return {
|
encode: function (op) {
|
||||||
struct: 'List',
|
return op
|
||||||
id: op.id,
|
},
|
||||||
type: op.type
|
requiredOps: function (op) {
|
||||||
|
return [] // [op.target]
|
||||||
|
},
|
||||||
|
execute: function * (op) {
|
||||||
|
return yield* this.deleteOperation(op.target)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requiredOps: function () {
|
Insert: {
|
||||||
/*
|
/* {
|
||||||
var ids = []
|
content: any,
|
||||||
if (op.start != null) {
|
id: Id,
|
||||||
ids.push(op.start)
|
left: Id,
|
||||||
}
|
origin: Id,
|
||||||
if (op.end != null){
|
right: Id,
|
||||||
ids.push(op.end)
|
parent: Id,
|
||||||
}
|
parentSub: string (optional), // child of Map type
|
||||||
return ids
|
}
|
||||||
*/
|
*/
|
||||||
return []
|
encode: function (op) {
|
||||||
},
|
// TODO: you could not send the "left" property, then you also have to
|
||||||
execute: function * (op) {
|
// "op.left = null" in $execute or $decode
|
||||||
op.start = null
|
var e = {
|
||||||
op.end = null
|
id: op.id,
|
||||||
},
|
left: op.left,
|
||||||
ref: function * (op, pos) {
|
right: op.right,
|
||||||
if (op.start == null) {
|
origin: op.origin,
|
||||||
return null
|
parent: op.parent,
|
||||||
}
|
struct: op.struct
|
||||||
var res = null
|
|
||||||
var o = yield* this.getOperation(op.start)
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (!o.deleted) {
|
|
||||||
res = o
|
|
||||||
pos--
|
|
||||||
}
|
}
|
||||||
if (pos >= 0 && o.right != null) {
|
if (op.parentSub != null) {
|
||||||
o = (yield* this.getOperation(o.right))
|
e.parentSub = op.parentSub
|
||||||
|
}
|
||||||
|
if (op.opContent != null) {
|
||||||
|
e.opContent = op.opContent
|
||||||
} else {
|
} 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) {
|
List: {
|
||||||
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: {
|
|
||||||
/*
|
|
||||||
{
|
{
|
||||||
map: {},
|
start: null,
|
||||||
struct: "Map",
|
end: null,
|
||||||
|
struct: "List",
|
||||||
type: "",
|
type: "",
|
||||||
id: this.os.getNextOpId()
|
id: this.os.getNextOpId()
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
encode: function (op) {
|
encode: function (op) {
|
||||||
return {
|
return {
|
||||||
struct: 'Map',
|
struct: 'List',
|
||||||
type: op.type,
|
id: op.id,
|
||||||
id: op.id,
|
type: op.type
|
||||||
map: {} // overwrite map!!
|
}
|
||||||
|
},
|
||||||
|
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 () {
|
Map: {
|
||||||
return []
|
/*
|
||||||
},
|
{
|
||||||
execute: function * () {},
|
map: {},
|
||||||
/*
|
struct: "Map",
|
||||||
Get a property by name
|
type: "",
|
||||||
*/
|
id: this.os.getNextOpId()
|
||||||
get: function * (op, name) {
|
}
|
||||||
var oid = op.map[name]
|
*/
|
||||||
if (oid != null) {
|
encode: function (op) {
|
||||||
var res = yield* this.getOperation(oid)
|
return {
|
||||||
return (res == null || res.deleted) ? void 0 : (res.opContent == null
|
struct: 'Map',
|
||||||
? res.content : yield* this.getType(res.opContent))
|
type: op.type,
|
||||||
}
|
id: op.id,
|
||||||
},
|
map: {} // overwrite map!!
|
||||||
/*
|
}
|
||||||
Delete a property by name
|
},
|
||||||
*/
|
requiredOps: function () {
|
||||||
delete: function * (op, name) {
|
return []
|
||||||
var v = op.map[name] || null
|
},
|
||||||
if (v != null) {
|
execute: function * () {},
|
||||||
yield* Struct.Delete.create.call(this, {
|
/*
|
||||||
target: v
|
Get a property by name
|
||||||
})
|
*/
|
||||||
|
get: function * (op, name) {
|
||||||
|
var oid = op.map[name]
|
||||||
|
if (oid != null) {
|
||||||
|
var res = yield* this.getOperation(oid)
|
||||||
|
return (res == null || res.deleted) ? void 0 : (res.opContent == null
|
||||||
|
? res.content : yield* this.getType(res.opContent))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
Delete a property by name
|
||||||
|
*/
|
||||||
|
delete: function * (op, name) {
|
||||||
|
var v = op.map[name] || null
|
||||||
|
if (v != null) {
|
||||||
|
yield* Struct.Delete.create.call(this, {
|
||||||
|
target: v
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Y.Struct = Struct
|
||||||
}
|
}
|
||||||
Y.Struct = Struct
|
|
||||||
|
1065
src/Transaction.js
1065
src/Transaction.js
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
|||||||
/* global Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
;(function () {
|
function extend (Y) {
|
||||||
class YArray {
|
class YArray {
|
||||||
constructor (os, _model, idArray, valArray) {
|
constructor (os, _model, idArray, valArray) {
|
||||||
this.os = os
|
this.os = os
|
||||||
@ -166,7 +165,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Y.Array = new Y.utils.CustomType({
|
Y.extend('Array', new Y.utils.CustomType({
|
||||||
class: YArray,
|
class: YArray,
|
||||||
createType: function * YArrayCreator () {
|
createType: function * YArrayCreator () {
|
||||||
var modelid = this.store.getNextOpId()
|
var modelid = this.store.getNextOpId()
|
||||||
@ -188,5 +187,11 @@
|
|||||||
})
|
})
|
||||||
return new YArray(os, model.id, idArray, valArray)
|
return new YArray(os, model.id, idArray, valArray)
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
})()
|
}
|
||||||
|
|
||||||
|
if (typeof Y !== 'undefined') {
|
||||||
|
extend(Y)
|
||||||
|
} else {
|
||||||
|
module.exports = extend
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
/* global createUsers, databases, wait, Y, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, garbageCollectAllUsers, describeManyTimes */
|
/* global createUsers, databases, wait, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, garbageCollectAllUsers, describeManyTimes */
|
||||||
/* eslint-env browser,jasmine */
|
/* eslint-env browser,jasmine */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var Y = require('../SpecHelper.js')
|
||||||
var numberOfYArrayTests = 10
|
var numberOfYArrayTests = 10
|
||||||
var repeatArrayTests = 2
|
var repeatArrayTests = 2
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
/* global Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
;(function () {
|
module.exports = function (Y) {
|
||||||
class YMap {
|
class YMap {
|
||||||
constructor (os, model, contents, opContents) {
|
constructor (os, model, contents, opContents) {
|
||||||
this._model = model.id
|
this._model = model.id
|
||||||
@ -292,4 +291,4 @@
|
|||||||
return new YMap(os, model, contents, opContents)
|
return new YMap(os, model, contents, opContents)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})()
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
/* global createUsers, Y, databases, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, describeManyTimes */
|
/* global createUsers, databases, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, describeManyTimes */
|
||||||
/* eslint-env browser,jasmine */
|
/* eslint-env browser,jasmine */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var Y = require('../SpecHelper.js')
|
||||||
var numberOfYMapTests = 10
|
var numberOfYMapTests = 10
|
||||||
var repeatMapTeasts = 1
|
var repeatMapTeasts = 1
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
/* global Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
;(function () {
|
module.exports = function (Y) {
|
||||||
class YTextBind extends Y.Array['class'] {
|
class YTextBind extends Y.Array['class'] {
|
||||||
constructor (os, _model, idArray, valArray) {
|
constructor (os, _model, idArray, valArray) {
|
||||||
super(os, _model, idArray, valArray)
|
super(os, _model, idArray, valArray)
|
||||||
@ -287,4 +286,4 @@
|
|||||||
return new YTextBind(os, model.id, idArray, valArray)
|
return new YTextBind(os, model.id, idArray, valArray)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})()
|
}
|
||||||
|
303
src/Utils.js
303
src/Utils.js
@ -1,4 +1,3 @@
|
|||||||
/* global Y */
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -21,178 +20,182 @@
|
|||||||
database request to finish). EventHandler will help you to make your type
|
database request to finish). EventHandler will help you to make your type
|
||||||
synchronously.
|
synchronously.
|
||||||
*/
|
*/
|
||||||
class EventHandler {
|
module.exports = function (Y) {
|
||||||
/*
|
Y.utils = {}
|
||||||
onevent: is called when the structure changes.
|
|
||||||
|
|
||||||
Note: "awaiting opertations" is used to denote operations that were
|
class EventHandler {
|
||||||
prematurely called. Events for received operations can not be executed until
|
/*
|
||||||
all prematurely called operations were executed ("waiting operations")
|
onevent: is called when the structure changes.
|
||||||
*/
|
|
||||||
constructor (onevent) {
|
Note: "awaiting opertations" is used to denote operations that were
|
||||||
this.waiting = []
|
prematurely called. Events for received operations can not be executed until
|
||||||
this.awaiting = 0
|
all prematurely called operations were executed ("waiting operations")
|
||||||
this.onevent = onevent
|
*/
|
||||||
this.eventListeners = []
|
constructor (onevent) {
|
||||||
}
|
this.waiting = []
|
||||||
/*
|
this.awaiting = 0
|
||||||
Call this when a new operation arrives. It will be executed right away if
|
this.onevent = onevent
|
||||||
there are no waiting operations, that you prematurely executed
|
this.eventListeners = []
|
||||||
*/
|
|
||||||
receivedOp (op) {
|
|
||||||
if (this.awaiting <= 0) {
|
|
||||||
this.onevent([op])
|
|
||||||
} else {
|
|
||||||
this.waiting.push(Y.utils.copyObject(op))
|
|
||||||
}
|
}
|
||||||
}
|
/*
|
||||||
/*
|
Call this when a new operation arrives. It will be executed right away if
|
||||||
You created some operations, and you want the `onevent` function to be
|
there are no waiting operations, that you prematurely executed
|
||||||
called right away. Received operations will not be executed untill all
|
*/
|
||||||
prematurely called operations are executed
|
receivedOp (op) {
|
||||||
*/
|
if (this.awaiting <= 0) {
|
||||||
awaitAndPrematurelyCall (ops) {
|
this.onevent([op])
|
||||||
this.awaiting++
|
} else {
|
||||||
this.onevent(ops)
|
this.waiting.push(Y.utils.copyObject(op))
|
||||||
}
|
|
||||||
/*
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
/*
|
||||||
/*
|
You created some operations, and you want the `onevent` function to be
|
||||||
Call this when you successfully awaited the execution of n Insert operations
|
called right away. Received operations will not be executed untill all
|
||||||
*/
|
prematurely called operations are executed
|
||||||
awaitedInserts (n) {
|
*/
|
||||||
var ops = this.waiting.splice(this.waiting.length - n)
|
awaitAndPrematurelyCall (ops) {
|
||||||
for (var oid = 0; oid < ops.length; oid++) {
|
this.awaiting++
|
||||||
var op = ops[oid]
|
this.onevent(ops)
|
||||||
for (var i = this.waiting.length - 1; i >= 0; i--) {
|
}
|
||||||
let w = this.waiting[i]
|
/*
|
||||||
if (Y.utils.compareIds(op.left, w.id)) {
|
Basic event listener boilerplate...
|
||||||
// include the effect of op in w
|
TODO: maybe put this in a different type..
|
||||||
w.right = op.id
|
*/
|
||||||
// exclude the effect of w in op
|
addEventListener (f) {
|
||||||
op.left = w.left
|
this.eventListeners.push(f)
|
||||||
} else if (Y.utils.compareIds(op.right, w.id)) {
|
}
|
||||||
// similar..
|
removeEventListener (f) {
|
||||||
w.left = op.id
|
this.eventListeners = this.eventListeners.filter(function (g) {
|
||||||
op.right = w.right
|
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 Insert operations
|
||||||
/*
|
*/
|
||||||
Call this when you successfully awaited the execution of n Delete operations
|
awaitedInserts (n) {
|
||||||
*/
|
var ops = this.waiting.splice(this.waiting.length - n)
|
||||||
awaitedDeletes (n, newLeft) {
|
for (var oid = 0; oid < ops.length; oid++) {
|
||||||
var ops = this.waiting.splice(this.waiting.length - n)
|
var op = ops[oid]
|
||||||
for (var j in ops) {
|
for (var i = this.waiting.length - 1; i >= 0; i--) {
|
||||||
var del = ops[j]
|
|
||||||
if (newLeft != null) {
|
|
||||||
for (var i in this.waiting) {
|
|
||||||
let w = this.waiting[i]
|
let w = this.waiting[i]
|
||||||
// We will just care about w.left
|
if (Y.utils.compareIds(op.left, w.id)) {
|
||||||
if (Y.utils.compareIds(del.target, w.left)) {
|
// include the effect of op in w
|
||||||
del.left = newLeft
|
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)
|
Y.utils.EventHandler = EventHandler
|
||||||
Try to execute the events for the waiting operations
|
|
||||||
|
/*
|
||||||
|
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 () {
|
class CustomType { // eslint-disable-line
|
||||||
this.awaiting--
|
constructor (def) {
|
||||||
if (this.awaiting <= 0 && this.waiting.length > 0) {
|
if (def.createType == null ||
|
||||||
var events = this.waiting
|
def.initType == null ||
|
||||||
this.waiting = []
|
def.class == null
|
||||||
this.onevent(events)
|
) {
|
||||||
|
throw new Error('Custom type was not initialized correctly!')
|
||||||
|
}
|
||||||
|
this.createType = def.createType
|
||||||
|
this.initType = def.initType
|
||||||
|
this.class = def.class
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Y.utils.CustomType = CustomType
|
||||||
Y.utils.EventHandler = EventHandler
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A wrapper for the definition of a custom type.
|
Make a flat copy of an object
|
||||||
Every custom type must have three properties:
|
(just copy properties)
|
||||||
|
*/
|
||||||
* createType
|
function copyObject (o) {
|
||||||
- Defines the model of a newly created custom type and returns the type
|
var c = {}
|
||||||
* initType
|
for (var key in o) {
|
||||||
- Given a model, creates a custom type
|
c[key] = o[key]
|
||||||
* 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!')
|
|
||||||
}
|
}
|
||||||
this.createType = def.createType
|
return c
|
||||||
this.initType = def.initType
|
|
||||||
this.class = def.class
|
|
||||||
}
|
}
|
||||||
}
|
Y.utils.copyObject = copyObject
|
||||||
Y.utils.CustomType = CustomType
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Make a flat copy of an object
|
Defines a smaller relation on Id's
|
||||||
(just copy properties)
|
*/
|
||||||
*/
|
function smaller (a, b) {
|
||||||
function copyObject (o) {
|
return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1])
|
||||||
var c = {}
|
|
||||||
for (var key in o) {
|
|
||||||
c[key] = o[key]
|
|
||||||
}
|
}
|
||||||
return c
|
Y.utils.smaller = smaller
|
||||||
}
|
|
||||||
Y.utils.copyObject = copyObject
|
|
||||||
|
|
||||||
/*
|
function compareIds (id1, id2) {
|
||||||
Defines a smaller relation on Id's
|
if (id1 == null || id2 == null) {
|
||||||
*/
|
if (id1 == null && id2 == null) {
|
||||||
function smaller (a, b) {
|
return true
|
||||||
return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1])
|
}
|
||||||
}
|
return false
|
||||||
Y.utils.smaller = smaller
|
}
|
||||||
|
if (id1[0] === id2[0] && id1[1] === id2[1]) {
|
||||||
function compareIds (id1, id2) {
|
|
||||||
if (id1 == null || id2 == null) {
|
|
||||||
if (id1 == null && id2 == null) {
|
|
||||||
return true
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (id1[0] === id2[0] && id1[1] === id2[1]) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
Y.utils.compareIds = compareIds
|
||||||
}
|
}
|
||||||
Y.utils.compareIds = compareIds
|
|
||||||
|
71
src/y.js
71
src/y.js
@ -1,11 +1,68 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
'use strict'
|
'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) {
|
function Y (opts) {
|
||||||
return new Promise(function (resolve) {
|
opts.types = opts.types != null ? opts.types : []
|
||||||
var yconfig = new YConfig(opts, function () {
|
var modules = [opts.db.name, opts.connector.name].concat(opts.types)
|
||||||
yconfig.db.whenUserIdSet(function () {
|
var promises = []
|
||||||
resolve(yconfig)
|
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') {
|
if (typeof window !== 'undefined') {
|
||||||
window.Y = Y
|
window.Y = Y
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof YConcurrency_TestingMode !== 'undefined') {
|
|
||||||
g.Y = Y //eslint-disable-line
|
|
||||||
// debugger //eslint-disable-line
|
|
||||||
}
|
|
||||||
Y.utils = {}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user