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