switched to *standard* coding style
This commit is contained in:
parent
ee116b8ca4
commit
ee983ceff6
@ -1,4 +0,0 @@
|
||||
build/
|
||||
y.js
|
||||
y.js.map
|
||||
interfaces/
|
43
.eslintrc
43
.eslintrc
@ -1,43 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true
|
||||
},
|
||||
"rules": {
|
||||
"strict": 0,
|
||||
"camelcase": [1, {"properties": "never"}],
|
||||
"no-underscore-dangle": 0,
|
||||
"no-constant-condition": 0,
|
||||
"no-empty": 0,
|
||||
"new-cap": [2, { "capIsNewExceptions": ["List", "Y"] }],
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"globals": {
|
||||
"copyObject": true,
|
||||
"Struct": true,
|
||||
"OperationStore": true,
|
||||
"AbstractOperationStore": true,
|
||||
"AbstractTransaction": true,
|
||||
"AbstractConnector": true,
|
||||
"Transaction": true,
|
||||
"IndexedDB": true,
|
||||
"IDBRequest": true,
|
||||
"GeneratorFunction": true,
|
||||
"Y": true,
|
||||
"setTimeout": true,
|
||||
"setInterval": true,
|
||||
"Operation": true,
|
||||
"getRandom": true,
|
||||
"RBTree": true,
|
||||
"compareIds": true,
|
||||
"EventHandler": true,
|
||||
"compareAllUsers": true,
|
||||
"createUsers": true,
|
||||
"getRandomNumber": true,
|
||||
"applyRandomTransactions": true,
|
||||
"CustomType": true,
|
||||
"window": true,
|
||||
"document": true,
|
||||
"smaller": true,
|
||||
"wait": true
|
||||
}
|
||||
}
|
13
.flowconfig
13
.flowconfig
@ -1,13 +0,0 @@
|
||||
[ignore]
|
||||
.*/node_modules/.*
|
||||
.*/build/.*
|
||||
./y.js
|
||||
./y.js.map
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
./interfaces
|
||||
./src
|
||||
|
||||
[options]
|
@ -1,29 +1,30 @@
|
||||
/* global Y */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: "Memory"
|
||||
name: 'Memory'
|
||||
},
|
||||
connector: {
|
||||
name: "WebRTC",
|
||||
room: "mineeeeeee",
|
||||
name: 'WebRTC',
|
||||
room: 'mineeeeeee',
|
||||
debug: true
|
||||
}
|
||||
}).then(function(yconfig){
|
||||
window.y = yconfig.root;
|
||||
window.yconfig = yconfig;
|
||||
var textarea = document.getElementById("textfield");
|
||||
var contenteditable = document.getElementById("contenteditable");
|
||||
yconfig.root.observe(function(events){
|
||||
}).then(function (yconfig) {
|
||||
window.y = yconfig.root
|
||||
window.yconfig = yconfig
|
||||
var textarea = document.getElementById('textfield')
|
||||
var contenteditable = document.getElementById('contenteditable')
|
||||
yconfig.root.observe(function (events) {
|
||||
for (var e in events) {
|
||||
var event = events[e];
|
||||
if (event.name === "text" && (event.type === "add" || event.type === "update")) {
|
||||
event.object.get(event.name).then(function(text){ //eslint-disable-line
|
||||
text.bind(textarea);
|
||||
text.bind(contenteditable);
|
||||
window.ytext = text;
|
||||
});
|
||||
var event = events[e]
|
||||
if (event.name === 'text' && (event.type === 'add' || event.type === 'update')) {
|
||||
event.object.get(event.name).then(function (text) { // eslint-disable-line
|
||||
text.bind(textarea)
|
||||
text.bind(contenteditable)
|
||||
window.ytext = text
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
yconfig.root.set("text", Y.TextBind);
|
||||
});
|
||||
})
|
||||
yconfig.root.set('text', Y.TextBind)
|
||||
})
|
||||
|
148
gulpfile.js
148
gulpfile.js
@ -1,4 +1,4 @@
|
||||
/*eslint-env node */
|
||||
/* eslint-env node */
|
||||
|
||||
/** Gulp Commands
|
||||
|
||||
@ -38,47 +38,42 @@
|
||||
Builds the test suite
|
||||
- test:
|
||||
Test this library
|
||||
- lint:
|
||||
Lint this library. A successful lint is required for committing to this repository!
|
||||
|
||||
*/
|
||||
|
||||
|
||||
var gulp = require("gulp");
|
||||
var sourcemaps = require("gulp-sourcemaps");
|
||||
var babel = require("gulp-babel");
|
||||
var uglify = require("gulp-uglify");
|
||||
var minimist = require("minimist");
|
||||
var eslint = require("gulp-eslint");
|
||||
var jasmine = require("gulp-jasmine");
|
||||
var jasmineBrowser = require("gulp-jasmine-browser");
|
||||
var concat = require("gulp-concat");
|
||||
var watch = require("gulp-watch");
|
||||
var gulp = require('gulp')
|
||||
var sourcemaps = require('gulp-sourcemaps')
|
||||
var babel = require('gulp-babel')
|
||||
var uglify = require('gulp-uglify')
|
||||
var minimist = require('minimist')
|
||||
var jasmine = require('gulp-jasmine')
|
||||
var jasmineBrowser = require('gulp-jasmine-browser')
|
||||
var concat = require('gulp-concat')
|
||||
var watch = require('gulp-watch')
|
||||
|
||||
var polyfills = [
|
||||
"./node_modules/gulp-babel/node_modules/babel-core/node_modules/regenerator/runtime.js"
|
||||
];
|
||||
'./node_modules/gulp-babel/node_modules/babel-core/node_modules/regenerator/runtime.js'
|
||||
]
|
||||
|
||||
var options = minimist(process.argv.slice(2), {
|
||||
string: ["export", "name", "testport", "testfiles"],
|
||||
string: ['export', 'name', 'testport', 'testfiles'],
|
||||
default: {
|
||||
export: "ignore",
|
||||
name: "y.js",
|
||||
testport: "8888",
|
||||
testfiles: "src/**/*.js"
|
||||
export: 'ignore',
|
||||
name: 'y.js',
|
||||
testport: '8888',
|
||||
testfiles: 'src/**/*.js'
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
var files = {
|
||||
y: polyfills.concat(["src/y.js", "src/Connector.js", "src/OperationStore.js", "src/Struct.js", "src/Utils.js",
|
||||
"src/OperationStores/RedBlackTree.js", "src/**/*.js", "!src/**/*.spec.js"]),
|
||||
lint: ["src/**/*.js", "gulpfile.js"],
|
||||
y: polyfills.concat(['src/y.js', 'src/Connector.js', 'src/OperationStore.js', 'src/Struct.js', 'src/Utils.js',
|
||||
'src/OperationStores/RedBlackTree.js', 'src/**/*.js', '!src/**/*.spec.js']),
|
||||
test: polyfills.concat([options.testfiles]),
|
||||
build_test: ["build_test/y.js"]
|
||||
};
|
||||
build_test: ['build_test/y.js']
|
||||
}
|
||||
|
||||
gulp.task("build", function () {
|
||||
/*return gulp.src(files.y)
|
||||
gulp.task('build', function () {
|
||||
/*
|
||||
return gulp.src(files.y)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat(options.name))
|
||||
.pipe(babel({
|
||||
@ -90,71 +85,62 @@ gulp.task("build", function () {
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(gulp.dest("."));*/
|
||||
return gulp.src(files.y)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat(options.name))
|
||||
.pipe(babel({
|
||||
loose: "all",
|
||||
modules: "ignore",
|
||||
optional: ["es7.asyncFunctions"],
|
||||
blacklist: ["regenerator"],
|
||||
experimental: true
|
||||
}))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest("."));
|
||||
});
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat(options.name))
|
||||
.pipe(babel({
|
||||
loose: 'all',
|
||||
modules: 'ignore',
|
||||
optional: ['es7.asyncFunctions'],
|
||||
blacklist: ['regenerator'],
|
||||
experimental: true
|
||||
}))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('.'))
|
||||
})
|
||||
|
||||
gulp.task("lint", function(){
|
||||
return gulp.src(files.lint)
|
||||
.pipe(eslint())
|
||||
.pipe(eslint.format())
|
||||
.pipe(eslint.failOnError());
|
||||
});
|
||||
|
||||
gulp.task("test", function () {
|
||||
gulp.task('test', function () {
|
||||
return gulp.src(files.test)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat("jasmine"))
|
||||
.pipe(concat('jasmine'))
|
||||
.pipe(babel({
|
||||
loose: "all",
|
||||
optional: ["es7.asyncFunctions"],
|
||||
modules: "ignore",
|
||||
loose: 'all',
|
||||
optional: ['es7.asyncFunctions'],
|
||||
modules: 'ignore',
|
||||
experimental: true
|
||||
}))
|
||||
.pipe(uglify())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest("build"))
|
||||
.pipe(gulp.dest('build'))
|
||||
.pipe(jasmine({
|
||||
verbose: true,
|
||||
includeStuckTrace: true
|
||||
}));
|
||||
});
|
||||
}))
|
||||
})
|
||||
|
||||
gulp.task("build_jasmine_browser", function(){
|
||||
gulp.task('build_jasmine_browser', function () {
|
||||
gulp.src(files.test)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat("jasmine_browser.js"))
|
||||
.pipe(babel({
|
||||
loose: "all",
|
||||
modules: "ignore",
|
||||
optional: ["es7.asyncFunctions"],
|
||||
// blacklist: "regenerator",
|
||||
experimental: true
|
||||
}))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest("build"));
|
||||
});
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('jasmine_browser.js'))
|
||||
.pipe(babel({
|
||||
loose: 'all',
|
||||
modules: 'ignore',
|
||||
optional: ['es7.asyncFunctions'],
|
||||
// blacklist: "regenerator",
|
||||
experimental: true
|
||||
}))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('build'))
|
||||
})
|
||||
|
||||
gulp.task('develop', ['build_jasmine_browser', 'build'], function () {
|
||||
gulp.watch(files.test, ['build_jasmine_browser'])
|
||||
// gulp.watch(files.test, ["test"])
|
||||
gulp.watch(files.test, ['build'])
|
||||
|
||||
gulp.task("develop", ["build_jasmine_browser", "build"], function(){
|
||||
|
||||
gulp.watch(files.test, ["build_jasmine_browser"]);
|
||||
//gulp.watch(files.test, ["test"]);
|
||||
gulp.watch(files.test, ["build"]);
|
||||
|
||||
return gulp.src("build/jasmine_browser.js")
|
||||
.pipe(watch("build/jasmine_browser.js"))
|
||||
return gulp.src('build/jasmine_browser.js')
|
||||
.pipe(watch('build/jasmine_browser.js'))
|
||||
.pipe(jasmineBrowser.specRunner())
|
||||
.pipe(jasmineBrowser.server({port: options.testport}));
|
||||
});
|
||||
.pipe(jasmineBrowser.server({port: options.testport}))
|
||||
})
|
||||
|
||||
gulp.task("default", ["build", "test"]);
|
||||
gulp.task('default', ['build', 'test'])
|
||||
|
@ -1,5 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
declare var describe : Function;
|
||||
declare var it : Function;
|
||||
declare var expect : Function;
|
17
package.json
17
package.json
@ -5,13 +5,21 @@
|
||||
"main": "y.js",
|
||||
"scripts": {
|
||||
"test": "gulp test",
|
||||
"lint": "gulp lint",
|
||||
"lint": "standard",
|
||||
"build": "gulp build"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint",
|
||||
"build"
|
||||
],
|
||||
"standard": {
|
||||
"parser": "babel-eslint",
|
||||
"ignore": [
|
||||
"build/**",
|
||||
"./y.js",
|
||||
"./y.js.map"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/y-js/yjs.git"
|
||||
@ -33,12 +41,10 @@
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^3.1.15",
|
||||
"eslint": "^0.22.1",
|
||||
"babel-eslint": "^3.1.23",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-babel": "^5.1.0",
|
||||
"gulp-concat": "^2.5.2",
|
||||
"gulp-eslint": "^0.13.2",
|
||||
"gulp-jasmine": "^2.0.1",
|
||||
"gulp-jasmine-browser": "^0.1.3",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
@ -46,6 +52,7 @@
|
||||
"gulp-util": "^3.0.5",
|
||||
"gulp-watch": "^4.2.4",
|
||||
"minimist": "^1.1.1",
|
||||
"pre-commit": "^1.0.10"
|
||||
"pre-commit": "^1.0.10",
|
||||
"standard": "^5.0.0-2"
|
||||
}
|
||||
}
|
||||
|
247
src/Connector.js
247
src/Connector.js
@ -1,192 +1,191 @@
|
||||
|
||||
class AbstractConnector { //eslint-disable-line no-unused-vars
|
||||
class AbstractConnector { // eslint-disable-line no-unused-vars
|
||||
/*
|
||||
opts
|
||||
.role : String Role of this client ("master" or "slave")
|
||||
.userId : String that uniquely defines the user.
|
||||
*/
|
||||
constructor (y, opts) {
|
||||
this.y = y;
|
||||
if (opts == null){
|
||||
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";
|
||||
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'!");
|
||||
throw new Error("Role must be either 'master' or 'slave'!")
|
||||
}
|
||||
this.role = opts.role;
|
||||
this.connections = {};
|
||||
this.userEventListeners = [];
|
||||
this.whenSyncedListeners = [];
|
||||
this.currentSyncTarget = null;
|
||||
this.syncingClients = [];
|
||||
this.forwardToSyncingClients = (opts.forwardToSyncingClients === false) ? false : true;
|
||||
this.debug = opts.debug ? true : false;
|
||||
this.broadcastedHB = false;
|
||||
this.role = opts.role
|
||||
this.connections = {}
|
||||
this.userEventListeners = []
|
||||
this.whenSyncedListeners = []
|
||||
this.currentSyncTarget = null
|
||||
this.syncingClients = []
|
||||
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
|
||||
this.debug = opts.debug === true
|
||||
this.broadcastedHB = false
|
||||
}
|
||||
setUserId (userId) {
|
||||
this.userId = userId;
|
||||
this.y.db.setUserId(userId);
|
||||
this.userId = userId
|
||||
this.y.db.setUserId(userId)
|
||||
}
|
||||
onUserEvent (f) {
|
||||
this.userEventListeners.push(f);
|
||||
this.userEventListeners.push(f)
|
||||
}
|
||||
userLeft (user : string) {
|
||||
delete this.connections[user];
|
||||
if (user === this.currentSyncTarget){
|
||||
this.currentSyncTarget = null;
|
||||
this.findNextSyncTarget();
|
||||
userLeft (user) {
|
||||
delete this.connections[user]
|
||||
if (user === this.currentSyncTarget) {
|
||||
this.currentSyncTarget = null
|
||||
this.findNextSyncTarget()
|
||||
}
|
||||
for (var f of this.userEventListeners){
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: "userLeft",
|
||||
action: 'userLeft',
|
||||
user: user
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
userJoined (user, role) {
|
||||
if(role == null){
|
||||
throw new Error("You must specify the role of the joined user!");
|
||||
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!");
|
||||
throw new Error('This user already joined!')
|
||||
}
|
||||
this.connections[user] = {
|
||||
isSynced: false,
|
||||
role: role
|
||||
};
|
||||
}
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: "userJoined",
|
||||
action: 'userJoined',
|
||||
user: user,
|
||||
role: role
|
||||
});
|
||||
})
|
||||
}
|
||||
if (this.currentSyncTarget == null) {
|
||||
this.findNextSyncTarget();
|
||||
this.findNextSyncTarget()
|
||||
}
|
||||
}
|
||||
// Execute a function _when_ we are connected.
|
||||
// If not connected, wait until connected
|
||||
whenSynced (f) {
|
||||
if (this.isSynced === true) {
|
||||
f();
|
||||
f()
|
||||
} else {
|
||||
this.whenSyncedListeners.push(f);
|
||||
this.whenSyncedListeners.push(f)
|
||||
}
|
||||
}
|
||||
// returns false, if there is no sync target
|
||||
// true otherwise
|
||||
findNextSyncTarget () {
|
||||
if (this.currentSyncTarget != null) {
|
||||
return; // "The current sync has not finished!"
|
||||
return // "The current sync has not finished!"
|
||||
}
|
||||
|
||||
var syncUser = null;
|
||||
var syncUser = null
|
||||
for (var uid in this.connections) {
|
||||
if (!this.connections[uid].isSynced) {
|
||||
syncUser = uid;
|
||||
break;
|
||||
syncUser = uid
|
||||
break
|
||||
}
|
||||
}
|
||||
if (syncUser != null){
|
||||
var conn = this;
|
||||
this.currentSyncTarget = syncUser;
|
||||
this.y.db.requestTransaction(function*(){
|
||||
if (syncUser != null) {
|
||||
var conn = this
|
||||
this.currentSyncTarget = syncUser
|
||||
this.y.db.requestTransaction(function *() {
|
||||
conn.send(syncUser, {
|
||||
type: "sync step 1",
|
||||
type: 'sync step 1',
|
||||
stateVector: yield* this.getStateVector()
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
// set the state to synced!
|
||||
if (!this.isSynced) {
|
||||
this.isSynced = true;
|
||||
this.isSynced = true
|
||||
for (var f of this.whenSyncedListeners) {
|
||||
f();
|
||||
f()
|
||||
}
|
||||
this.whenSyncedListeners = null;
|
||||
this.whenSyncedListeners = null
|
||||
}
|
||||
}
|
||||
send (uid, message) {
|
||||
if (this.debug) {
|
||||
console.log(`me -> ${uid}: ${message.type}`);//eslint-disable-line
|
||||
console.dir(m); //eslint-disable-line
|
||||
console.log(`me -> ${uid}: ${message.type}`);// eslint-disable-line
|
||||
console.dir(m); // eslint-disable-line
|
||||
}
|
||||
super(uid, message);
|
||||
super(uid, message)
|
||||
}
|
||||
// You received a raw message, and you know that it is intended for to Yjs. Then call this function.
|
||||
receiveMessage (sender, m){
|
||||
receiveMessage (sender, m) {
|
||||
if (sender === this.userId) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (this.debug) {
|
||||
console.log(`${sender} -> me: ${m.type}`);//eslint-disable-line
|
||||
console.dir(m); //eslint-disable-line
|
||||
console.log(`${sender} -> me: ${m.type}`);// eslint-disable-line
|
||||
console.dir(m); // eslint-disable-line
|
||||
}
|
||||
if (m.type === "sync step 1") {
|
||||
if (m.type === 'sync step 1') {
|
||||
// TODO: make transaction, stream the ops
|
||||
let conn = this;
|
||||
this.y.db.requestTransaction(function*(){
|
||||
var ops = yield* this.getOperations(m.stateVector);
|
||||
var sv = yield* this.getStateVector();
|
||||
let conn = this
|
||||
this.y.db.requestTransaction(function *() {
|
||||
var ops = yield* this.getOperations(m.stateVector)
|
||||
var sv = yield* this.getStateVector()
|
||||
conn.send(sender, {
|
||||
type: "sync step 2",
|
||||
type: 'sync step 2',
|
||||
os: ops,
|
||||
stateVector: sv
|
||||
});
|
||||
})
|
||||
if (this.forwardToSyncingClients) {
|
||||
conn.syncingClients.push(sender);
|
||||
setTimeout(function(){
|
||||
conn.syncingClients = conn.syncingClients.filter(function(cli){
|
||||
return cli !== sender;
|
||||
});
|
||||
conn.syncingClients.push(sender)
|
||||
setTimeout(function () {
|
||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
||||
return cli !== sender
|
||||
})
|
||||
conn.send(sender, {
|
||||
type: "sync done"
|
||||
});
|
||||
}, conn.syncingClientDuration);
|
||||
type: 'sync done'
|
||||
})
|
||||
}, conn.syncingClientDuration)
|
||||
} else {
|
||||
conn.send(sender, {
|
||||
type: "sync done"
|
||||
});
|
||||
type: 'sync done'
|
||||
})
|
||||
}
|
||||
});
|
||||
} else if (m.type === "sync step 2") {
|
||||
this.y.db.apply(m.os);
|
||||
let conn = this;
|
||||
var broadcastHB = !this.broadcastedHB;
|
||||
this.broadcastedHB = true;
|
||||
this.y.db.requestTransaction(function*(){
|
||||
var ops = yield* this.getOperations(m.stateVector);
|
||||
})
|
||||
} else if (m.type === 'sync step 2') {
|
||||
this.y.db.apply(m.os)
|
||||
let conn = this
|
||||
var broadcastHB = !this.broadcastedHB
|
||||
this.broadcastedHB = true
|
||||
this.y.db.requestTransaction(function *() {
|
||||
var ops = yield* this.getOperations(m.stateVector)
|
||||
if (ops.length > 0) {
|
||||
m = {
|
||||
type: "update",
|
||||
type: 'update',
|
||||
ops: ops
|
||||
};
|
||||
}
|
||||
if (!broadcastHB) {
|
||||
conn.send(sender, m);
|
||||
conn.send(sender, m)
|
||||
} else {
|
||||
// broadcast only once!
|
||||
conn.broadcast(m);
|
||||
conn.broadcast(m)
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (m.type === "sync done") {
|
||||
this.connections[sender].isSynced = true;
|
||||
})
|
||||
} else if (m.type === 'sync done') {
|
||||
this.connections[sender].isSynced = true
|
||||
if (sender === this.currentSyncTarget) {
|
||||
this.currentSyncTarget = null;
|
||||
this.findNextSyncTarget();
|
||||
this.currentSyncTarget = null
|
||||
this.findNextSyncTarget()
|
||||
}
|
||||
} else if (m.type === "update") {
|
||||
} else if (m.type === 'update') {
|
||||
if (this.forwardToSyncingClients) {
|
||||
for (var client of this.syncingClients) {
|
||||
this.send(client, m);
|
||||
this.send(client, m)
|
||||
}
|
||||
}
|
||||
this.y.db.apply(m.ops);
|
||||
this.y.db.apply(m.ops)
|
||||
}
|
||||
}
|
||||
// Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||
@ -202,36 +201,36 @@ class AbstractConnector { //eslint-disable-line no-unused-vars
|
||||
// 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);
|
||||
for (var n of node.children) {
|
||||
if (n.getAttribute('isArray') === 'true') {
|
||||
return parseArray(n)
|
||||
} else {
|
||||
return parseObject(n);
|
||||
return parseObject(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
function parseObject (node) {
|
||||
var json = {};
|
||||
var json = {}
|
||||
for (var attrName in node.attrs) {
|
||||
var value = node.attrs[attrName];
|
||||
var int = parseInt(value);
|
||||
if (isNaN(int) || ("" + int) !== value){
|
||||
json[attrName] = value;
|
||||
var value = node.attrs[attrName]
|
||||
var int = parseInt(value, 10)
|
||||
if (isNaN(int) || ('' + int) !== value) {
|
||||
json[attrName] = value
|
||||
} else {
|
||||
json[attrName] = int;
|
||||
json[attrName] = int
|
||||
}
|
||||
}
|
||||
for (var n in node.children){
|
||||
var name = n.name;
|
||||
if (n.getAttribute("isArray") === "true") {
|
||||
json[name] = parseArray(n);
|
||||
for (var n in node.children) {
|
||||
var name = n.name
|
||||
if (n.getAttribute('isArray') === 'true') {
|
||||
json[name] = parseArray(n)
|
||||
} else {
|
||||
json[name] = parseObject(n);
|
||||
json[name] = parseObject(n)
|
||||
}
|
||||
}
|
||||
return json;
|
||||
return json
|
||||
}
|
||||
parseObject(m);
|
||||
parseObject(m)
|
||||
}
|
||||
// encode message in xml
|
||||
// we use string because Strophe only accepts an "xml-string"..
|
||||
@ -245,34 +244,34 @@ class AbstractConnector { //eslint-disable-line no-unused-vars
|
||||
// attributes is optional
|
||||
function encodeObject (m, json) {
|
||||
for (var name in json) {
|
||||
var value = json[name];
|
||||
var value = json[name]
|
||||
if (name == null) {
|
||||
// nop
|
||||
} else if (value.constructor === Object) {
|
||||
encodeObject(m.c(name), value);
|
||||
encodeObject(m.c(name), value)
|
||||
} else if (value.constructor === Array) {
|
||||
encodeArray(m.c(name), value);
|
||||
encodeArray(m.c(name), value)
|
||||
} else {
|
||||
m.setAttribute(name, value);
|
||||
m.setAttribute(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
function encodeArray (m, array) {
|
||||
m.setAttribute("isArray", "true");
|
||||
m.setAttribute('isArray', 'true')
|
||||
for (var e of array) {
|
||||
if (e.constructor === Object) {
|
||||
encodeObject(m.c("array-element"), e);
|
||||
encodeObject(m.c('array-element'), e)
|
||||
} else {
|
||||
encodeArray(m.c("array-element"), e);
|
||||
encodeArray(m.c('array-element'), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (obj.constructor === Object) {
|
||||
encodeObject(msg.c("y", { xmlns: "http://y.ninja/connector-stanza" }), obj);
|
||||
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);
|
||||
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
||||
} else {
|
||||
throw new Error("I can't encode this json!");
|
||||
throw new Error("I can't encode this json!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,102 +1,104 @@
|
||||
/* global getRandom, AbstractConnector, Y, wait */
|
||||
|
||||
var globalRoom = {
|
||||
users: {},
|
||||
buffers: {},
|
||||
removeUser: function(user : AbstractConnector){
|
||||
removeUser: function (user) {
|
||||
for (var i in this.users) {
|
||||
this.users[i].userLeft(user);
|
||||
this.users[i].userLeft(user)
|
||||
}
|
||||
delete this.users[user];
|
||||
delete this.buffers[user];
|
||||
delete this.users[user]
|
||||
delete this.buffers[user]
|
||||
},
|
||||
addUser: function(connector){
|
||||
this.users[connector.userId] = connector;
|
||||
this.buffers[connector.userId] = [];
|
||||
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");
|
||||
var u = this.users[uname]
|
||||
u.userJoined(connector.userId, 'master')
|
||||
connector.userJoined(u.userId, 'master')
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
function flushOne(){
|
||||
var bufs = [];
|
||||
}
|
||||
function flushOne () {
|
||||
var bufs = []
|
||||
for (var i in globalRoom.buffers) {
|
||||
if (globalRoom.buffers[i].length > 0) {
|
||||
bufs.push(i);
|
||||
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;
|
||||
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;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// setInterval(flushOne, 10);
|
||||
// setInterval(flushOne, 10)
|
||||
|
||||
var userIdCounter = 0;
|
||||
var userIdCounter = 0
|
||||
|
||||
class Test extends AbstractConnector {
|
||||
constructor (y, options) {
|
||||
if(options === undefined){
|
||||
throw new Error("Options must not be undefined!");
|
||||
if (options === undefined) {
|
||||
throw new Error('Options must not be undefined!')
|
||||
}
|
||||
options.role = "master";
|
||||
options.forwardToSyncingClients = false;
|
||||
super(y, options);
|
||||
this.setUserId((userIdCounter++) + "");
|
||||
globalRoom.addUser(this);
|
||||
this.globalRoom = globalRoom;
|
||||
options.role = 'master'
|
||||
options.forwardToSyncingClients = false
|
||||
super(y, options)
|
||||
this.setUserId((userIdCounter++) + '')
|
||||
globalRoom.addUser(this)
|
||||
this.globalRoom = globalRoom
|
||||
}
|
||||
send (userId, message) {
|
||||
globalRoom.buffers[userId].push(JSON.parse(JSON.stringify([this.userId, message])));
|
||||
globalRoom.buffers[userId].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])));
|
||||
globalRoom.buffers[key].push(JSON.parse(JSON.stringify([this.userId, message])))
|
||||
}
|
||||
}
|
||||
disconnect () {
|
||||
globalRoom.removeUser(this.userId);
|
||||
globalRoom.removeUser(this.userId)
|
||||
}
|
||||
flush() {
|
||||
var buff = globalRoom.buffers[this.userId];
|
||||
flush () {
|
||||
var buff = globalRoom.buffers[this.userId]
|
||||
while (buff.length > 0) {
|
||||
var m = buff.shift();
|
||||
this.receiveMessage(m[0], m[1]);
|
||||
var m = buff.shift()
|
||||
this.receiveMessage(m[0], m[1])
|
||||
}
|
||||
}
|
||||
flushAll () {
|
||||
var def = Promise.defer();
|
||||
var def = Promise.defer()
|
||||
// flushes may result in more created operations,
|
||||
// flush until there is nothing more to flush
|
||||
function nextFlush() {
|
||||
var c = flushOne();
|
||||
function nextFlush () {
|
||||
var c = flushOne()
|
||||
if (c) {
|
||||
while(flushOne()) {
|
||||
//nop
|
||||
while (flushOne()) {
|
||||
// nop
|
||||
}
|
||||
wait().then(nextFlush);
|
||||
wait().then(nextFlush)
|
||||
} else {
|
||||
wait().then(function(){
|
||||
def.resolve();
|
||||
});
|
||||
wait().then(function () {
|
||||
def.resolve()
|
||||
})
|
||||
}
|
||||
}
|
||||
// in the case that there are
|
||||
// still actions that want to be performed
|
||||
wait(0).then(nextFlush);
|
||||
return def.promise;
|
||||
wait(0).then(nextFlush)
|
||||
return def.promise
|
||||
}
|
||||
flushOne() {
|
||||
flushOne();
|
||||
flushOne () {
|
||||
flushOne()
|
||||
}
|
||||
}
|
||||
|
||||
Y.Test = Test;
|
||||
Y.Test = Test
|
||||
|
@ -1,86 +1,87 @@
|
||||
/* global AbstractConnector, Y */
|
||||
|
||||
class WebRTC extends AbstractConnector {
|
||||
constructor (y, options) {
|
||||
if(options === undefined){
|
||||
throw new Error("Options must not be undefined!");
|
||||
if (options === undefined) {
|
||||
throw new Error('Options must not be undefined!')
|
||||
}
|
||||
if(options.room == null) {
|
||||
throw new Error("You must define a room name!");
|
||||
if (options.room == null) {
|
||||
throw new Error('You must define a room name!')
|
||||
}
|
||||
options.role = "slave";
|
||||
super(y, options);
|
||||
options.role = 'slave'
|
||||
super(y, options)
|
||||
|
||||
var room = options.room;
|
||||
var room = options.room
|
||||
|
||||
var webrtcOptions = {
|
||||
url: options.url || "https://yatta.ninja:8888",
|
||||
url: options.url || 'https://yatta.ninja:8888',
|
||||
room: options.room
|
||||
};
|
||||
}
|
||||
|
||||
var swr = new SimpleWebRTC(webrtcOptions); //eslint-disable-line no-undef
|
||||
this.swr = swr;
|
||||
var self = this;
|
||||
var swr = new SimpleWebRTC(webrtcOptions) // eslint-disable-line no-undef
|
||||
this.swr = swr
|
||||
var self = this
|
||||
|
||||
swr.once("connectionReady", function(userId){
|
||||
swr.once('connectionReady', function (userId) {
|
||||
// SimpleWebRTC (swr) is initialized
|
||||
swr.joinRoom(room);
|
||||
swr.joinRoom(room)
|
||||
|
||||
swr.once("joinedRoom", function(){
|
||||
self.setUserId(userId);
|
||||
swr.once('joinedRoom', function () {
|
||||
self.setUserId(userId)
|
||||
/*
|
||||
var i;
|
||||
var i
|
||||
// notify the connector class about all the users that already
|
||||
// joined the session
|
||||
for(i in self.swr.webrtc.peers){
|
||||
self.userJoined(self.swr.webrtc.peers[i].id, "master");
|
||||
self.userJoined(self.swr.webrtc.peers[i].id, "master")
|
||||
}*/
|
||||
swr.on("channelMessage", function(peer, room_, message){
|
||||
swr.on('channelMessage', function (peer, room_, message) {
|
||||
// The client received a message
|
||||
// Check if the connector is already initialized,
|
||||
// only then forward the message to the connector class
|
||||
if(message.type != null ){
|
||||
self.receiveMessage(peer.id, message.payload);
|
||||
if (message.type != null) {
|
||||
self.receiveMessage(peer.id, message.payload)
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
swr.on("createdPeer", function(peer){
|
||||
swr.on('createdPeer', function (peer) {
|
||||
// a new peer/client joined the session.
|
||||
// Notify the connector class, if the connector
|
||||
// is already initialized
|
||||
self.userJoined(peer.id, "master");
|
||||
});
|
||||
self.userJoined(peer.id, 'master')
|
||||
})
|
||||
|
||||
swr.on("peerStreamRemoved", function(peer){
|
||||
swr.on('peerStreamRemoved', function (peer) {
|
||||
// a client left the session.
|
||||
// Notify the connector class, if the connector
|
||||
// is already initialized
|
||||
self.userLeft(peer.id);
|
||||
});
|
||||
});
|
||||
self.userLeft(peer.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
send (uid, message) {
|
||||
var self = this;
|
||||
var self = this
|
||||
// we have to make sure that the message is sent under all circumstances
|
||||
var send = function(){
|
||||
var send = function () {
|
||||
// check if the clients still exists
|
||||
var peer = self.swr.webrtc.getPeers(uid)[0];
|
||||
var success;
|
||||
if(peer){
|
||||
var peer = self.swr.webrtc.getPeers(uid)[0]
|
||||
var success
|
||||
if (peer) {
|
||||
// success is true, if the message is successfully sent
|
||||
success = peer.sendDirectly("simplewebrtc", "yjs", message);
|
||||
success = peer.sendDirectly('simplewebrtc', 'yjs', message)
|
||||
}
|
||||
if(!success){
|
||||
if (!success) {
|
||||
// resend the message if it didn't work
|
||||
setTimeout(send, 500);
|
||||
setTimeout(send, 500)
|
||||
}
|
||||
};
|
||||
}
|
||||
// try to send the message
|
||||
send();
|
||||
send()
|
||||
}
|
||||
broadcast (message) {
|
||||
this.swr.sendDirectlyToAll("simplewebrtc", "yjs", message);
|
||||
this.swr.sendDirectlyToAll('simplewebrtc', 'yjs', message)
|
||||
}
|
||||
}
|
||||
|
||||
Y.WebRTC = WebRTC;
|
||||
Y.WebRTC = WebRTC
|
||||
|
@ -1,107 +1,107 @@
|
||||
/* @flow */
|
||||
/*eslint-env browser,jasmine */
|
||||
/* global Y */
|
||||
/* eslint-env browser,jasmine */
|
||||
|
||||
/***
|
||||
This is "just" a compilation of functions that help to test this library!
|
||||
/*
|
||||
This is just a compilation of functions that help to test this library!
|
||||
***/
|
||||
|
||||
function wait(t = 0) {//eslint-disable-line
|
||||
var def = Promise.defer();
|
||||
setTimeout(function(){
|
||||
def.resolve();
|
||||
}, t);
|
||||
return def.promise;
|
||||
var def = Promise.defer()
|
||||
setTimeout(function () {
|
||||
def.resolve()
|
||||
}, t)
|
||||
return def.promise
|
||||
}
|
||||
|
||||
// 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)];
|
||||
return o[Math.floor(Math.random() * o.length)]
|
||||
} else if (o.constructor === Object) {
|
||||
var ks = [];
|
||||
var ks = []
|
||||
for (var key in o) {
|
||||
ks.push(key);
|
||||
ks.push(key)
|
||||
}
|
||||
return o[getRandom(ks)];
|
||||
return o[getRandom(ks)]
|
||||
}
|
||||
}
|
||||
function getRandomNumber(n) {//eslint-disable-line
|
||||
if (n == null) {
|
||||
n = 9999;
|
||||
n = 9999
|
||||
}
|
||||
return Math.floor(Math.random() * n);
|
||||
return Math.floor(Math.random() * n)
|
||||
}
|
||||
|
||||
async function applyRandomTransactions (users, objects, transactions, numberOfTransactions) {//eslint-disable-line
|
||||
function randomTransaction (root) {
|
||||
var f = getRandom(transactions);
|
||||
f(root);
|
||||
var f = getRandom(transactions)
|
||||
f(root)
|
||||
}
|
||||
for(var i = 0; i < numberOfTransactions; i++) {
|
||||
var r = Math.random();
|
||||
for (var i = 0; i < numberOfTransactions; i++) {
|
||||
var r = Math.random()
|
||||
if (r >= 0.9) {
|
||||
// 10% chance to flush
|
||||
users[0].connector.flushOne();
|
||||
users[0].connector.flushOne()
|
||||
} else {
|
||||
randomTransaction(getRandom(objects));
|
||||
randomTransaction(getRandom(objects))
|
||||
}
|
||||
wait();
|
||||
wait()
|
||||
}
|
||||
}
|
||||
|
||||
async function compareAllUsers(users){//eslint-disable-line
|
||||
var s1, s2;
|
||||
var db1 = [];
|
||||
function* t1(){
|
||||
s1 = yield* this.getStateSet();
|
||||
var s1, s2
|
||||
var db1 = []
|
||||
function * t1 () {
|
||||
s1 = yield* this.getStateSet()
|
||||
}
|
||||
function* t2(){
|
||||
s2 = yield* this.getStateSet();
|
||||
function * t2 () {
|
||||
s2 = yield* this.getStateSet()
|
||||
}
|
||||
await users[0].connector.flushAll();
|
||||
await users[0].connector.flushAll()
|
||||
for (var uid = 0; uid < users.length; uid++) {
|
||||
if (s1 == null) {
|
||||
var u = users[uid];
|
||||
u.db.requestTransaction(t1);
|
||||
await wait();
|
||||
var u = users[uid]
|
||||
u.db.requestTransaction(t1)
|
||||
await wait()
|
||||
u.db.os.iterate(null, null, function(o){//eslint-disable-line
|
||||
db1.push(o);
|
||||
});
|
||||
db1.push(o)
|
||||
})
|
||||
} else {
|
||||
var u2 = users[uid];
|
||||
u2.db.requestTransaction(t2);
|
||||
await wait();
|
||||
expect(s1).toEqual(s2);
|
||||
var count = 0;
|
||||
var u2 = users[uid]
|
||||
u2.db.requestTransaction(t2)
|
||||
await wait()
|
||||
expect(s1).toEqual(s2)
|
||||
var count = 0
|
||||
u2.db.os.iterate(null, null, function(o){//eslint-disable-line
|
||||
expect(db1[count++]).toEqual(o);
|
||||
});
|
||||
expect(db1[count++]).toEqual(o)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createUsers(self, numberOfUsers) {//eslint-disable-line
|
||||
if (globalRoom.users[0] != null) {//eslint-disable-line
|
||||
await globalRoom.users[0].flushAll();//eslint-disable-line
|
||||
await globalRoom.users[0].flushAll()//eslint-disable-line
|
||||
}
|
||||
//destroy old users
|
||||
// destroy old users
|
||||
for (var u in globalRoom.users) {//eslint-disable-line
|
||||
globalRoom.users[u].y.destroy()//eslint-disable-line
|
||||
}
|
||||
self.users = [];
|
||||
self.users = []
|
||||
|
||||
var promises = [];
|
||||
var promises = []
|
||||
for (var i = 0; i < numberOfUsers; i++) {
|
||||
promises.push(Y({
|
||||
db: {
|
||||
name: "Memory"
|
||||
name: 'Memory'
|
||||
},
|
||||
connector: {
|
||||
name: "Test",
|
||||
name: 'Test',
|
||||
debug: false
|
||||
}
|
||||
}));
|
||||
}))
|
||||
}
|
||||
self.users = await Promise.all(promises);
|
||||
self.users = await Promise.all(promises)
|
||||
}
|
||||
|
@ -1,58 +1,52 @@
|
||||
/* @flow */
|
||||
class AbstractTransaction { //eslint-disable-line no-unused-vars
|
||||
constructor (store : OperationStore) {
|
||||
this.store = store;
|
||||
/* global Y, copyObject, Struct, RBTree */
|
||||
|
||||
class AbstractTransaction { // eslint-disable-line no-unused-vars
|
||||
constructor (store) {
|
||||
this.store = store
|
||||
}
|
||||
*getType (id) {
|
||||
var sid = JSON.stringify(id);
|
||||
var t = this.store.initializedTypes[sid];
|
||||
* getType (id) {
|
||||
var sid = JSON.stringify(id)
|
||||
var t = this.store.initializedTypes[sid]
|
||||
if (t == null) {
|
||||
var op = yield* this.getOperation(id);
|
||||
var op = yield* this.getOperation(id)
|
||||
if (op != null) {
|
||||
t = yield* Y[op.type].initType.call(this, this.store, op);
|
||||
this.store.initializedTypes[sid] = t;
|
||||
t = yield* Y[op.type].initType.call(this, this.store, op)
|
||||
this.store.initializedTypes[sid] = t
|
||||
}
|
||||
}
|
||||
return t;
|
||||
return t
|
||||
}
|
||||
*createType (model) {
|
||||
var sid = JSON.stringify(model.id);
|
||||
var t = yield* Y[model.type].initType.call(this, this.store, model);
|
||||
this.store.initializedTypes[sid] = t;
|
||||
return t;
|
||||
* createType (model) {
|
||||
var sid = JSON.stringify(model.id)
|
||||
var t = yield* Y[model.type].initType.call(this, this.store, model)
|
||||
this.store.initializedTypes[sid] = t
|
||||
return t
|
||||
}
|
||||
*applyCreatedOperations (ops) {
|
||||
var send = [];
|
||||
* applyCreatedOperations (ops) {
|
||||
var send = []
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
var op = ops[i];
|
||||
yield* this.store.tryExecute.call(this, op);
|
||||
send.push(copyObject(Struct[op.struct].encode(op)));
|
||||
var op = ops[i]
|
||||
yield* this.store.tryExecute.call(this, op)
|
||||
send.push(copyObject(Struct[op.struct].encode(op)))
|
||||
}
|
||||
if (this.store.y.connector.broadcastedHB){
|
||||
if (this.store.y.connector.broadcastedHB) {
|
||||
this.store.y.connector.broadcast({
|
||||
type: "update",
|
||||
type: 'update',
|
||||
ops: send
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Listener = {
|
||||
f : GeneratorFunction, // is called when all operations are available
|
||||
missing : number // number of operations that are missing
|
||||
}
|
||||
|
||||
type Id = [string, number];
|
||||
|
||||
class AbstractOperationStore { //eslint-disable-line no-unused-vars
|
||||
class AbstractOperationStore { // eslint-disable-line no-unused-vars
|
||||
constructor (y) {
|
||||
this.y = y;
|
||||
this.y = y
|
||||
// E.g. this.listenersById[id] : Array<Listener>
|
||||
this.listenersById = {};
|
||||
this.listenersById = {}
|
||||
// Execute the next time a transaction is requested
|
||||
this.listenersByIdExecuteNow = [];
|
||||
this.listenersByIdExecuteNow = []
|
||||
// A transaction is requested
|
||||
this.listenersByIdRequestPending = false;
|
||||
this.listenersByIdRequestPending = false
|
||||
/* To make things more clear, the following naming conventions:
|
||||
* ls : we put this.listenersById on ls
|
||||
* l : Array<Listener>
|
||||
@ -65,163 +59,163 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
|
||||
*/
|
||||
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
|
||||
// wont be kept in memory.
|
||||
this.initializedTypes = {};
|
||||
this.whenUserIdSetListener = null;
|
||||
this.waitingOperations = new RBTree();
|
||||
this.initializedTypes = {}
|
||||
this.whenUserIdSetListener = null
|
||||
this.waitingOperations = new RBTree()
|
||||
}
|
||||
setUserId (userId) {
|
||||
this.userId = userId;
|
||||
this.opClock = 0;
|
||||
this.userId = userId
|
||||
this.opClock = 0
|
||||
if (this.whenUserIdSetListener != null) {
|
||||
this.whenUserIdSetListener();
|
||||
this.whenUserIdSetListener = null;
|
||||
this.whenUserIdSetListener()
|
||||
this.whenUserIdSetListener = null
|
||||
}
|
||||
}
|
||||
whenUserIdSet (f) {
|
||||
if (this.userId != null) {
|
||||
f();
|
||||
f()
|
||||
} else {
|
||||
this.whenUserIdSetListener = f;
|
||||
this.whenUserIdSetListener = f
|
||||
}
|
||||
}
|
||||
getNextOpId () {
|
||||
if (this.userId == null) {
|
||||
throw new Error("OperationStore not yet initialized!");
|
||||
throw new Error('OperationStore not yet initialized!')
|
||||
}
|
||||
return [this.userId, this.opClock++];
|
||||
return [this.userId, this.opClock++]
|
||||
}
|
||||
apply (ops) {
|
||||
for (var key in ops) {
|
||||
var o = ops[key];
|
||||
var required = Struct[o.struct].requiredOps(o);
|
||||
this.whenOperationsExist(required, o);
|
||||
var o = ops[key]
|
||||
var required = 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 : Array<Id>, op : Operation) {
|
||||
whenOperationsExist (ids, op) {
|
||||
if (ids.length > 0) {
|
||||
let listener : Listener = {
|
||||
let listener = {
|
||||
op: op,
|
||||
missing: ids.length
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
l.push(listener)
|
||||
}
|
||||
} else {
|
||||
this.listenersByIdExecuteNow.push({
|
||||
op: op
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (this.listenersByIdRequestPending){
|
||||
return;
|
||||
if (this.listenersByIdRequestPending) {
|
||||
return
|
||||
}
|
||||
|
||||
this.listenersByIdRequestPending = true;
|
||||
var store = this;
|
||||
this.listenersByIdRequestPending = true
|
||||
var store = this
|
||||
|
||||
this.requestTransaction(function*(){
|
||||
var exeNow = store.listenersByIdExecuteNow;
|
||||
store.listenersByIdExecuteNow = [];
|
||||
this.requestTransaction(function *() {
|
||||
var exeNow = store.listenersByIdExecuteNow
|
||||
store.listenersByIdExecuteNow = []
|
||||
|
||||
var ls = store.listenersById;
|
||||
store.listenersById = {};
|
||||
var ls = store.listenersById
|
||||
store.listenersById = {}
|
||||
|
||||
store.listenersByIdRequestPending = false;
|
||||
store.listenersByIdRequestPending = false
|
||||
|
||||
for (let key in exeNow) {
|
||||
let o = exeNow[key].op;
|
||||
yield* store.tryExecute.call(this, o);
|
||||
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;
|
||||
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);
|
||||
let listener = l[key]
|
||||
let o = listener.op
|
||||
if (--listener.missing === 0) {
|
||||
yield* store.tryExecute.call(this, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
*tryExecute (op) {
|
||||
if (op.struct === "Delete") {
|
||||
yield* Struct.Delete.execute.call(this, op);
|
||||
* tryExecute (op) {
|
||||
if (op.struct === 'Delete') {
|
||||
yield* Struct.Delete.execute.call(this, op)
|
||||
} else {
|
||||
while (op != null) {
|
||||
var state = yield* this.getState(op.id[0]);
|
||||
if (op.id[1] === state.clock){
|
||||
state.clock++;
|
||||
yield* this.setState.call(this, state);
|
||||
yield* Struct[op.struct].execute.call(this, op);
|
||||
yield* this.addOperation(op);
|
||||
yield* this.store.operationAdded(this, op);
|
||||
var state = yield* this.getState(op.id[0])
|
||||
if (op.id[1] === state.clock) {
|
||||
state.clock++
|
||||
yield* this.setState(state)
|
||||
yield* Struct[op.struct].execute.call(this, op)
|
||||
yield* this.addOperation(op)
|
||||
yield* this.store.operationAdded(this, op)
|
||||
// find next operation to execute
|
||||
op = this.store.waitingOperations.find([op.id[0], state.clock]);
|
||||
op = this.store.waitingOperations.find([op.id[0], state.clock])
|
||||
if (op != null) {
|
||||
this.store.waitingOperations.delete([op.id[0], state.clock]);
|
||||
this.store.waitingOperations.delete([op.id[0], state.clock])
|
||||
}
|
||||
} else {
|
||||
if (op.id[1] > state.clock) {
|
||||
// has to be executed at some point later
|
||||
this.store.waitingOperations.add(op);
|
||||
this.store.waitingOperations.add(op)
|
||||
}
|
||||
op = null;
|
||||
op = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// called by a transaction when an operation is added
|
||||
*operationAdded (transaction, op) {
|
||||
var sid = JSON.stringify(op.id);
|
||||
var l = this.listenersById[sid];
|
||||
delete this.listenersById[sid];
|
||||
* operationAdded (transaction, op) {
|
||||
var sid = JSON.stringify(op.id)
|
||||
var l = this.listenersById[sid]
|
||||
delete this.listenersById[sid]
|
||||
|
||||
// notify whenOperation listeners (by id)
|
||||
if (l != null) {
|
||||
for (var key in l){
|
||||
var listener = l[key];
|
||||
if (--listener.missing === 0){
|
||||
this.whenOperationsExist([], listener.op);
|
||||
for (var key in l) {
|
||||
var listener = l[key]
|
||||
if (--listener.missing === 0) {
|
||||
this.whenOperationsExist([], listener.op)
|
||||
}
|
||||
}
|
||||
}
|
||||
// notify parent, if it has been initialized as a custom type
|
||||
var t = this.initializedTypes[JSON.stringify(op.parent)];
|
||||
var t = this.initializedTypes[JSON.stringify(op.parent)]
|
||||
if (t != null) {
|
||||
yield* t._changed(transaction, copyObject(op));
|
||||
yield* t._changed(transaction, copyObject(op))
|
||||
}
|
||||
}
|
||||
removeParentListener (id, f) {
|
||||
var ls = this.parentListeners[id];
|
||||
var ls = this.parentListeners[id]
|
||||
if (ls != null) {
|
||||
this.parentListeners[id] = ls.filter(function(g){
|
||||
return (f !== g);
|
||||
});
|
||||
this.parentListeners[id] = ls.filter(function (g) {
|
||||
return (f !== g)
|
||||
})
|
||||
}
|
||||
}
|
||||
addParentListener (id, f) {
|
||||
var ls = this.parentListeners[JSON.stringify(id)];
|
||||
var ls = this.parentListeners[JSON.stringify(id)]
|
||||
if (ls == null) {
|
||||
ls = [];
|
||||
this.parentListeners[JSON.stringify(id)] = ls;
|
||||
ls = []
|
||||
this.parentListeners[JSON.stringify(id)] = ls
|
||||
}
|
||||
ls.push(f);
|
||||
ls.push(f)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
/* @flow */
|
||||
/*eslint-env browser,jasmine,console */
|
||||
|
||||
describe("OperationStore", function() {
|
||||
});
|
@ -1,208 +1,179 @@
|
||||
|
||||
type State = {
|
||||
user: string,
|
||||
clock: number
|
||||
};
|
||||
|
||||
type StateVector = Array<State>;
|
||||
|
||||
type StateSet = Object;
|
||||
|
||||
type IDBTransaction = Function;
|
||||
type IDBObjectStore = Function;
|
||||
type IDBRequest = Function;
|
||||
type IDBCursor = Function;
|
||||
type IDBKeyRange = Function;
|
||||
|
||||
type IDBOpenDBRequest = Function;
|
||||
|
||||
declare var indexedDB : Object;
|
||||
|
||||
Y.IndexedDB = (function(){ //eslint-disable-line no-unused-vars
|
||||
class Transaction extends AbstractTransaction { //eslint-disable-line
|
||||
transaction: IDBTransaction;
|
||||
sv: IDBObjectStore;
|
||||
os: IDBObjectStore;
|
||||
store: OperationStore;
|
||||
|
||||
constructor (store : OperationStore) {
|
||||
super(store);
|
||||
this.transaction = store.db.transaction(["OperationStore", "StateVector"], "readwrite");
|
||||
this.sv = this.transaction.objectStore("StateVector");
|
||||
this.os = this.transaction.objectStore("OperationStore");
|
||||
this.buffer = {};
|
||||
Y.IndexedDB = (function () { // eslint-disable-line
|
||||
class Transaction extends AbstractTransaction { // eslint-disable-line
|
||||
constructor (store) {
|
||||
super(store)
|
||||
this.transaction = store.db.transaction(['OperationStore', 'StateVector'], 'readwrite')
|
||||
this.sv = this.transaction.objectStore('StateVector')
|
||||
this.os = this.transaction.objectStore('OperationStore')
|
||||
this.buffer = {}
|
||||
}
|
||||
*setOperation (op) {
|
||||
yield this.os.put(op);
|
||||
this.buffer[JSON.stringify(op.id)] = op;
|
||||
return op;
|
||||
* setOperation (op) {
|
||||
yield this.os.put(op)
|
||||
this.buffer[JSON.stringify(op.id)] = op
|
||||
return op
|
||||
}
|
||||
*getOperation (id) {
|
||||
var op = this.buffer[JSON.stringify(id)];
|
||||
* getOperation (id) {
|
||||
var op = this.buffer[JSON.stringify(id)]
|
||||
if (op == null) {
|
||||
op = yield this.os.get(id);
|
||||
this.buffer[JSON.stringify(id)] = op;
|
||||
op = yield this.os.get(id)
|
||||
this.buffer[JSON.stringify(id)] = op
|
||||
}
|
||||
return op;
|
||||
return op
|
||||
}
|
||||
*removeOperation (id) {
|
||||
this.buffer[JSON.stringify(id)] = null;
|
||||
return yield this.os.delete(id);
|
||||
* removeOperation (id) {
|
||||
this.buffer[JSON.stringify(id)] = null
|
||||
return yield this.os.delete(id)
|
||||
}
|
||||
*setState (state : State) : State {
|
||||
return yield this.sv.put(state);
|
||||
* setState (state) {
|
||||
return yield this.sv.put(state)
|
||||
}
|
||||
*getState (user : string) : State {
|
||||
var state;
|
||||
if ((state = yield this.sv.get(user)) != null){
|
||||
return state;
|
||||
* getState (user) {
|
||||
var state
|
||||
if ((state = yield this.sv.get(user)) != null) {
|
||||
return state
|
||||
} else {
|
||||
return {
|
||||
user: user,
|
||||
clock: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
*getStateVector () : StateVector {
|
||||
var stateVector = [];
|
||||
var cursorResult = this.sv.openCursor();
|
||||
var cursor;
|
||||
while ((cursor = yield cursorResult) != null) {
|
||||
stateVector.push(cursor.value);
|
||||
cursor.continue();
|
||||
}
|
||||
return stateVector;
|
||||
}
|
||||
*getStateSet () : StateSet {
|
||||
var sv : StateVector = yield* this.getStateVector();
|
||||
var ss : StateSet = {};
|
||||
for (var state of sv){
|
||||
ss[state.user] = state.clock;
|
||||
}
|
||||
return ss;
|
||||
}
|
||||
|
||||
*getOperations (startSS : StateSet) {
|
||||
if (startSS == null){
|
||||
startSS = {};
|
||||
}
|
||||
var ops = [];
|
||||
|
||||
var endSV : StateVector = yield* this.getStateVector();
|
||||
for (var endState of endSV) {
|
||||
var user = endState.user;
|
||||
var startPos = startSS[user] || 0;
|
||||
var endPos = endState.clock;
|
||||
var range = IDBKeyRange.bound([user, startPos], [user, endPos]);
|
||||
var cursorResult = this.os.openCursor(range);
|
||||
var cursor;
|
||||
while ((cursor = yield cursorResult) != null) {
|
||||
ops.push(cursor.value);
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
* getStateVector () {
|
||||
var stateVector = []
|
||||
var cursorResult = this.sv.openCursor()
|
||||
var cursor
|
||||
while ((cursor = yield cursorResult) != null) {
|
||||
stateVector.push(cursor.value)
|
||||
cursor.continue()
|
||||
}
|
||||
return stateVector
|
||||
}
|
||||
* getStateSet () {
|
||||
var sv = yield* this.getStateVector()
|
||||
var ss = {}
|
||||
for (var state of sv) {
|
||||
ss[state.user] = state.clock
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
* getOperations (startSS) {
|
||||
if (startSS == null) {
|
||||
startSS = {}
|
||||
}
|
||||
var ops = []
|
||||
|
||||
var endSV = yield* this.getStateVector()
|
||||
for (var endState of endSV) {
|
||||
var user = endState.user
|
||||
var startPos = startSS[user] || 0
|
||||
var endPos = endState.clock
|
||||
var range = window.IDBKeyRange.bound([user, startPos], [user, endPos])
|
||||
var cursorResult = this.os.openCursor(range)
|
||||
var cursor
|
||||
while ((cursor = yield cursorResult) != null) {
|
||||
ops.push(cursor.value)
|
||||
cursor.continue()
|
||||
}
|
||||
}
|
||||
return ops
|
||||
}
|
||||
}
|
||||
class OperationStore extends AbstractOperationStore { //eslint-disable-line no-undef
|
||||
namespace: string;
|
||||
ready: Promise;
|
||||
whenReadyListeners: Array<Function>;
|
||||
class OperationStore extends AbstractOperationStore { // eslint-disable-line no-undef
|
||||
constructor (y, opts) {
|
||||
super(y);
|
||||
super(y)
|
||||
if (opts == null) {
|
||||
opts = {};
|
||||
opts = {}
|
||||
}
|
||||
if (opts.namespace == null || typeof opts.namespace !== "string") {
|
||||
throw new Error("IndexedDB: expect a string (opts.namespace)!");
|
||||
if (opts.namespace == null || typeof opts.namespace !== 'string') {
|
||||
throw new Error('IndexedDB: expect a string (opts.namespace)!')
|
||||
} else {
|
||||
this.namespace = opts.namespace;
|
||||
this.namespace = opts.namespace
|
||||
}
|
||||
if (opts.idbVersion != null) {
|
||||
this.idbVersion = opts.idbVersion;
|
||||
this.idbVersion = opts.idbVersion
|
||||
} else {
|
||||
this.idbVersion = 5;
|
||||
this.idbVersion = 5
|
||||
}
|
||||
|
||||
this.transactionQueue = {
|
||||
queue: [],
|
||||
onRequest: null
|
||||
};
|
||||
}
|
||||
|
||||
var store = this;
|
||||
var store = this
|
||||
|
||||
var tGen = (function *transactionGen(){
|
||||
store.db = yield indexedDB.open(opts.namespace, store.idbVersion);
|
||||
var transactionQueue = store.transactionQueue;
|
||||
var tGen = (function * transactionGen () {
|
||||
store.db = yield window.indexedDB.open(opts.namespace, store.idbVersion)
|
||||
var transactionQueue = store.transactionQueue
|
||||
|
||||
var transaction = null;
|
||||
var cont = true;
|
||||
var transaction = null
|
||||
var cont = true
|
||||
while (cont) {
|
||||
var request = yield transactionQueue;
|
||||
transaction = new Transaction(store);
|
||||
var request = yield transactionQueue
|
||||
transaction = new Transaction(store)
|
||||
|
||||
yield* request.call(transaction, request);/*
|
||||
yield* request.call(transaction, request) /*
|
||||
while (transactionQueue.queue.length > 0) {
|
||||
yield* transactionQueue.queue.shift().call(transaction);
|
||||
yield* transactionQueue.queue.shift().call(transaction)
|
||||
}*/
|
||||
}
|
||||
})();
|
||||
})()
|
||||
|
||||
function handleTransactions(t){ //eslint-disable-line no-unused-vars
|
||||
var request = t.value;
|
||||
if (t.done){
|
||||
return;
|
||||
} else if (request.constructor === IDBRequest
|
||||
|| request.constructor === IDBCursor ) {
|
||||
request.onsuccess = function(){
|
||||
handleTransactions(tGen.next(request.result));
|
||||
};
|
||||
request.onerror = function(err){
|
||||
tGen.throw(err);
|
||||
};
|
||||
} else if (request === store.transactionQueue) {
|
||||
if (request.queue.length > 0){
|
||||
handleTransactions(tGen.next(request.queue.shift()));
|
||||
} else {
|
||||
request.onRequest = function(){
|
||||
request.onRequest = null;
|
||||
handleTransactions(tGen.next(request.queue.shift()));
|
||||
};
|
||||
function handleTransactions (t) { // eslint-disable-line no-unused-vars
|
||||
var request = t.value
|
||||
if (t.done) {
|
||||
return
|
||||
} else if (request.constructor === window.IDBRequest || request.constructor === window.IDBCursor) {
|
||||
request.onsuccess = function () {
|
||||
handleTransactions(tGen.next(request.result))
|
||||
}
|
||||
} else if ( request.constructor === IDBOpenDBRequest ) {
|
||||
request.onsuccess = function(event){
|
||||
var db = event.target.result;
|
||||
handleTransactions(tGen.next(db));
|
||||
};
|
||||
request.onerror = function(){
|
||||
tGen.throw("Couldn't open IndexedDB database!");
|
||||
};
|
||||
request.onupgradeneeded = function(event){
|
||||
var db = event.target.result;
|
||||
try {
|
||||
db.createObjectStore("OperationStore", {keyPath: "id"});
|
||||
db.createObjectStore("StateVector", {keyPath: "user"});
|
||||
} catch (e) {
|
||||
// console.log("Store already exists!");
|
||||
request.onerror = function (err) {
|
||||
tGen.throw(err)
|
||||
}
|
||||
} else if (request === store.transactionQueue) {
|
||||
if (request.queue.length > 0) {
|
||||
handleTransactions(tGen.next(request.queue.shift()))
|
||||
} else {
|
||||
request.onRequest = function () {
|
||||
request.onRequest = null
|
||||
handleTransactions(tGen.next(request.queue.shift()))
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (request.constructor === window.IDBOpenDBRequest) {
|
||||
request.onsuccess = function (event) {
|
||||
var db = event.target.result
|
||||
handleTransactions(tGen.next(db))
|
||||
}
|
||||
request.onerror = function () {
|
||||
tGen.throw("Couldn't open IndexedDB database!")
|
||||
}
|
||||
request.onupgradeneeded = function (event) {
|
||||
var db = event.target.result
|
||||
try {
|
||||
db.createObjectStore('OperationStore', {keyPath: 'id'})
|
||||
db.createObjectStore('StateVector', {keyPath: 'user'})
|
||||
} catch (e) {
|
||||
// console.log("Store already exists!")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tGen.throw("You can not yield this type!");
|
||||
tGen.throw('You can not yield this type!')
|
||||
}
|
||||
}
|
||||
handleTransactions(tGen.next());
|
||||
handleTransactions(tGen.next())
|
||||
|
||||
}
|
||||
requestTransaction (makeGen : Function) {
|
||||
this.transactionQueue.queue.push(makeGen);
|
||||
requestTransaction (makeGen) {
|
||||
this.transactionQueue.queue.push(makeGen)
|
||||
if (this.transactionQueue.onRequest != null) {
|
||||
this.transactionQueue.onRequest();
|
||||
this.transactionQueue.onRequest()
|
||||
}
|
||||
}
|
||||
*removeDatabase () {
|
||||
this.db.close();
|
||||
yield indexedDB.deleteDatabase(this.namespace);
|
||||
* removeDatabase () {
|
||||
this.db.close()
|
||||
yield window.indexedDB.deleteDatabase(this.namespace)
|
||||
}
|
||||
}
|
||||
return OperationStore;
|
||||
})();
|
||||
return OperationStore
|
||||
})()
|
||||
|
@ -1,117 +1,117 @@
|
||||
/* @flow */
|
||||
/*eslint-env browser,jasmine */
|
||||
/* global Y */
|
||||
/* eslint-env browser,jasmine */
|
||||
|
||||
if(typeof window !== "undefined"){
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
|
||||
describe("IndexedDB", function() {
|
||||
var ob;
|
||||
beforeAll(function(){
|
||||
ob = new Y.IndexedDB(null, {namespace: "Test"});
|
||||
});
|
||||
if (typeof window !== 'undefined') {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000
|
||||
describe('IndexedDB', function () {
|
||||
var ob
|
||||
beforeAll(function () {
|
||||
ob = new Y.IndexedDB(null, {namespace: 'Test'})
|
||||
})
|
||||
|
||||
it("can add and get operation", function(done) {
|
||||
ob.requestTransaction(function*(){
|
||||
it('can add and get operation', function (done) {
|
||||
ob.requestTransaction(function *() {
|
||||
var op = yield* this.setOperation({
|
||||
"id": ["1", 0],
|
||||
"stuff": true
|
||||
});
|
||||
expect(yield* this.getOperation(["1", 0]))
|
||||
.toEqual(op);
|
||||
done();
|
||||
});
|
||||
});
|
||||
'id': ['1', 0],
|
||||
'stuff': true
|
||||
})
|
||||
expect(yield* this.getOperation(['1', 0]))
|
||||
.toEqual(op)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it("can remove operation", function(done) {
|
||||
ob.requestTransaction(function*(){
|
||||
it('can remove operation', function (done) {
|
||||
ob.requestTransaction(function *() {
|
||||
var op = yield* this.setOperation({
|
||||
"id": ["1", 0],
|
||||
"stuff": true
|
||||
});
|
||||
expect(yield* this.getOperation(["1", 0]))
|
||||
.toEqual(op);
|
||||
yield* this.removeOperation(["1", 0]);
|
||||
expect(yield* this.getOperation(["1", 0]))
|
||||
.toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
'id': ['1', 0],
|
||||
'stuff': true
|
||||
})
|
||||
expect(yield* this.getOperation(['1', 0]))
|
||||
.toEqual(op)
|
||||
yield* this.removeOperation(['1', 0])
|
||||
expect(yield* this.getOperation(['1', 0]))
|
||||
.toBeUndefined()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it("getOperation(op) returns undefined if op does not exist", function(done){
|
||||
ob.requestTransaction(function*(){
|
||||
var op = yield* this.getOperation("plzDon'tBeThere");
|
||||
expect(op).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('getOperation(op) returns undefined if op does not exist', function (done) {
|
||||
ob.requestTransaction(function *() {
|
||||
var op = yield* this.getOperation("plzDon'tBeThere")
|
||||
expect(op).toBeUndefined()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it("yield throws if request is unknown", function(done){
|
||||
ob.requestTransaction(function*(){
|
||||
it('yield throws if request is unknown', function (done) {
|
||||
ob.requestTransaction(function *() {
|
||||
try {
|
||||
yield* this.setOperation();
|
||||
yield* this.setOperation()
|
||||
} catch (e) {
|
||||
expect(true).toEqual(true);
|
||||
done();
|
||||
return;
|
||||
expect(true).toEqual(true)
|
||||
done()
|
||||
return
|
||||
}
|
||||
expect("Expected an Error!").toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
expect('Expected an Error!').toEqual(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it("sets and gets stateVector", function(done){
|
||||
ob.requestTransaction(function*(){
|
||||
var s1 = {user: "1", clock: 1};
|
||||
var s2 = {user: "2", clock: 3};
|
||||
yield* this.setState(s1);
|
||||
yield* this.setState(s2);
|
||||
var sv = yield* this.getStateVector();
|
||||
expect(sv).not.toBeUndefined();
|
||||
expect(sv).toEqual([s1, s2]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('sets and gets stateVector', function (done) {
|
||||
ob.requestTransaction(function *() {
|
||||
var s1 = {user: '1', clock: 1}
|
||||
var s2 = {user: '2', clock: 3}
|
||||
yield* this.setState(s1)
|
||||
yield* this.setState(s2)
|
||||
var sv = yield* this.getStateVector()
|
||||
expect(sv).not.toBeUndefined()
|
||||
expect(sv).toEqual([s1, s2])
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it("gets stateSet", function(done){
|
||||
ob.requestTransaction(function*(){
|
||||
var s1 = {user: "1", clock: 1};
|
||||
var s2 = {user: "2", clock: 3};
|
||||
yield* this.setState(s1);
|
||||
yield* this.setState(s2);
|
||||
var sv = yield* this.getStateSet();
|
||||
expect(sv).not.toBeUndefined();
|
||||
it('gets stateSet', function (done) {
|
||||
ob.requestTransaction(function *() {
|
||||
var s1 = {user: '1', clock: 1}
|
||||
var s2 = {user: '2', clock: 3}
|
||||
yield* this.setState(s1)
|
||||
yield* this.setState(s2)
|
||||
var sv = yield* this.getStateSet()
|
||||
expect(sv).not.toBeUndefined()
|
||||
expect(sv).toEqual({
|
||||
"1": 1,
|
||||
"2": 3
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
'1': 1,
|
||||
'2': 3
|
||||
})
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it("getOperations returns operations (no parameters)", function(done){
|
||||
ob.requestTransaction(function*(){
|
||||
var s1 = {user: "1", clock: 55};
|
||||
yield* this.setState(s1);
|
||||
it('getOperations returns operations (no parameters)', function (done) {
|
||||
ob.requestTransaction(function *() {
|
||||
var s1 = {user: '1', clock: 55}
|
||||
yield* this.setState(s1)
|
||||
var op1 = yield* this.setOperation({
|
||||
"id": ["1", 0],
|
||||
"stuff": true
|
||||
});
|
||||
'id': ['1', 0],
|
||||
'stuff': true
|
||||
})
|
||||
var op2 = yield* this.setOperation({
|
||||
"id": ["1", 3],
|
||||
"stuff": true
|
||||
});
|
||||
var ops = yield* this.getOperations();
|
||||
expect(ops.length).toBeGreaterThan(1);
|
||||
expect(ops[0]).toEqual(op1);
|
||||
expect(ops[1]).toEqual(op2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
afterAll(function(done){
|
||||
ob.requestTransaction(function*(){
|
||||
yield* ob.removeDatabase();
|
||||
ob = null;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
'id': ['1', 3],
|
||||
'stuff': true
|
||||
})
|
||||
var ops = yield* this.getOperations()
|
||||
expect(ops.length).toBeGreaterThan(1)
|
||||
expect(ops[0]).toEqual(op1)
|
||||
expect(ops[1]).toEqual(op2)
|
||||
done()
|
||||
})
|
||||
})
|
||||
afterAll(function (done) {
|
||||
ob.requestTransaction(function *() {
|
||||
yield* ob.removeDatabase()
|
||||
ob = null
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1,155 +1,144 @@
|
||||
|
||||
type State = {
|
||||
user: string,
|
||||
clock: number
|
||||
};
|
||||
|
||||
/* global Struct, RBTree, Y */
|
||||
|
||||
function copyObject (o) {
|
||||
var c = {};
|
||||
var c = {}
|
||||
for (var key in o) {
|
||||
c[key] = o[key];
|
||||
c[key] = o[key]
|
||||
}
|
||||
return c;
|
||||
return c
|
||||
}
|
||||
|
||||
type StateVector = Array<State>;
|
||||
type StateSet = Object;
|
||||
Y.Memory = (function () { // eslint-disable-line no-unused-vars
|
||||
class Transaction extends AbstractTransaction { // eslint-disable-line
|
||||
|
||||
Y.Memory = (function(){ //eslint-disable-line no-unused-vars
|
||||
class Transaction extends AbstractTransaction { //eslint-disable-line
|
||||
ss: StateSet;
|
||||
os: RBTree;
|
||||
store: OperationStore;
|
||||
|
||||
constructor (store : OperationStore) {
|
||||
super(store);
|
||||
this.ss = store.ss;
|
||||
this.os = store.os;
|
||||
constructor (store) {
|
||||
super(store)
|
||||
this.ss = store.ss
|
||||
this.os = store.os
|
||||
}
|
||||
*setOperation (op) {
|
||||
* setOperation (op) { // eslint-disable-line
|
||||
// TODO: you can remove this step! probs..
|
||||
var n = this.os.findNode(op.id);
|
||||
n.val = op;
|
||||
return op;
|
||||
var n = this.os.findNode(op.id)
|
||||
n.val = op
|
||||
return op
|
||||
}
|
||||
*addOperation (op) {
|
||||
this.os.add(op);
|
||||
* addOperation (op) { // eslint-disable-line
|
||||
this.os.add(op)
|
||||
}
|
||||
*getOperation (id) {
|
||||
* getOperation (id) { // eslint-disable-line
|
||||
if (id == null) {
|
||||
throw new Error("You must define id!");
|
||||
throw new Error('You must define id!')
|
||||
}
|
||||
return this.os.find(id);
|
||||
return this.os.find(id)
|
||||
}
|
||||
*removeOperation (id) {
|
||||
this.os.delete(id);
|
||||
* removeOperation (id) { // eslint-disable-line
|
||||
this.os.delete(id)
|
||||
}
|
||||
*setState (state : State) : State {
|
||||
this.ss[state.user] = state.clock;
|
||||
* setState (state) { // eslint-disable-line
|
||||
this.ss[state.user] = state.clock
|
||||
}
|
||||
*getState (user : string) : State {
|
||||
var clock = this.ss[user];
|
||||
if (clock == null){
|
||||
clock = 0;
|
||||
* getState (user) { // eslint-disable-line
|
||||
var clock = this.ss[user]
|
||||
if (clock == null) {
|
||||
clock = 0
|
||||
}
|
||||
return {
|
||||
user: user,
|
||||
clock: clock
|
||||
};
|
||||
}
|
||||
}
|
||||
*getStateVector () : StateVector {
|
||||
var stateVector = [];
|
||||
* getStateVector () { // eslint-disable-line
|
||||
var stateVector = []
|
||||
for (var user in this.ss) {
|
||||
var clock = this.ss[user];
|
||||
var clock = this.ss[user]
|
||||
stateVector.push({
|
||||
user: user,
|
||||
clock: clock
|
||||
});
|
||||
})
|
||||
}
|
||||
return stateVector;
|
||||
return stateVector
|
||||
}
|
||||
*getStateSet () : StateSet {
|
||||
return this.ss;
|
||||
* getStateSet () { // eslint-disable-line
|
||||
return this.ss
|
||||
}
|
||||
*getOperations (startSS : StateSet) {
|
||||
* getOperations (startSS) {
|
||||
// TODO: use bounds here!
|
||||
if (startSS == null){
|
||||
startSS = {};
|
||||
if (startSS == null) {
|
||||
startSS = {}
|
||||
}
|
||||
var ops = [];
|
||||
var ops = []
|
||||
|
||||
var endSV : StateVector = yield* this.getStateVector();
|
||||
var endSV = yield* this.getStateVector()
|
||||
for (var endState of endSV) {
|
||||
var user = endState.user;
|
||||
if (user === "_") {
|
||||
continue;
|
||||
var user = endState.user
|
||||
if (user === '_') {
|
||||
continue
|
||||
}
|
||||
var startPos = startSS[user] || 0;
|
||||
var endPos = endState.clock;
|
||||
var startPos = startSS[user] || 0
|
||||
var endPos = endState.clock
|
||||
|
||||
this.os.iterate([user, startPos], [user, endPos], function(op){//eslint-disable-line
|
||||
ops.push(Struct[op.struct].encode(op));
|
||||
});
|
||||
this.os.iterate([user, startPos], [user, endPos], function (op) {// eslint-disable-line
|
||||
ops.push(Struct[op.struct].encode(op))
|
||||
})
|
||||
}
|
||||
var res = [];
|
||||
var res = []
|
||||
for (var op of ops) {
|
||||
res.push(yield* this.makeOperationReady.call(this, startSS, op));
|
||||
res.push(yield* this.makeOperationReady(startSS, op))
|
||||
}
|
||||
return res;
|
||||
return res
|
||||
}
|
||||
*makeOperationReady (ss, op) {
|
||||
* makeOperationReady (ss, op) {
|
||||
// instead of ss, you could use currSS (a ss that increments when you add an operation)
|
||||
var clock;
|
||||
var o = op;
|
||||
while (o.right != null){
|
||||
var clock
|
||||
var o = op
|
||||
while (o.right != null) {
|
||||
// while unknown, go to the right
|
||||
clock = ss[o.right[0]];
|
||||
clock = ss[o.right[0]]
|
||||
if (clock != null && o.right[1] < clock) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
o = yield* this.getOperation(o.right);
|
||||
o = yield* this.getOperation(o.right)
|
||||
}
|
||||
op = copyObject(op);
|
||||
op.right = o.right;
|
||||
return op;
|
||||
op = copyObject(op)
|
||||
op.right = o.right
|
||||
return op
|
||||
}
|
||||
}
|
||||
class OperationStore extends AbstractOperationStore { //eslint-disable-line no-undef
|
||||
class OperationStore extends AbstractOperationStore { // eslint-disable-line no-undef
|
||||
constructor (y) {
|
||||
super(y);
|
||||
this.os = new RBTree();
|
||||
this.ss = {};
|
||||
this.waitingTransactions = [];
|
||||
this.transactionInProgress = false;
|
||||
super(y)
|
||||
this.os = new RBTree()
|
||||
this.ss = {}
|
||||
this.waitingTransactions = []
|
||||
this.transactionInProgress = false
|
||||
}
|
||||
requestTransaction (_makeGen : Function) {
|
||||
requestTransaction (_makeGen) {
|
||||
if (!this.transactionInProgress) {
|
||||
this.transactionInProgress = true;
|
||||
this.transactionInProgress = true
|
||||
setTimeout(() => {
|
||||
var makeGen = _makeGen;
|
||||
var makeGen = _makeGen
|
||||
while (makeGen != null) {
|
||||
var t = new Transaction(this);
|
||||
var gen = makeGen.call(t);
|
||||
var res = gen.next();
|
||||
while(!res.done){
|
||||
if (res.value === "transaction") {
|
||||
res = gen.next(t);
|
||||
var t = new Transaction(this)
|
||||
var gen = makeGen.call(t)
|
||||
var res = gen.next()
|
||||
while (!res.done) {
|
||||
if (res.value === 'transaction') {
|
||||
res = gen.next(t)
|
||||
} else {
|
||||
throw new Error("You must not yield this type. (Maybe you meant to use 'yield*'?)");
|
||||
throw new Error("You must not yield this type. (Maybe you meant to use 'yield*'?)")
|
||||
}
|
||||
}
|
||||
makeGen = this.waitingTransactions.shift();
|
||||
makeGen = this.waitingTransactions.shift()
|
||||
}
|
||||
this.transactionInProgress = false;
|
||||
}, 0);
|
||||
this.transactionInProgress = false
|
||||
}, 0)
|
||||
} else {
|
||||
this.waitingTransactions.push(_makeGen);
|
||||
this.waitingTransactions.push(_makeGen)
|
||||
}
|
||||
}
|
||||
*removeDatabase () {
|
||||
delete this.os;
|
||||
* removeDatabase () { // eslint-disable-line
|
||||
delete this.os
|
||||
}
|
||||
}
|
||||
return OperationStore;
|
||||
})();
|
||||
return OperationStore
|
||||
})()
|
||||
|
@ -1,367 +1,367 @@
|
||||
|
||||
/* global compareIds */
|
||||
function smaller (a, b) {
|
||||
return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]);
|
||||
return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1])
|
||||
}
|
||||
|
||||
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;
|
||||
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!");
|
||||
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; }
|
||||
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;
|
||||
return this.parent.parent
|
||||
}
|
||||
get parent () {
|
||||
return this._parent;
|
||||
return this._parent
|
||||
}
|
||||
get sibling () {
|
||||
return (this === this.parent.left) ?
|
||||
this.parent.right : this.parent.left;
|
||||
this.parent.right : this.parent.left
|
||||
}
|
||||
get left () {
|
||||
return this._left;
|
||||
return this._left
|
||||
}
|
||||
get right () {
|
||||
return this._right;
|
||||
return this._right
|
||||
}
|
||||
set left (n) {
|
||||
if (n !== null) {
|
||||
n._parent = this;
|
||||
n._parent = this
|
||||
}
|
||||
this._left = n;
|
||||
this._left = n
|
||||
}
|
||||
set right (n) {
|
||||
if (n !== null) {
|
||||
n._parent = this;
|
||||
n._parent = this
|
||||
}
|
||||
this._right = n;
|
||||
this._right = n
|
||||
}
|
||||
rotateLeft (tree) {
|
||||
var parent = this.parent;
|
||||
var newParent = this.right;
|
||||
var newRight = this.right.left;
|
||||
newParent.left = this;
|
||||
this.right = newRight;
|
||||
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;
|
||||
tree.root = newParent
|
||||
newParent._parent = null
|
||||
} else if (parent.left === this) {
|
||||
parent.left = newParent;
|
||||
parent.left = newParent
|
||||
} else if (parent.right === this) {
|
||||
parent.right = newParent;
|
||||
parent.right = newParent
|
||||
} else {
|
||||
throw new Error("The elements are wrongly connected!");
|
||||
throw new Error('The elements are wrongly connected!')
|
||||
}
|
||||
}
|
||||
next () {
|
||||
if ( this.right !== null ) {
|
||||
if (this.right !== null) {
|
||||
// search the most left node in the right tree
|
||||
var o = this.right;
|
||||
var o = this.right
|
||||
while (o.left !== null) {
|
||||
o = o.left;
|
||||
o = o.left
|
||||
}
|
||||
return o;
|
||||
return o
|
||||
} else {
|
||||
var p = this;
|
||||
var p = this
|
||||
while (p.parent !== null && p !== p.parent.left) {
|
||||
p = p.parent;
|
||||
p = p.parent
|
||||
}
|
||||
return 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;
|
||||
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;
|
||||
tree.root = newParent
|
||||
newParent._parent = null
|
||||
} else if (parent.left === this) {
|
||||
parent.left = newParent;
|
||||
parent.left = newParent
|
||||
} else if (parent.right === this) {
|
||||
parent.right = newParent;
|
||||
parent.right = newParent
|
||||
} else {
|
||||
throw new Error("The elements are wrongly connected!");
|
||||
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;
|
||||
return this.parent.parent.right
|
||||
} else {
|
||||
return this.parent.parent.left;
|
||||
return this.parent.parent.left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RBTree { //eslint-disable-line no-unused-vars
|
||||
class RBTree { // eslint-disable-line no-unused-vars
|
||||
constructor () {
|
||||
this.root = null;
|
||||
this.length = 0;
|
||||
this.root = null
|
||||
this.length = 0
|
||||
}
|
||||
findNodeWithLowerBound (from) {
|
||||
if (from === void 0) {
|
||||
throw new Error("You must define from!");
|
||||
throw new Error('You must define from!')
|
||||
}
|
||||
var o = this.root;
|
||||
var o = this.root
|
||||
if (o === null) {
|
||||
return false;
|
||||
return false
|
||||
} else {
|
||||
while (true) {
|
||||
if ((from === null || smaller(from, o.val.id)) && o.left !== null) {
|
||||
// o is included in the bound
|
||||
// try to find an element that is closer to the bound
|
||||
o = o.left;
|
||||
o = o.left
|
||||
} else if (from !== null && 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;
|
||||
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.next()
|
||||
}
|
||||
} else {
|
||||
return o;
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
iterate (from, to, f) {
|
||||
var o = this.findNodeWithLowerBound(from);
|
||||
while ( o !== null && (to === null || smaller(o.val.id, to) || compareIds(o.val.id, to)) ) {
|
||||
f(o.val);
|
||||
o = o.next();
|
||||
var o = this.findNodeWithLowerBound(from)
|
||||
while (o !== null && (to === null || smaller(o.val.id, to) || compareIds(o.val.id, to))) {
|
||||
f(o.val)
|
||||
o = o.next()
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
find (id) {
|
||||
return this.findNode(id).val;
|
||||
return this.findNode(id).val
|
||||
}
|
||||
findNode (id) {
|
||||
if (id == null || id.constructor !== Array) {
|
||||
throw new Error("Expect id to be an array!");
|
||||
throw new Error('Expect id to be an array!')
|
||||
}
|
||||
var o = this.root;
|
||||
var o = this.root
|
||||
if (o === null) {
|
||||
return false;
|
||||
return false
|
||||
} else {
|
||||
while (true) {
|
||||
if (o === null) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (smaller(id, o.val.id)) {
|
||||
o = o.left;
|
||||
o = o.left
|
||||
} else if (smaller(o.val.id, id)) {
|
||||
o = o.right;
|
||||
o = o.right
|
||||
} else {
|
||||
return o;
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete (id) {
|
||||
if (id == null || id.constructor !== Array) {
|
||||
throw new Error("id is expected to be an Array!");
|
||||
throw new Error('id is expected to be an Array!')
|
||||
}
|
||||
var d = this.findNode(id);
|
||||
var d = this.findNode(id)
|
||||
if (d == null) {
|
||||
throw new Error("Element does not exist!");
|
||||
throw new Error('Element does not exist!')
|
||||
}
|
||||
this.length--;
|
||||
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;
|
||||
var o = d.left
|
||||
// find
|
||||
while (o.right !== null) {
|
||||
o = o.right;
|
||||
o = o.right
|
||||
}
|
||||
// switch
|
||||
d.val = o.val;
|
||||
d = o;
|
||||
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;
|
||||
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;
|
||||
isFakeChild = false
|
||||
}
|
||||
|
||||
if (d.parent === null) {
|
||||
if (!isFakeChild) {
|
||||
this.root = child;
|
||||
child.blacken();
|
||||
child._parent = null;
|
||||
this.root = child
|
||||
child.blacken()
|
||||
child._parent = null
|
||||
} else {
|
||||
this.root = null;
|
||||
this.root = null
|
||||
}
|
||||
return;
|
||||
return
|
||||
} else if (d.parent.left === d) {
|
||||
d.parent.left = child;
|
||||
d.parent.left = child
|
||||
} else if (d.parent.right === d) {
|
||||
d.parent.right = child;
|
||||
d.parent.right = child
|
||||
} else {
|
||||
throw new Error("Impossible!");
|
||||
throw new Error('Impossible!')
|
||||
}
|
||||
if ( d.isBlack() ) {
|
||||
if ( child.isRed() ) {
|
||||
child.blacken();
|
||||
if (d.isBlack()) {
|
||||
if (child.isRed()) {
|
||||
child.blacken()
|
||||
} else {
|
||||
this._fixDelete(child);
|
||||
this._fixDelete(child)
|
||||
}
|
||||
}
|
||||
this.root.blacken();
|
||||
this.root.blacken()
|
||||
if (isFakeChild) {
|
||||
if (child.parent.left === child) {
|
||||
child.parent.left = null;
|
||||
child.parent.left = null
|
||||
} else if (child.parent.right === child) {
|
||||
child.parent.right = null;
|
||||
child.parent.right = null
|
||||
} else {
|
||||
throw new Error("Impossible #3");
|
||||
throw new Error('Impossible #3')
|
||||
}
|
||||
}
|
||||
}
|
||||
_fixDelete (n) {
|
||||
function isBlack (node) {
|
||||
return node !== null ? node.isBlack() : true;
|
||||
return node !== null ? node.isBlack() : true
|
||||
}
|
||||
function isRed(node) {
|
||||
return node !== null ? node.isRed() : false;
|
||||
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;
|
||||
return
|
||||
}
|
||||
// d was already replaced by the child
|
||||
// d is not the root
|
||||
// d and child are black
|
||||
var sibling = n.sibling;
|
||||
var sibling = n.sibling
|
||||
if (isRed(sibling)) {
|
||||
// make sibling the grandfather
|
||||
n.parent.redden();
|
||||
sibling.blacken();
|
||||
n.parent.redden()
|
||||
sibling.blacken()
|
||||
if (n === n.parent.left) {
|
||||
n.parent.rotateLeft(this);
|
||||
n.parent.rotateLeft(this)
|
||||
} else if (n === n.parent.right) {
|
||||
n.parent.rotateRight(this);
|
||||
n.parent.rotateRight(this)
|
||||
} else {
|
||||
throw new Error("Impossible #2");
|
||||
throw new Error('Impossible #2')
|
||||
}
|
||||
sibling = n.sibling;
|
||||
sibling = n.sibling
|
||||
}
|
||||
// parent, sibling, and children of n are black
|
||||
if ( n.parent.isBlack() &&
|
||||
sibling.isBlack() &&
|
||||
isBlack(sibling.left) &&
|
||||
isBlack(sibling.right)
|
||||
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()
|
||||
this._fixDelete(n.parent)
|
||||
} else if (n.parent.isRed() &&
|
||||
sibling.isBlack() &&
|
||||
isBlack(sibling.left) &&
|
||||
isBlack(sibling.right)
|
||||
) {
|
||||
sibling.redden();
|
||||
n.parent.blacken();
|
||||
sibling.redden()
|
||||
n.parent.blacken()
|
||||
} else {
|
||||
if ( n === n.parent.left &&
|
||||
sibling.isBlack() &&
|
||||
isRed(sibling.left) &&
|
||||
isBlack(sibling.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.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.redden()
|
||||
sibling.right.blacken()
|
||||
sibling.rotateLeft(this)
|
||||
sibling = n.sibling
|
||||
}
|
||||
sibling.color = n.parent.color;
|
||||
n.parent.blacken();
|
||||
sibling.color = n.parent.color
|
||||
n.parent.blacken()
|
||||
if (n === n.parent.left) {
|
||||
sibling.right.blacken();
|
||||
n.parent.rotateLeft(this);
|
||||
sibling.right.blacken()
|
||||
n.parent.rotateLeft(this)
|
||||
} else {
|
||||
sibling.left.blacken();
|
||||
n.parent.rotateRight(this);
|
||||
sibling.left.blacken()
|
||||
n.parent.rotateRight(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
add (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!");
|
||||
throw new Error('v is expected to have an id property which is an Array!')
|
||||
}
|
||||
var node = new N(v);
|
||||
var node = new N(v)
|
||||
if (this.root !== null) {
|
||||
var p = this.root; // p abbrev. parent
|
||||
var p = this.root // p abbrev. parent
|
||||
while (true) {
|
||||
if (smaller(node.val.id, p.val.id)) {
|
||||
if (p.left === null) {
|
||||
p.left = node;
|
||||
break;
|
||||
p.left = node
|
||||
break
|
||||
} else {
|
||||
p = p.left;
|
||||
p = p.left
|
||||
}
|
||||
} else if (smaller(p.val.id, node.val.id)) {
|
||||
if (p.right === null) {
|
||||
p.right = node;
|
||||
break;
|
||||
p.right = node
|
||||
break
|
||||
} else {
|
||||
p = p.right;
|
||||
p = p.right
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
this._fixInsert(node);
|
||||
this._fixInsert(node)
|
||||
} else {
|
||||
this.root = node;
|
||||
this.root = node
|
||||
}
|
||||
this.length++;
|
||||
this.root.blacken();
|
||||
this.length++
|
||||
this.root.blacken()
|
||||
}
|
||||
_fixInsert (n) {
|
||||
if (n.parent === null) {
|
||||
n.blacken();
|
||||
return;
|
||||
n.blacken()
|
||||
return
|
||||
} else if (n.parent.isBlack()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
var uncle = n.getUncle();
|
||||
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);
|
||||
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
|
||||
@ -370,30 +370,28 @@ class RBTree { //eslint-disable-line no-unused-vars
|
||||
// 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;
|
||||
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();
|
||||
n.parent.blacken()
|
||||
n.grandparent.redden()
|
||||
if (n === n.parent.left) {
|
||||
// Case 1
|
||||
n.grandparent.rotateRight(this);
|
||||
n.grandparent.rotateRight(this)
|
||||
} else {
|
||||
// Case 2
|
||||
n.grandparent.rotateLeft(this);
|
||||
n.grandparent.rotateLeft(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,209 +1,209 @@
|
||||
/* @flow */
|
||||
/*eslint-env browser,jasmine,console */
|
||||
/* global RBTree, smaller, compareIds */
|
||||
/* eslint-env browser,jasmine,console */
|
||||
|
||||
var numberOfRBTreeTests = 1000;
|
||||
var numberOfRBTreeTests = 1000
|
||||
|
||||
function itRedNodesDoNotHaveBlackChildren (tree) {
|
||||
it("Red nodes do not have black children", function(){
|
||||
it('Red nodes do not have black children', function () {
|
||||
function traverse (n) {
|
||||
if (n == null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (n.isRed()) {
|
||||
if (n.left != null) {
|
||||
expect(n.left.isRed()).not.toBeTruthy();
|
||||
expect(n.left.isRed()).not.toBeTruthy()
|
||||
}
|
||||
if (n.right != null) {
|
||||
expect(n.right.isRed()).not.toBeTruthy();
|
||||
expect(n.right.isRed()).not.toBeTruthy()
|
||||
}
|
||||
}
|
||||
traverse(n.left);
|
||||
traverse(n.right);
|
||||
traverse(n.left)
|
||||
traverse(n.right)
|
||||
}
|
||||
traverse(tree.root);
|
||||
});
|
||||
traverse(tree.root)
|
||||
})
|
||||
}
|
||||
|
||||
function itBlackHeightOfSubTreesAreEqual (tree){
|
||||
it("Black-height of sub-trees are equal", function(){
|
||||
function itBlackHeightOfSubTreesAreEqual (tree) {
|
||||
it('Black-height of sub-trees are equal', function () {
|
||||
function traverse (n) {
|
||||
if (n == null) {
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
var sub1 = traverse(n.left);
|
||||
var sub2 = traverse(n.right);
|
||||
expect(sub1).toEqual(sub2);
|
||||
if(n.isRed()) {
|
||||
return sub1;
|
||||
var sub1 = traverse(n.left)
|
||||
var sub2 = traverse(n.right)
|
||||
expect(sub1).toEqual(sub2)
|
||||
if (n.isRed()) {
|
||||
return sub1
|
||||
} else {
|
||||
return sub1 + 1;
|
||||
return sub1 + 1
|
||||
}
|
||||
}
|
||||
traverse(tree.root);
|
||||
});
|
||||
traverse(tree.root)
|
||||
})
|
||||
}
|
||||
|
||||
function itRootNodeIsBlack(tree) {
|
||||
it("root node is black", function(){
|
||||
expect(tree.root == null || tree.root.isBlack()).toBeTruthy();
|
||||
});
|
||||
function itRootNodeIsBlack (tree) {
|
||||
it('root node is black', function () {
|
||||
expect(tree.root == null || tree.root.isBlack()).toBeTruthy()
|
||||
})
|
||||
}
|
||||
|
||||
describe("RedBlack Tree", function(){
|
||||
beforeEach(function(){
|
||||
this.tree = new RBTree();
|
||||
});
|
||||
it("can add&retrieve 5 elements", function(){
|
||||
this.tree.add({val: "four", id: [4]});
|
||||
this.tree.add({val: "one", id: [1]});
|
||||
this.tree.add({val: "three", id: [3]});
|
||||
this.tree.add({val: "two", id: [2]});
|
||||
this.tree.add({val: "five", id: [5]});
|
||||
expect(this.tree.find([1]).val).toEqual("one");
|
||||
expect(this.tree.find([2]).val).toEqual("two");
|
||||
expect(this.tree.find([3]).val).toEqual("three");
|
||||
expect(this.tree.find([4]).val).toEqual("four");
|
||||
expect(this.tree.find([5]).val).toEqual("five");
|
||||
});
|
||||
describe('RedBlack Tree', function () {
|
||||
beforeEach(function () {
|
||||
this.tree = new RBTree()
|
||||
})
|
||||
it('can add&retrieve 5 elements', function () {
|
||||
this.tree.add({val: 'four', id: [4]})
|
||||
this.tree.add({val: 'one', id: [1]})
|
||||
this.tree.add({val: 'three', id: [3]})
|
||||
this.tree.add({val: 'two', id: [2]})
|
||||
this.tree.add({val: 'five', id: [5]})
|
||||
expect(this.tree.find([1]).val).toEqual('one')
|
||||
expect(this.tree.find([2]).val).toEqual('two')
|
||||
expect(this.tree.find([3]).val).toEqual('three')
|
||||
expect(this.tree.find([4]).val).toEqual('four')
|
||||
expect(this.tree.find([5]).val).toEqual('five')
|
||||
})
|
||||
|
||||
it("5 elements do not exist anymore after deleting them", function(){
|
||||
this.tree.add({val: "four", id: [4]});
|
||||
this.tree.add({val: "one", id: [1]});
|
||||
this.tree.add({val: "three", id: [3]});
|
||||
this.tree.add({val: "two", id: [2]});
|
||||
this.tree.add({val: "five", id: [5]});
|
||||
this.tree.delete([4]);
|
||||
expect(this.tree.find([4])).not.toBeTruthy();
|
||||
this.tree.delete([3]);
|
||||
expect(this.tree.find([3])).not.toBeTruthy();
|
||||
this.tree.delete([2]);
|
||||
expect(this.tree.find([2])).not.toBeTruthy();
|
||||
this.tree.delete([1]);
|
||||
expect(this.tree.find([1])).not.toBeTruthy();
|
||||
this.tree.delete([5]);
|
||||
expect(this.tree.find([5])).not.toBeTruthy();
|
||||
});
|
||||
it('5 elements do not exist anymore after deleting them', function () {
|
||||
this.tree.add({val: 'four', id: [4]})
|
||||
this.tree.add({val: 'one', id: [1]})
|
||||
this.tree.add({val: 'three', id: [3]})
|
||||
this.tree.add({val: 'two', id: [2]})
|
||||
this.tree.add({val: 'five', id: [5]})
|
||||
this.tree.delete([4])
|
||||
expect(this.tree.find([4])).not.toBeTruthy()
|
||||
this.tree.delete([3])
|
||||
expect(this.tree.find([3])).not.toBeTruthy()
|
||||
this.tree.delete([2])
|
||||
expect(this.tree.find([2])).not.toBeTruthy()
|
||||
this.tree.delete([1])
|
||||
expect(this.tree.find([1])).not.toBeTruthy()
|
||||
this.tree.delete([5])
|
||||
expect(this.tree.find([5])).not.toBeTruthy()
|
||||
})
|
||||
|
||||
it("debug #1", function(){
|
||||
this.tree.add({id: [2]});
|
||||
this.tree.add({id: [0]});
|
||||
this.tree.delete([2]);
|
||||
this.tree.add({id: [1]});
|
||||
expect(this.tree.find([0])).not.toBeUndefined();
|
||||
expect(this.tree.find([1])).not.toBeUndefined();
|
||||
expect(this.tree.find([2])).toBeUndefined();
|
||||
});
|
||||
describe("debug #2", function(){
|
||||
var tree = new RBTree();
|
||||
tree.add({id: [8433]});
|
||||
tree.add({id: [12844]});
|
||||
tree.add({id: [1795]});
|
||||
tree.add({id: [30302]});
|
||||
tree.add({id: [64287]});
|
||||
tree.delete([8433]);
|
||||
tree.add({id: [28996]});
|
||||
tree.delete([64287]);
|
||||
tree.add({id: [22721]});
|
||||
it('debug #1', function () {
|
||||
this.tree.add({id: [2]})
|
||||
this.tree.add({id: [0]})
|
||||
this.tree.delete([2])
|
||||
this.tree.add({id: [1]})
|
||||
expect(this.tree.find([0])).not.toBeUndefined()
|
||||
expect(this.tree.find([1])).not.toBeUndefined()
|
||||
expect(this.tree.find([2])).toBeUndefined()
|
||||
})
|
||||
describe('debug #2', function () {
|
||||
var tree = new RBTree()
|
||||
tree.add({id: [8433]})
|
||||
tree.add({id: [12844]})
|
||||
tree.add({id: [1795]})
|
||||
tree.add({id: [30302]})
|
||||
tree.add({id: [64287]})
|
||||
tree.delete([8433])
|
||||
tree.add({id: [28996]})
|
||||
tree.delete([64287])
|
||||
tree.add({id: [22721]})
|
||||
|
||||
itRootNodeIsBlack(tree, []);
|
||||
itBlackHeightOfSubTreesAreEqual(tree, []);
|
||||
});
|
||||
itRootNodeIsBlack(tree, [])
|
||||
itBlackHeightOfSubTreesAreEqual(tree, [])
|
||||
})
|
||||
|
||||
describe(`After adding&deleting (0.8/0.2) ${numberOfRBTreeTests} times`, function () {
|
||||
var elements = [];
|
||||
var tree = new RBTree();
|
||||
for(var i = 0; i < numberOfRBTreeTests; i++) {
|
||||
var r = Math.random();
|
||||
var elements = []
|
||||
var tree = new RBTree()
|
||||
for (var i = 0; i < numberOfRBTreeTests; i++) {
|
||||
var r = Math.random()
|
||||
if (r < 0.8) {
|
||||
var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)];
|
||||
elements.push(obj);
|
||||
tree.add({id: obj});
|
||||
var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)]
|
||||
elements.push(obj)
|
||||
tree.add({id: obj})
|
||||
} else if (elements.length > 0) {
|
||||
var elemid = Math.floor(Math.random() * elements.length);
|
||||
var elem = elements[elemid];
|
||||
elements = elements.filter(function(e){return !compareIds(e,elem); }); //eslint-disable-line
|
||||
tree.delete(elem);
|
||||
var elemid = Math.floor(Math.random() * elements.length)
|
||||
var elem = elements[elemid]
|
||||
elements = elements.filter(function (e) {return !compareIds(e, elem); }); // eslint-disable-line
|
||||
tree.delete(elem)
|
||||
}
|
||||
}
|
||||
itRootNodeIsBlack(tree);
|
||||
itRootNodeIsBlack(tree)
|
||||
|
||||
it("can find every object", function(){
|
||||
for(var id of elements) {
|
||||
expect(tree.find(id).id).toEqual(id);
|
||||
it('can find every object', function () {
|
||||
for (var id of elements) {
|
||||
expect(tree.find(id).id).toEqual(id)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it("can find every object with lower bound search", function(){
|
||||
for(var id of elements) {
|
||||
expect(tree.findNodeWithLowerBound(id).val.id).toEqual(id);
|
||||
it('can find every object with lower bound search', function () {
|
||||
for (var id of elements) {
|
||||
expect(tree.findNodeWithLowerBound(id).val.id).toEqual(id)
|
||||
}
|
||||
});
|
||||
itRedNodesDoNotHaveBlackChildren(tree);
|
||||
})
|
||||
itRedNodesDoNotHaveBlackChildren(tree)
|
||||
|
||||
itBlackHeightOfSubTreesAreEqual(tree);
|
||||
itBlackHeightOfSubTreesAreEqual(tree)
|
||||
|
||||
it("iterating over a tree with lower bound yields the right amount of results", function(){
|
||||
var lowerBound = elements[Math.floor(Math.random() * elements.length)];
|
||||
var expectedResults = elements.filter(function(e, pos){
|
||||
return (smaller(lowerBound, e) || compareIds(e, lowerBound)) && elements.indexOf(e) === pos;
|
||||
}).length;
|
||||
it('iterating over a tree with lower bound yields the right amount of results', function () {
|
||||
var lowerBound = elements[Math.floor(Math.random() * elements.length)]
|
||||
var expectedResults = elements.filter(function (e, pos) {
|
||||
return (smaller(lowerBound, e) || compareIds(e, lowerBound)) && elements.indexOf(e) === pos
|
||||
}).length
|
||||
|
||||
var actualResults = 0;
|
||||
tree.iterate(lowerBound, null, function(val){
|
||||
expect(val).not.toBeUndefined();
|
||||
actualResults++;
|
||||
});
|
||||
expect(expectedResults).toEqual(actualResults);
|
||||
});
|
||||
var actualResults = 0
|
||||
tree.iterate(lowerBound, null, function (val) {
|
||||
expect(val).not.toBeUndefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
})
|
||||
|
||||
it("iterating over a tree without bounds yield the right amount of results", function(){
|
||||
var lowerBound = null;
|
||||
var expectedResults = elements.filter(function(e, pos){
|
||||
return elements.indexOf(e) === pos;
|
||||
}).length;
|
||||
var actualResults = 0;
|
||||
tree.iterate(lowerBound, null, function(val){
|
||||
expect(val).not.toBeUndefined();
|
||||
actualResults++;
|
||||
});
|
||||
expect(expectedResults).toEqual(actualResults);
|
||||
});
|
||||
it('iterating over a tree without bounds yield the right amount of results', function () {
|
||||
var lowerBound = null
|
||||
var expectedResults = elements.filter(function (e, pos) {
|
||||
return elements.indexOf(e) === pos
|
||||
}).length
|
||||
var actualResults = 0
|
||||
tree.iterate(lowerBound, null, function (val) {
|
||||
expect(val).not.toBeUndefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
})
|
||||
|
||||
it("iterating over a tree with upper bound yields the right amount of results", function(){
|
||||
var upperBound = elements[Math.floor(Math.random() * elements.length)];
|
||||
var expectedResults = elements.filter(function(e, pos){
|
||||
return (smaller(e, upperBound) || compareIds(e, upperBound)) && elements.indexOf(e) === pos;
|
||||
}).length;
|
||||
it('iterating over a tree with upper bound yields the right amount of results', function () {
|
||||
var upperBound = elements[Math.floor(Math.random() * elements.length)]
|
||||
var expectedResults = elements.filter(function (e, pos) {
|
||||
return (smaller(e, upperBound) || compareIds(e, upperBound)) && elements.indexOf(e) === pos
|
||||
}).length
|
||||
|
||||
var actualResults = 0;
|
||||
tree.iterate(null, upperBound, function(val){
|
||||
expect(val).not.toBeUndefined();
|
||||
actualResults++;
|
||||
});
|
||||
expect(expectedResults).toEqual(actualResults);
|
||||
});
|
||||
var actualResults = 0
|
||||
tree.iterate(null, upperBound, function (val) {
|
||||
expect(val).not.toBeUndefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
})
|
||||
|
||||
it("iterating over a tree with upper and lower bounds yield the right amount of results", function(){
|
||||
var b1 = elements[Math.floor(Math.random() * elements.length)];
|
||||
var b2 = elements[Math.floor(Math.random() * elements.length)];
|
||||
var upperBound, lowerBound;
|
||||
it('iterating over a tree with upper and lower bounds yield the right amount of results', function () {
|
||||
var b1 = elements[Math.floor(Math.random() * elements.length)]
|
||||
var b2 = elements[Math.floor(Math.random() * elements.length)]
|
||||
var upperBound, lowerBound
|
||||
if (smaller(b1, b2)) {
|
||||
lowerBound = b1;
|
||||
upperBound = b2;
|
||||
lowerBound = b1
|
||||
upperBound = b2
|
||||
} else {
|
||||
lowerBound = b2;
|
||||
upperBound = b1;
|
||||
lowerBound = b2
|
||||
upperBound = b1
|
||||
}
|
||||
var expectedResults = elements.filter(function(e, pos){
|
||||
return (smaller(lowerBound, e) || compareIds(e, lowerBound))
|
||||
&& (smaller(e, upperBound) || compareIds(e, upperBound)) && elements.indexOf(e) === pos;
|
||||
}).length;
|
||||
var actualResults = 0;
|
||||
tree.iterate(lowerBound, upperBound, function(val){
|
||||
expect(val).not.toBeUndefined();
|
||||
actualResults++;
|
||||
});
|
||||
expect(expectedResults).toEqual(actualResults);
|
||||
});
|
||||
});
|
||||
});
|
||||
var expectedResults = elements.filter(function (e, pos) {
|
||||
return (smaller(lowerBound, e) || compareIds(e, lowerBound)) &&
|
||||
(smaller(e, upperBound) || compareIds(e, upperBound)) && elements.indexOf(e) === pos
|
||||
}).length
|
||||
var actualResults = 0
|
||||
tree.iterate(lowerBound, upperBound, function (val) {
|
||||
expect(val).not.toBeUndefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
270
src/Struct.js
270
src/Struct.js
@ -1,36 +1,16 @@
|
||||
/* @flow */
|
||||
|
||||
// Op is anything that we could get from the OperationStore.
|
||||
type Op = Object;
|
||||
type Id = [string, number];
|
||||
|
||||
type List = {
|
||||
id: Id,
|
||||
start: Insert,
|
||||
end: Insert
|
||||
};
|
||||
|
||||
type Insert = {
|
||||
id: Id,
|
||||
left: Insert,
|
||||
right: Insert,
|
||||
origin: Insert,
|
||||
parent: List,
|
||||
content: any
|
||||
};
|
||||
|
||||
function compareIds(id1, id2) {
|
||||
/* global copyObject, Y*/
|
||||
|
||||
function compareIds (id1, id2) {
|
||||
if (id1 == null || id2 == null) {
|
||||
if (id1 == null && id2 == null) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (id1[0] === id2[0] && id1[1] === id2[1]) {
|
||||
return true;
|
||||
return true
|
||||
} else {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,26 +22,26 @@ var Struct = {
|
||||
*/
|
||||
Delete: {
|
||||
encode: function (op) {
|
||||
return op;
|
||||
return op
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
return [op.target];
|
||||
return [op.target]
|
||||
},
|
||||
execute: function* (op) {
|
||||
var target = yield* this.getOperation(op.target);
|
||||
execute: function * (op) {
|
||||
var target = yield* this.getOperation(op.target)
|
||||
if (!target.deleted) {
|
||||
target.deleted = true;
|
||||
yield* this.setOperation(target);
|
||||
var t = this.store.initializedTypes[JSON.stringify(target.parent)];
|
||||
target.deleted = true
|
||||
yield* this.setOperation(target)
|
||||
var t = this.store.initializedTypes[JSON.stringify(target.parent)]
|
||||
if (t != null) {
|
||||
yield* t._changed(this, copyObject(op));
|
||||
yield* t._changed(this, copyObject(op))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
Insert: {
|
||||
/*{
|
||||
/* {
|
||||
content: any,
|
||||
left: Id,
|
||||
right: Id,
|
||||
@ -71,8 +51,9 @@ var Struct = {
|
||||
id: this.os.getNextOpId()
|
||||
}
|
||||
*/
|
||||
encode: function(op){
|
||||
/*var e = {
|
||||
encode: function (op) {
|
||||
/*
|
||||
var e = {
|
||||
id: op.id,
|
||||
left: op.left,
|
||||
right: op.right,
|
||||
@ -80,44 +61,44 @@ var Struct = {
|
||||
parent: op.parent,
|
||||
content: op.content,
|
||||
struct: "Insert"
|
||||
};
|
||||
}
|
||||
if (op.parentSub != null){
|
||||
e.parentSub = op.parentSub;
|
||||
e.parentSub = op.parentSub
|
||||
}
|
||||
return e;*/
|
||||
return op;
|
||||
return op
|
||||
},
|
||||
requiredOps: function(op){
|
||||
var ids = [];
|
||||
if(op.left != null){
|
||||
ids.push(op.left);
|
||||
requiredOps: function (op) {
|
||||
var ids = []
|
||||
if (op.left != null) {
|
||||
ids.push(op.left)
|
||||
}
|
||||
if(op.right != null){
|
||||
ids.push(op.right);
|
||||
if (op.right != null) {
|
||||
ids.push(op.right)
|
||||
}
|
||||
//if(op.right == null && op.left == null) {}
|
||||
ids.push(op.parent);
|
||||
// if(op.right == null && op.left == null) {}
|
||||
ids.push(op.parent)
|
||||
|
||||
if (op.opContent != null) {
|
||||
ids.push(op.opContent);
|
||||
ids.push(op.opContent)
|
||||
}
|
||||
return ids;
|
||||
return ids
|
||||
},
|
||||
getDistanceToOrigin: function *(op){
|
||||
getDistanceToOrigin: function *(op) {
|
||||
if (op.left == null) {
|
||||
return 0;
|
||||
return 0
|
||||
} else {
|
||||
var d = 0;
|
||||
var o = yield* this.getOperation(op.left);
|
||||
var d = 0
|
||||
var o = yield* this.getOperation(op.left)
|
||||
while (!compareIds(op.origin, (o ? o.id : null))) {
|
||||
d++;
|
||||
d++
|
||||
if (o.left == null) {
|
||||
break;
|
||||
break
|
||||
} else {
|
||||
o = yield* this.getOperation(o.left);
|
||||
o = yield* this.getOperation(o.left)
|
||||
}
|
||||
}
|
||||
return d;
|
||||
return d
|
||||
}
|
||||
},
|
||||
/*
|
||||
@ -135,86 +116,86 @@ var Struct = {
|
||||
# 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;
|
||||
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);
|
||||
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;
|
||||
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 && !compareIds(o.id, op.right)){
|
||||
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o);
|
||||
if (o != null && !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;
|
||||
op.left = o.id
|
||||
distanceToOrigin = i + 1
|
||||
}
|
||||
} else if (oOriginDistance < i) {
|
||||
// case 2
|
||||
if (i - distanceToOrigin <= oOriginDistance) {
|
||||
op.left = o.id;
|
||||
distanceToOrigin = i + 1;
|
||||
op.left = o.id
|
||||
distanceToOrigin = i + 1
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
break
|
||||
}
|
||||
i++;
|
||||
o = o.right ? yield* this.getOperation(o.right) : null;
|
||||
i++
|
||||
o = o.right ? yield* this.getOperation(o.right) : null
|
||||
} else {
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// reconnect..
|
||||
var left = null;
|
||||
var right = null;
|
||||
parent = parent || (yield* this.getOperation(op.parent));
|
||||
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);
|
||||
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;
|
||||
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;
|
||||
yield* this.setOperation(right);
|
||||
right = yield* this.getOperation(op.right)
|
||||
right.left = op.id
|
||||
yield* this.setOperation(right)
|
||||
}
|
||||
|
||||
// notify parent
|
||||
if (op.parentSub != null) {
|
||||
if (left == null) {
|
||||
parent.map[op.parentSub] = op.id;
|
||||
yield* this.setOperation(parent);
|
||||
parent.map[op.parentSub] = op.id
|
||||
yield* this.setOperation(parent)
|
||||
}
|
||||
} else {
|
||||
if (right == null || left == null) {
|
||||
if (right == null) {
|
||||
parent.end = op.id;
|
||||
parent.end = op.id
|
||||
}
|
||||
if (left == null) {
|
||||
parent.start = op.id;
|
||||
parent.start = op.id
|
||||
}
|
||||
yield* this.setOperation(parent);
|
||||
yield* this.setOperation(parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -229,61 +210,61 @@ var Struct = {
|
||||
id: this.os.getNextOpId()
|
||||
}
|
||||
*/
|
||||
encode: function(op){
|
||||
encode: function (op) {
|
||||
return {
|
||||
struct: "List",
|
||||
struct: 'List',
|
||||
id: op.id,
|
||||
type: op.type
|
||||
};
|
||||
}
|
||||
},
|
||||
requiredOps: function(){
|
||||
requiredOps: function () {
|
||||
/*
|
||||
var ids = [];
|
||||
var ids = []
|
||||
if (op.start != null) {
|
||||
ids.push(op.start);
|
||||
ids.push(op.start)
|
||||
}
|
||||
if (op.end != null){
|
||||
ids.push(op.end);
|
||||
ids.push(op.end)
|
||||
}
|
||||
return ids;
|
||||
return ids
|
||||
*/
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
execute: function* (op) {
|
||||
op.start = null;
|
||||
op.end = null;
|
||||
execute: function * (op) { // eslint-disable-line
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
ref: function* (op : Op, pos : number) : Insert {
|
||||
ref: function * (op, pos) {
|
||||
if (op.start == null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
var res = null;
|
||||
var o = yield* this.getOperation(op.start);
|
||||
var res = null
|
||||
var o = yield* this.getOperation(op.start)
|
||||
|
||||
while ( true ) {
|
||||
while (true) {
|
||||
if (!o.deleted) {
|
||||
res = o;
|
||||
pos--;
|
||||
res = o
|
||||
pos--
|
||||
}
|
||||
if (pos >= 0 && o.right != null) {
|
||||
o = (yield* this.getOperation(o.right));
|
||||
o = (yield* this.getOperation(o.right))
|
||||
} else {
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
return res;
|
||||
return res
|
||||
},
|
||||
map: function* (o : Op, f : Function) : Array<any> {
|
||||
o = o.start;
|
||||
var res = [];
|
||||
while ( o !== null) {
|
||||
var operation = yield* this.getOperation(o);
|
||||
map: function * (o, f) {
|
||||
o = o.start
|
||||
var res = []
|
||||
while (o !== null) {
|
||||
var operation = yield* this.getOperation(o)
|
||||
if (!operation.deleted) {
|
||||
res.push(f(operation));
|
||||
res.push(f(operation))
|
||||
}
|
||||
o = operation.right;
|
||||
o = operation.right
|
||||
}
|
||||
return res;
|
||||
return res
|
||||
}
|
||||
},
|
||||
Map: {
|
||||
@ -295,42 +276,41 @@ var Struct = {
|
||||
id: this.os.getNextOpId()
|
||||
}
|
||||
*/
|
||||
encode: function(op){
|
||||
encode: function (op) {
|
||||
return {
|
||||
struct: "Map",
|
||||
struct: 'Map',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {} // overwrite map!!
|
||||
};
|
||||
}
|
||||
},
|
||||
requiredOps: function(){
|
||||
requiredOps: function () {
|
||||
/*
|
||||
var ids = [];
|
||||
var ids = []
|
||||
for (var end in op.map) {
|
||||
ids.push(op.map[end]);
|
||||
ids.push(op.map[end])
|
||||
}
|
||||
return ids;
|
||||
return ids
|
||||
*/
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
execute: function* () {
|
||||
},
|
||||
get: function* (op, name) {
|
||||
var oid = op.map[name];
|
||||
execute: function * () {},
|
||||
get: function * (op, name) {
|
||||
var oid = op.map[name]
|
||||
if (oid != null) {
|
||||
var res = yield* this.getOperation(oid);
|
||||
var res = yield* this.getOperation(oid)
|
||||
return (res == null || res.deleted) ? void 0 : (res.opContent == null
|
||||
? res.content : yield* this.getType(res.opContent));
|
||||
? res.content : yield* this.getType(res.opContent))
|
||||
}
|
||||
},
|
||||
delete: function* (op, name) {
|
||||
var v = op.map[name] || null;
|
||||
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
|
||||
|
@ -1,86 +1,85 @@
|
||||
/* global EventHandler, Y, CustomType, Struct */
|
||||
|
||||
|
||||
(function(){
|
||||
|
||||
;(function () {
|
||||
class YArray {
|
||||
constructor (os, _model, idArray, valArray) {
|
||||
this.os = os;
|
||||
this._model = _model;
|
||||
this.os = os
|
||||
this._model = _model
|
||||
// Array of all the operation id's
|
||||
this.idArray = idArray;
|
||||
this.idArray = idArray
|
||||
// Array of all the values
|
||||
this.valArray = valArray;
|
||||
this.eventHandler = new EventHandler( ops =>{
|
||||
var userEvents = [];
|
||||
this.valArray = valArray
|
||||
this.eventHandler = new EventHandler(ops => {
|
||||
var userEvents = []
|
||||
for (var i in ops) {
|
||||
var op = ops[i];
|
||||
if (op.struct === "Insert") {
|
||||
let pos;
|
||||
var op = ops[i]
|
||||
if (op.struct === 'Insert') {
|
||||
let pos
|
||||
// we check op.left only!,
|
||||
// because op.right might not be defined when this is called
|
||||
if (op.left === null) {
|
||||
pos = 0;
|
||||
pos = 0
|
||||
} else {
|
||||
var sid = JSON.stringify(op.left);
|
||||
pos = this.idArray.indexOf(sid) + 1;
|
||||
var sid = JSON.stringify(op.left)
|
||||
pos = this.idArray.indexOf(sid) + 1
|
||||
if (pos <= 0) {
|
||||
throw new Error("Unexpected operation!");
|
||||
throw new Error('Unexpected operation!')
|
||||
}
|
||||
}
|
||||
this.idArray.splice(pos, 0, JSON.stringify(op.id));
|
||||
this.valArray.splice(pos, 0, op.content);
|
||||
this.idArray.splice(pos, 0, JSON.stringify(op.id))
|
||||
this.valArray.splice(pos, 0, op.content)
|
||||
userEvents.push({
|
||||
type: "insert",
|
||||
type: 'insert',
|
||||
object: this,
|
||||
index: pos,
|
||||
length: 1
|
||||
});
|
||||
} else if (op.struct === "Delete") {
|
||||
let pos = this.idArray.indexOf(JSON.stringify(op.target));
|
||||
this.idArray.splice(pos, 1);
|
||||
this.valArray.splice(pos, 1);
|
||||
})
|
||||
} else if (op.struct === 'Delete') {
|
||||
let pos = this.idArray.indexOf(JSON.stringify(op.target))
|
||||
this.idArray.splice(pos, 1)
|
||||
this.valArray.splice(pos, 1)
|
||||
userEvents.push({
|
||||
type: "delete",
|
||||
type: 'delete',
|
||||
object: this,
|
||||
index: pos,
|
||||
length: 1
|
||||
});
|
||||
})
|
||||
} else {
|
||||
throw new Error("Unexpected struct!");
|
||||
throw new Error('Unexpected struct!')
|
||||
}
|
||||
}
|
||||
this.eventHandler.callUserEventListeners(userEvents);
|
||||
});
|
||||
this.eventHandler.callUserEventListeners(userEvents)
|
||||
})
|
||||
}
|
||||
get length () {
|
||||
return this.idArray.length;
|
||||
return this.idArray.length
|
||||
}
|
||||
get (pos) {
|
||||
if (pos == null || typeof pos !== "number") {
|
||||
throw new Error("pos must be a number!");
|
||||
if (pos == null || typeof pos !== 'number') {
|
||||
throw new Error('pos must be a number!')
|
||||
}
|
||||
return this.valArray[pos];
|
||||
return this.valArray[pos]
|
||||
}
|
||||
toArray() {
|
||||
return this.valArray.slice();
|
||||
toArray () {
|
||||
return this.valArray.slice()
|
||||
}
|
||||
insert (pos, contents) {
|
||||
if (typeof pos !== "number") {
|
||||
throw new Error("pos must be a number!");
|
||||
if (typeof pos !== 'number') {
|
||||
throw new Error('pos must be a number!')
|
||||
}
|
||||
if (!(contents instanceof Array)) {
|
||||
throw new Error("contents must be an Array of objects!");
|
||||
throw new Error('contents must be an Array of objects!')
|
||||
}
|
||||
if (contents.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (pos > this.idArray.length || pos < 0) {
|
||||
throw new Error("This position exceeds the range of the array!");
|
||||
throw new Error('This position exceeds the range of the array!')
|
||||
}
|
||||
var mostLeft = pos === 0 ? null : JSON.parse(this.idArray[pos - 1]);
|
||||
var mostLeft = pos === 0 ? null : JSON.parse(this.idArray[pos - 1])
|
||||
|
||||
var ops = [];
|
||||
var prevId = mostLeft;
|
||||
var ops = []
|
||||
var prevId = mostLeft
|
||||
for (var i = 0; i < contents.length; i++) {
|
||||
var op = {
|
||||
left: prevId,
|
||||
@ -90,97 +89,97 @@
|
||||
// at the time of creating this operation, and is therefore not defined in idArray
|
||||
parent: this._model,
|
||||
content: contents[i],
|
||||
struct: "Insert",
|
||||
struct: 'Insert',
|
||||
id: this.os.getNextOpId()
|
||||
};
|
||||
ops.push(op);
|
||||
prevId = op.id;
|
||||
}
|
||||
ops.push(op)
|
||||
prevId = op.id
|
||||
}
|
||||
var eventHandler = this.eventHandler;
|
||||
eventHandler.awaitAndPrematurelyCall(ops);
|
||||
this.os.requestTransaction(function*(){
|
||||
var eventHandler = this.eventHandler
|
||||
eventHandler.awaitAndPrematurelyCall(ops)
|
||||
this.os.requestTransaction(function *() {
|
||||
// now we can set the right reference.
|
||||
var mostRight;
|
||||
var mostRight
|
||||
if (mostLeft != null) {
|
||||
mostRight = (yield* this.getOperation(mostLeft)).right;
|
||||
mostRight = (yield* this.getOperation(mostLeft)).right
|
||||
} else {
|
||||
mostRight = (yield* this.getOperation(ops[0].parent)).start;
|
||||
mostRight = (yield* this.getOperation(ops[0].parent)).start
|
||||
}
|
||||
for (var j in ops) {
|
||||
ops[j].right = mostRight;
|
||||
ops[j].right = mostRight
|
||||
}
|
||||
yield* this.applyCreatedOperations(ops);
|
||||
eventHandler.awaitedLastInserts(ops.length);
|
||||
});
|
||||
yield* this.applyCreatedOperations(ops)
|
||||
eventHandler.awaitedLastInserts(ops.length)
|
||||
})
|
||||
}
|
||||
delete (pos, length = 1) {
|
||||
if (typeof length !== "number") {
|
||||
throw new Error("pos must be a number!");
|
||||
if (typeof length !== 'number') {
|
||||
throw new Error('pos must be a number!')
|
||||
}
|
||||
if (typeof pos !== "number") {
|
||||
throw new Error("pos must be a number!");
|
||||
if (typeof pos !== 'number') {
|
||||
throw new Error('pos must be a number!')
|
||||
}
|
||||
if (pos + length > this.idArray.length || pos < 0 || length < 0) {
|
||||
throw new Error("The deletion range exceeds the range of the array!");
|
||||
throw new Error('The deletion range exceeds the range of the array!')
|
||||
}
|
||||
if (length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
var eventHandler = this.eventHandler;
|
||||
var newLeft = pos > 0 ? JSON.parse(this.idArray[pos - 1]) : null;
|
||||
var dels = [];
|
||||
var eventHandler = this.eventHandler
|
||||
var newLeft = pos > 0 ? JSON.parse(this.idArray[pos - 1]) : null
|
||||
var dels = []
|
||||
for (var i = 0; i < length; i++) {
|
||||
dels.push({
|
||||
target: JSON.parse(this.idArray[pos + i]),
|
||||
struct: "Delete"
|
||||
});
|
||||
struct: 'Delete'
|
||||
})
|
||||
}
|
||||
eventHandler.awaitAndPrematurelyCall(dels);
|
||||
this.os.requestTransaction(function*(){
|
||||
yield* this.applyCreatedOperations(dels);
|
||||
eventHandler.awaitedLastDeletes(dels.length, newLeft);
|
||||
});
|
||||
eventHandler.awaitAndPrematurelyCall(dels)
|
||||
this.os.requestTransaction(function *() {
|
||||
yield* this.applyCreatedOperations(dels)
|
||||
eventHandler.awaitedLastDeletes(dels.length, newLeft)
|
||||
})
|
||||
}
|
||||
observe (f) {
|
||||
this.eventHandler.addUserEventListener(f);
|
||||
this.eventHandler.addUserEventListener(f)
|
||||
}
|
||||
*_changed (transaction, op) {
|
||||
if (op.struct === "Insert") {
|
||||
var l = op.left;
|
||||
var left;
|
||||
* _changed (transaction, op) {
|
||||
if (op.struct === 'Insert') {
|
||||
var l = op.left
|
||||
var left
|
||||
while (l != null) {
|
||||
left = yield* transaction.getOperation(l);
|
||||
left = yield* transaction.getOperation(l)
|
||||
if (!left.deleted) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
l = left.left;
|
||||
l = left.left
|
||||
}
|
||||
op.left = l;
|
||||
op.left = l
|
||||
}
|
||||
this.eventHandler.receivedOp(op);
|
||||
this.eventHandler.receivedOp(op)
|
||||
}
|
||||
}
|
||||
|
||||
Y.Array = new CustomType({
|
||||
class: YArray,
|
||||
createType: function* YArrayCreator () {
|
||||
createType: function * YArrayCreator () {
|
||||
var model = {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "List",
|
||||
type: "Array",
|
||||
struct: 'List',
|
||||
type: 'Array',
|
||||
id: this.store.getNextOpId()
|
||||
};
|
||||
yield* this.applyCreatedOperations([model]);
|
||||
return yield* this.createType(model);
|
||||
}
|
||||
yield* this.applyCreatedOperations([model])
|
||||
return yield* this.createType(model)
|
||||
},
|
||||
initType: function* YArrayInitializer(os, model){
|
||||
var valArray = [];
|
||||
var idArray = yield* Y.Struct.List.map.call(this, model, function(c){
|
||||
valArray.push(c.content);
|
||||
return JSON.stringify(c.id);
|
||||
});
|
||||
return new YArray(os, model.id, idArray, valArray);
|
||||
initType: function * YArrayInitializer (os, model) {
|
||||
var valArray = []
|
||||
var idArray = yield* Struct.List.map.call(this, model, function (c) {
|
||||
valArray.push(c.content)
|
||||
return JSON.stringify(c.id)
|
||||
})
|
||||
return new YArray(os, model.id, idArray, valArray)
|
||||
}
|
||||
});
|
||||
})();
|
||||
})
|
||||
})()
|
||||
|
@ -1,143 +1,145 @@
|
||||
/* @flow */
|
||||
/*eslint-env browser,jasmine */
|
||||
/* global createUsers, wait, Y, compareAllUsers, getRandomNumber, applyRandomTransactions */
|
||||
/* eslint-env browser,jasmine */
|
||||
|
||||
var numberOfYArrayTests = 80;
|
||||
var numberOfYArrayTests = 80
|
||||
|
||||
describe("Array Type", function(){
|
||||
var y1, y2, y3, flushAll;
|
||||
describe('Array Type', function () {
|
||||
var y1, y2, y3, flushAll
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000;
|
||||
beforeEach(async function(done){
|
||||
await createUsers(this, 5);
|
||||
y1 = this.users[0].root;
|
||||
y2 = this.users[1].root;
|
||||
y3 = this.users[2].root;
|
||||
flushAll = this.users[0].connector.flushAll;
|
||||
done();
|
||||
});
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000
|
||||
beforeEach(async function (done) {
|
||||
await createUsers(this, 5)
|
||||
y1 = this.users[0].root
|
||||
y2 = this.users[1].root
|
||||
y3 = this.users[2].root
|
||||
flushAll = this.users[0].connector.flushAll
|
||||
done()
|
||||
})
|
||||
afterEach(async function(done) {
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
});
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
})
|
||||
|
||||
describe("Basic tests", function(){
|
||||
it("insert three elements, try re-get property", async function(done){
|
||||
var array = await y1.set("Array", Y.Array);
|
||||
array.insert(0, [1, 2, 3]);
|
||||
array = await y1.get("Array"); // re-get property
|
||||
expect(array.toArray()).toEqual([1, 2, 3]);
|
||||
done();
|
||||
});
|
||||
it("Basic insert in array (handle three conflicts)", async function(done){
|
||||
var l1, l2, l3;
|
||||
await y1.set("Array", Y.Array);
|
||||
await flushAll();
|
||||
(l1 = await y1.get("Array")).insert(0, [0]);
|
||||
(l2 = await y2.get("Array")).insert(0, [1]);
|
||||
(l3 = await y3.get("Array")).insert(0, [2]);
|
||||
await flushAll();
|
||||
expect(l1.toArray()).toEqual(l2.toArray());
|
||||
expect(l2.toArray()).toEqual(l3.toArray());
|
||||
done();
|
||||
});
|
||||
it("Basic insert&delete in array (handle three conflicts)", async function(done){
|
||||
var l1, l2, l3;
|
||||
l1 = await y1.set("Array", Y.Array);
|
||||
l1.insert(0, ["x", "y", "z"]);
|
||||
await flushAll();
|
||||
l1.insert(1, [0]);
|
||||
l2 = await y2.get("Array");
|
||||
l2.delete(0);
|
||||
l2.delete(1);
|
||||
l3 = await y3.get("Array");
|
||||
l3.insert(1, [2]);
|
||||
await flushAll();
|
||||
expect(l1.toArray()).toEqual(l2.toArray());
|
||||
expect(l2.toArray()).toEqual(l3.toArray());
|
||||
expect(l2.toArray()).toEqual([0, 2, "y"]);
|
||||
done();
|
||||
});
|
||||
it("Basic insert. Then delete the whole array", async function(done){
|
||||
var l1, l2, l3;
|
||||
l1 = await y1.set("Array", Y.Array);
|
||||
l1.insert(0, ["x", "y", "z"]);
|
||||
await flushAll();
|
||||
l1.delete(0, 3);
|
||||
l2 = await y2.get("Array");
|
||||
l3 = await y3.get("Array");
|
||||
await flushAll();
|
||||
expect(l1.toArray()).toEqual(l2.toArray());
|
||||
expect(l2.toArray()).toEqual(l3.toArray());
|
||||
expect(l2.toArray()).toEqual([]);
|
||||
done();
|
||||
});
|
||||
it("throw insert & delete events", async function(done){
|
||||
var array = await this.users[0].root.set("array", Y.Array);
|
||||
var event;
|
||||
array.observe(function(e){
|
||||
event = e;
|
||||
});
|
||||
array.insert(0, [0]);
|
||||
describe('Basic tests', function () {
|
||||
it('insert three elements, try re-get property', async function (done) {
|
||||
var array = await y1.set('Array', Y.Array)
|
||||
array.insert(0, [1, 2, 3])
|
||||
array = await y1.get('Array') // re-get property
|
||||
expect(array.toArray()).toEqual([1, 2, 3])
|
||||
done()
|
||||
})
|
||||
it('Basic insert in array (handle three conflicts)', async function (done) {
|
||||
await y1.set('Array', Y.Array)
|
||||
await flushAll()
|
||||
var l1 = await y1.get('Array')
|
||||
l1.insert(0, [0])
|
||||
var l2 = await y2.get('Array')
|
||||
l2.insert(0, [1])
|
||||
var l3 = await y3.get('Array')
|
||||
l3.insert(0, [2])
|
||||
await flushAll()
|
||||
expect(l1.toArray()).toEqual(l2.toArray())
|
||||
expect(l2.toArray()).toEqual(l3.toArray())
|
||||
done()
|
||||
})
|
||||
it('Basic insert&delete in array (handle three conflicts)', async function (done) {
|
||||
var l1, l2, l3
|
||||
l1 = await y1.set('Array', Y.Array)
|
||||
l1.insert(0, ['x', 'y', 'z'])
|
||||
await flushAll()
|
||||
l1.insert(1, [0])
|
||||
l2 = await y2.get('Array')
|
||||
l2.delete(0)
|
||||
l2.delete(1)
|
||||
l3 = await y3.get('Array')
|
||||
l3.insert(1, [2])
|
||||
await flushAll()
|
||||
expect(l1.toArray()).toEqual(l2.toArray())
|
||||
expect(l2.toArray()).toEqual(l3.toArray())
|
||||
expect(l2.toArray()).toEqual([0, 2, 'y'])
|
||||
done()
|
||||
})
|
||||
it('Basic insert. Then delete the whole array', async function (done) {
|
||||
var l1, l2, l3
|
||||
l1 = await y1.set('Array', Y.Array)
|
||||
l1.insert(0, ['x', 'y', 'z'])
|
||||
await flushAll()
|
||||
l1.delete(0, 3)
|
||||
l2 = await y2.get('Array')
|
||||
l3 = await y3.get('Array')
|
||||
await flushAll()
|
||||
expect(l1.toArray()).toEqual(l2.toArray())
|
||||
expect(l2.toArray()).toEqual(l3.toArray())
|
||||
expect(l2.toArray()).toEqual([])
|
||||
done()
|
||||
})
|
||||
it('throw insert & delete events', async function (done) {
|
||||
var array = await this.users[0].root.set('array', Y.Array)
|
||||
var event
|
||||
array.observe(function (e) {
|
||||
event = e
|
||||
})
|
||||
array.insert(0, [0])
|
||||
expect(event).toEqual([{
|
||||
type: "insert",
|
||||
type: 'insert',
|
||||
object: array,
|
||||
index: 0,
|
||||
length: 1
|
||||
}]);
|
||||
array.delete(0);
|
||||
}])
|
||||
array.delete(0)
|
||||
expect(event).toEqual([{
|
||||
type: "delete",
|
||||
type: 'delete',
|
||||
object: array,
|
||||
index: 0,
|
||||
length: 1
|
||||
}]);
|
||||
await wait(50);
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe(`${numberOfYArrayTests} Random tests`, function(){
|
||||
}])
|
||||
await wait(50)
|
||||
done()
|
||||
})
|
||||
})
|
||||
describe(`${numberOfYArrayTests} Random tests`, function () {
|
||||
var randomArrayTransactions = [
|
||||
function insert (array) {
|
||||
array.insert(getRandomNumber(array.toArray().length), [getRandomNumber()]);
|
||||
array.insert(getRandomNumber(array.toArray().length), [getRandomNumber()])
|
||||
},
|
||||
function _delete (array) {
|
||||
var length = array.toArray().length;
|
||||
var length = array.toArray().length
|
||||
if (length > 0) {
|
||||
array.delete(getRandomNumber(length - 1));
|
||||
array.delete(getRandomNumber(length - 1))
|
||||
}
|
||||
}
|
||||
];
|
||||
function compareArrayValues(arrays){
|
||||
var firstArray;
|
||||
]
|
||||
function compareArrayValues (arrays) {
|
||||
var firstArray
|
||||
for (var l of arrays) {
|
||||
var val = l.toArray();
|
||||
var val = l.toArray()
|
||||
if (firstArray == null) {
|
||||
firstArray = val;
|
||||
firstArray = val
|
||||
} else {
|
||||
expect(val).toEqual(firstArray);
|
||||
expect(val).toEqual(firstArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
beforeEach(async function(done){
|
||||
await this.users[0].root.set("Array", Y.Array);
|
||||
await flushAll();
|
||||
beforeEach(async function (done) {
|
||||
await this.users[0].root.set('Array', Y.Array)
|
||||
await flushAll()
|
||||
|
||||
var promises = [];
|
||||
var promises = []
|
||||
for (var u = 0; u < this.users.length; u++) {
|
||||
promises.push(this.users[u].root.get("Array"));
|
||||
promises.push(this.users[u].root.get('Array'))
|
||||
}
|
||||
this.arrays = await Promise.all(promises);
|
||||
done();
|
||||
});
|
||||
it("arrays.length equals users.length", async function(done){
|
||||
expect(this.arrays.length).toEqual(this.users.length);
|
||||
done();
|
||||
});
|
||||
it(`succeed after ${numberOfYArrayTests} actions`, async function(done){
|
||||
await applyRandomTransactions(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests);
|
||||
await flushAll();
|
||||
await compareArrayValues(this.arrays);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
this.arrays = await Promise.all(promises)
|
||||
done()
|
||||
})
|
||||
it('arrays.length equals users.length', async function (done) { // eslint-disable-line
|
||||
expect(this.arrays.length).toEqual(this.users.length)
|
||||
done()
|
||||
})
|
||||
it(`succeed after ${numberOfYArrayTests} actions`, async function (done) {
|
||||
await applyRandomTransactions(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests)
|
||||
await flushAll()
|
||||
await compareArrayValues(this.arrays)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
260
src/Types/Map.js
260
src/Types/Map.js
@ -1,76 +1,78 @@
|
||||
(function(){
|
||||
/* global EventHandler, Y, CustomType, copyObject, compareIds */
|
||||
|
||||
;(function () {
|
||||
class YMap {
|
||||
constructor (os, model) {
|
||||
this._model = model.id;
|
||||
this.os = os;
|
||||
this.map = copyObject(model.map);
|
||||
this.contents = {};
|
||||
this.opContents = {};
|
||||
this.eventHandler = new EventHandler( ops =>{
|
||||
var userEvents = [];
|
||||
this._model = model.id
|
||||
this.os = os
|
||||
this.map = copyObject(model.map)
|
||||
this.contents = {}
|
||||
this.opContents = {}
|
||||
this.eventHandler = new EventHandler(ops => {
|
||||
var userEvents = []
|
||||
for (var i in ops) {
|
||||
var op = ops[i];
|
||||
var oldValue;
|
||||
var op = ops[i]
|
||||
var oldValue
|
||||
// key is the name to use to access (op)content
|
||||
var key = op.struct === "Delete" ? op.key : op.parentSub;
|
||||
var key = op.struct === 'Delete' ? op.key : op.parentSub
|
||||
|
||||
// compute oldValue
|
||||
if (this.opContents[key] != null) {
|
||||
let prevType = this.opContents[key];
|
||||
oldValue = () => { //eslint-disable-line
|
||||
let def = Promise.defer();
|
||||
this.os.requestTransaction(function*(){//eslint-disable-line
|
||||
def.resolve(yield* this.getType(prevType));
|
||||
});
|
||||
return def.promise;
|
||||
};
|
||||
let prevType = this.opContents[key]
|
||||
oldValue = () => {// eslint-disable-line
|
||||
let def = Promise.defer()
|
||||
this.os.requestTransaction(function *() {// eslint-disable-line
|
||||
def.resolve(yield* this.getType(prevType))
|
||||
})
|
||||
return def.promise
|
||||
}
|
||||
} else {
|
||||
oldValue = this.contents[key];
|
||||
oldValue = this.contents[key]
|
||||
}
|
||||
// compute op event
|
||||
if (op.struct === "Insert"){
|
||||
if (op.struct === 'Insert') {
|
||||
if (op.left === null) {
|
||||
if (op.opContent != null) {
|
||||
delete this.contents[key];
|
||||
this.opContents[key] = op.opContent;
|
||||
delete this.contents[key]
|
||||
this.opContents[key] = op.opContent
|
||||
} else {
|
||||
delete this.opContents[key];
|
||||
this.contents[key] = op.content;
|
||||
delete this.opContents[key]
|
||||
this.contents[key] = op.content
|
||||
}
|
||||
this.map[key] = op.id;
|
||||
this.map[key] = op.id
|
||||
var insertEvent = {
|
||||
name: key,
|
||||
object: this
|
||||
};
|
||||
if (oldValue === undefined) {
|
||||
insertEvent.type = "add";
|
||||
} else {
|
||||
insertEvent.type = "update";
|
||||
insertEvent.oldValue = oldValue;
|
||||
}
|
||||
userEvents.push(insertEvent);
|
||||
if (oldValue === undefined) {
|
||||
insertEvent.type = 'add'
|
||||
} else {
|
||||
insertEvent.type = 'update'
|
||||
insertEvent.oldValue = oldValue
|
||||
}
|
||||
userEvents.push(insertEvent)
|
||||
}
|
||||
} else if (op.struct === "Delete") {
|
||||
} else if (op.struct === 'Delete') {
|
||||
if (compareIds(this.map[key], op.target)) {
|
||||
if (this.opContents[key] != null) {
|
||||
delete this.opContents[key];
|
||||
delete this.opContents[key]
|
||||
} else {
|
||||
delete this.contents[key];
|
||||
delete this.contents[key]
|
||||
}
|
||||
var deleteEvent = {
|
||||
name: key,
|
||||
object: this,
|
||||
oldValue: oldValue,
|
||||
type: "delete"
|
||||
};
|
||||
userEvents.push(deleteEvent);
|
||||
type: 'delete'
|
||||
}
|
||||
userEvents.push(deleteEvent)
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unexpected Operation!");
|
||||
throw new Error('Unexpected Operation!')
|
||||
}
|
||||
}
|
||||
this.eventHandler.callUserEventListeners(userEvents);
|
||||
});
|
||||
this.eventHandler.callUserEventListeners(userEvents)
|
||||
})
|
||||
}
|
||||
get (key) {
|
||||
// return property.
|
||||
@ -78,34 +80,34 @@
|
||||
// if property is a type, return a promise
|
||||
if (this.opContents[key] == null) {
|
||||
if (key == null) {
|
||||
return copyObject(this.contents);
|
||||
return copyObject(this.contents)
|
||||
} else {
|
||||
return this.contents[key];
|
||||
return this.contents[key]
|
||||
}
|
||||
} else {
|
||||
let def = Promise.defer();
|
||||
var oid = this.opContents[key];
|
||||
this.os.requestTransaction(function*(){
|
||||
def.resolve(yield* this.getType(oid));
|
||||
});
|
||||
return def.promise;
|
||||
let def = Promise.defer()
|
||||
var oid = this.opContents[key]
|
||||
this.os.requestTransaction(function *() {
|
||||
def.resolve(yield* this.getType(oid))
|
||||
})
|
||||
return def.promise
|
||||
}
|
||||
}
|
||||
delete (key) {
|
||||
var right = this.map[key];
|
||||
var right = this.map[key]
|
||||
if (right != null) {
|
||||
var del = {
|
||||
target: right,
|
||||
struct: "Delete"
|
||||
};
|
||||
var eventHandler = this.eventHandler;
|
||||
var modDel = copyObject(del);
|
||||
modDel.key = key;
|
||||
eventHandler.awaitAndPrematurelyCall([modDel]);
|
||||
this.os.requestTransaction(function*(){
|
||||
yield* this.applyCreatedOperations([del]);
|
||||
eventHandler.awaitedLastDeletes(1);
|
||||
});
|
||||
struct: 'Delete'
|
||||
}
|
||||
var eventHandler = this.eventHandler
|
||||
var modDel = copyObject(del)
|
||||
modDel.key = key
|
||||
eventHandler.awaitAndPrematurelyCall([modDel])
|
||||
this.os.requestTransaction(function *() {
|
||||
yield* this.applyCreatedOperations([del])
|
||||
eventHandler.awaitedLastDeletes(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
set (key, value) {
|
||||
@ -113,108 +115,108 @@
|
||||
// if property is a type, return a promise
|
||||
// if not, apply immediately on this type an call event
|
||||
|
||||
var right = this.map[key] || null;
|
||||
var right = this.map[key] || null
|
||||
var insert = {
|
||||
left: null,
|
||||
right: right,
|
||||
origin: null,
|
||||
parent: this._model,
|
||||
parentSub: key,
|
||||
struct: "Insert"
|
||||
};
|
||||
var def = Promise.defer();
|
||||
if ( value instanceof CustomType) {
|
||||
// construct a new type
|
||||
this.os.requestTransaction(function*(){
|
||||
var type = yield* value.createType.call(this);
|
||||
insert.opContent = type._model;
|
||||
insert.id = this.store.getNextOpId();
|
||||
yield* this.applyCreatedOperations([insert]);
|
||||
def.resolve(type);
|
||||
});
|
||||
} else {
|
||||
insert.content = value;
|
||||
insert.id = this.os.getNextOpId();
|
||||
var eventHandler = this.eventHandler;
|
||||
eventHandler.awaitAndPrematurelyCall([insert]);
|
||||
|
||||
this.os.requestTransaction(function*(){
|
||||
yield* this.applyCreatedOperations([insert]);
|
||||
eventHandler.awaitedLastInserts(1);
|
||||
});
|
||||
def.resolve(value);
|
||||
struct: 'Insert'
|
||||
}
|
||||
return def.promise;
|
||||
var def = Promise.defer()
|
||||
if (value instanceof CustomType) {
|
||||
// construct a new type
|
||||
this.os.requestTransaction(function *() {
|
||||
var type = yield* value.createType.call(this)
|
||||
insert.opContent = type._model
|
||||
insert.id = this.store.getNextOpId()
|
||||
yield* this.applyCreatedOperations([insert])
|
||||
def.resolve(type)
|
||||
})
|
||||
} else {
|
||||
insert.content = value
|
||||
insert.id = this.os.getNextOpId()
|
||||
var eventHandler = this.eventHandler
|
||||
eventHandler.awaitAndPrematurelyCall([insert])
|
||||
|
||||
this.os.requestTransaction(function *() {
|
||||
yield* this.applyCreatedOperations([insert])
|
||||
eventHandler.awaitedLastInserts(1)
|
||||
})
|
||||
def.resolve(value)
|
||||
}
|
||||
return def.promise
|
||||
}
|
||||
observe (f) {
|
||||
this.eventHandler.addUserEventListener(f);
|
||||
this.eventHandler.addUserEventListener(f)
|
||||
}
|
||||
unobserve (f) {
|
||||
this.eventHandler.removeUserEventListener(f);
|
||||
this.eventHandler.removeUserEventListener(f)
|
||||
}
|
||||
observePath (path, f) {
|
||||
var self = this;
|
||||
var self = this
|
||||
if (path.length === 0) {
|
||||
this.observe(f);
|
||||
return Promise.resolve(function(){
|
||||
self.unobserve(f);
|
||||
});
|
||||
this.observe(f)
|
||||
return Promise.resolve(function () {
|
||||
self.unobserve(f)
|
||||
})
|
||||
} else {
|
||||
var deleteChildObservers;
|
||||
var resetObserverPath = function(){
|
||||
var promise = self.get(path[0]);
|
||||
var deleteChildObservers
|
||||
var resetObserverPath = function () {
|
||||
var promise = self.get(path[0])
|
||||
if (!promise instanceof Promise) {
|
||||
// its either not defined or a premitive value
|
||||
promise = self.set(path[0], Y.Map);
|
||||
promise = self.set(path[0], Y.Map)
|
||||
}
|
||||
return promise.then(function(map){
|
||||
return map.observePath(path.slice(1), f);
|
||||
}).then(function(_deleteChildObservers){
|
||||
deleteChildObservers = _deleteChildObservers;
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
var observer = function(events){
|
||||
return promise.then(function (map) {
|
||||
return map.observePath(path.slice(1), f)
|
||||
}).then(function (_deleteChildObservers) {
|
||||
deleteChildObservers = _deleteChildObservers
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
var observer = function (events) {
|
||||
for (var e in events) {
|
||||
var event = events[e];
|
||||
var event = events[e]
|
||||
if (event.name === path[0]) {
|
||||
deleteChildObservers();
|
||||
if (event.type === "add" || event.type === "update") {
|
||||
resetObserverPath();
|
||||
deleteChildObservers()
|
||||
if (event.type === 'add' || event.type === 'update') {
|
||||
resetObserverPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
self.observe(observer);
|
||||
}
|
||||
self.observe(observer)
|
||||
return resetObserverPath().then(
|
||||
Promise.resolve(function(){
|
||||
deleteChildObservers();
|
||||
self.unobserve(observer);
|
||||
Promise.resolve(function () {
|
||||
deleteChildObservers()
|
||||
self.unobserve(observer)
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
*_changed (transaction, op) {
|
||||
if (op.struct === "Delete") {
|
||||
op.key = (yield* transaction.getOperation(op.target)).parentSub;
|
||||
* _changed (transaction, op) {
|
||||
if (op.struct === 'Delete') {
|
||||
op.key = (yield* transaction.getOperation(op.target)).parentSub
|
||||
}
|
||||
this.eventHandler.receivedOp(op);
|
||||
this.eventHandler.receivedOp(op)
|
||||
}
|
||||
}
|
||||
Y.Map = new CustomType({
|
||||
class: YMap,
|
||||
createType: function* YMapCreator(){
|
||||
createType: function * YMapCreator () {
|
||||
var model = {
|
||||
map: {},
|
||||
struct: "Map",
|
||||
type: "Map",
|
||||
struct: 'Map',
|
||||
type: 'Map',
|
||||
id: this.store.getNextOpId()
|
||||
};
|
||||
yield* this.applyCreatedOperations([model]);
|
||||
return yield* this.createType(model);
|
||||
}
|
||||
yield* this.applyCreatedOperations([model])
|
||||
return yield* this.createType(model)
|
||||
},
|
||||
initType: function* YMapInitializer(os, model){
|
||||
return new YMap(os, model);
|
||||
initType: function * YMapInitializer (os, model) { // eslint-disable-line
|
||||
return new YMap(os, model)
|
||||
}
|
||||
});
|
||||
})();
|
||||
})
|
||||
})()
|
||||
|
@ -1,207 +1,207 @@
|
||||
/* @flow */
|
||||
/*eslint-env browser,jasmine */
|
||||
/* global createUsers, Y, compareAllUsers, getRandomNumber, applyRandomTransactions */
|
||||
/* eslint-env browser,jasmine */
|
||||
|
||||
var numberOfYMapTests = 100;
|
||||
var numberOfYMapTests = 100
|
||||
|
||||
describe("Map Type", function(){
|
||||
var y1, y2, y3, y4, flushAll;
|
||||
describe('Map Type', function () {
|
||||
var y1, y2, y3, y4, flushAll
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000;
|
||||
beforeEach(async function(done){
|
||||
await createUsers(this, 5);
|
||||
y1 = this.users[0].root;
|
||||
y2 = this.users[1].root;
|
||||
y3 = this.users[2].root;
|
||||
y4 = this.users[3].root;
|
||||
flushAll = this.users[0].connector.flushAll;
|
||||
done();
|
||||
});
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000
|
||||
beforeEach(async function (done) {
|
||||
await createUsers(this, 5)
|
||||
y1 = this.users[0].root
|
||||
y2 = this.users[1].root
|
||||
y3 = this.users[2].root
|
||||
y4 = this.users[3].root
|
||||
flushAll = this.users[0].connector.flushAll
|
||||
done()
|
||||
})
|
||||
afterEach(async function(done) {
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
}, 5000);
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
}, 5000)
|
||||
|
||||
describe("Basic tests", function(){
|
||||
it("Basic get&set of Map property (converge via sync)", async function(done){
|
||||
y1.set("stuff", "stuffy");
|
||||
expect(y1.get("stuff")).toEqual("stuffy");
|
||||
await flushAll();
|
||||
describe('Basic tests', function () {
|
||||
it('Basic get&set of Map property (converge via sync)', async function (done) {
|
||||
y1.set('stuff', 'stuffy')
|
||||
expect(y1.get('stuff')).toEqual('stuffy')
|
||||
await flushAll()
|
||||
for (var key in this.users) {
|
||||
var u = this.users[key].root;
|
||||
expect(u.get("stuff")).toEqual("stuffy");
|
||||
var u = this.users[key].root
|
||||
expect(u.get('stuff')).toEqual('stuffy')
|
||||
}
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
});
|
||||
it("Map can set custom types (Map)", async function(done){
|
||||
var map = await y1.set("Map", Y.Map);
|
||||
map.set("one", 1);
|
||||
map = await y1.get("Map");
|
||||
expect(map.get("one")).toEqual(1);
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
});
|
||||
it("Map can set custom types (Array)", async function(done){
|
||||
var array = await y1.set("Array", Y.Array);
|
||||
array.insert(0, [1, 2, 3]);
|
||||
array = await y1.get("Array");
|
||||
expect(array.toArray()).toEqual([1, 2, 3]);
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
});
|
||||
it("Basic get&set of Map property (converge via update)", async function(done){
|
||||
await flushAll();
|
||||
y1.set("stuff", "stuffy");
|
||||
expect(y1.get("stuff")).toEqual("stuffy");
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
})
|
||||
it('Map can set custom types (Map)', async function (done) {
|
||||
var map = await y1.set('Map', Y.Map)
|
||||
map.set('one', 1)
|
||||
map = await y1.get('Map')
|
||||
expect(map.get('one')).toEqual(1)
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
})
|
||||
it('Map can set custom types (Array)', async function (done) {
|
||||
var array = await y1.set('Array', Y.Array)
|
||||
array.insert(0, [1, 2, 3])
|
||||
array = await y1.get('Array')
|
||||
expect(array.toArray()).toEqual([1, 2, 3])
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
})
|
||||
it('Basic get&set of Map property (converge via update)', async function (done) {
|
||||
await flushAll()
|
||||
y1.set('stuff', 'stuffy')
|
||||
expect(y1.get('stuff')).toEqual('stuffy')
|
||||
|
||||
await flushAll();
|
||||
await flushAll()
|
||||
for (var key in this.users) {
|
||||
var r = this.users[key].root;
|
||||
expect(r.get("stuff")).toEqual("stuffy");
|
||||
var r = this.users[key].root
|
||||
expect(r.get('stuff')).toEqual('stuffy')
|
||||
}
|
||||
done();
|
||||
});
|
||||
it("Basic get&set of Map property (handle conflict)", async function(done){
|
||||
await flushAll();
|
||||
y1.set("stuff", "c0");
|
||||
done()
|
||||
})
|
||||
it('Basic get&set of Map property (handle conflict)', async function (done) {
|
||||
await flushAll()
|
||||
y1.set('stuff', 'c0')
|
||||
|
||||
y2.set("stuff", "c1");
|
||||
y2.set('stuff', 'c1')
|
||||
|
||||
await flushAll();
|
||||
await flushAll()
|
||||
for (var key in this.users) {
|
||||
var u = this.users[key];
|
||||
expect(u.root.get("stuff")).toEqual("c0");
|
||||
var u = this.users[key]
|
||||
expect(u.root.get('stuff')).toEqual('c0')
|
||||
}
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
});
|
||||
it("Basic get&set&delete of Map property (handle conflict)", async function(done){
|
||||
await flushAll();
|
||||
y1.set("stuff", "c0");
|
||||
y1.delete("stuff");
|
||||
y2.set("stuff", "c1");
|
||||
await flushAll();
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
})
|
||||
it('Basic get&set&delete of Map property (handle conflict)', async function (done) {
|
||||
await flushAll()
|
||||
y1.set('stuff', 'c0')
|
||||
y1.delete('stuff')
|
||||
y2.set('stuff', 'c1')
|
||||
await flushAll()
|
||||
|
||||
for (var key in this.users) {
|
||||
var u = this.users[key];
|
||||
expect(u.root.get("stuff")).toBeUndefined();
|
||||
var u = this.users[key]
|
||||
expect(u.root.get('stuff')).toBeUndefined()
|
||||
}
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
});
|
||||
it("Basic get&set of Map property (handle three conflicts)", async function(done){
|
||||
await flushAll();
|
||||
y1.set("stuff", "c0");
|
||||
y2.set("stuff", "c1");
|
||||
y2.set("stuff", "c2");
|
||||
y3.set("stuff", "c3");
|
||||
await flushAll();
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
})
|
||||
it('Basic get&set of Map property (handle three conflicts)', async function (done) {
|
||||
await flushAll()
|
||||
y1.set('stuff', 'c0')
|
||||
y2.set('stuff', 'c1')
|
||||
y2.set('stuff', 'c2')
|
||||
y3.set('stuff', 'c3')
|
||||
await flushAll()
|
||||
|
||||
for (var key in this.users) {
|
||||
var u = this.users[key];
|
||||
expect(u.root.get("stuff")).toEqual("c0");
|
||||
var u = this.users[key]
|
||||
expect(u.root.get('stuff')).toEqual('c0')
|
||||
}
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
});
|
||||
it("Basic get&set&delete of Map property (handle three conflicts)", async function(done){
|
||||
await flushAll();
|
||||
y1.set("stuff", "c0");
|
||||
y2.set("stuff", "c1");
|
||||
y2.set("stuff", "c2");
|
||||
y3.set("stuff", "c3");
|
||||
await flushAll();
|
||||
y1.set("stuff", "deleteme");
|
||||
y1.delete("stuff");
|
||||
y2.set("stuff", "c1");
|
||||
y3.set("stuff", "c2");
|
||||
y4.set("stuff", "c3");
|
||||
await flushAll();
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
})
|
||||
it('Basic get&set&delete of Map property (handle three conflicts)', async function (done) {
|
||||
await flushAll()
|
||||
y1.set('stuff', 'c0')
|
||||
y2.set('stuff', 'c1')
|
||||
y2.set('stuff', 'c2')
|
||||
y3.set('stuff', 'c3')
|
||||
await flushAll()
|
||||
y1.set('stuff', 'deleteme')
|
||||
y1.delete('stuff')
|
||||
y2.set('stuff', 'c1')
|
||||
y3.set('stuff', 'c2')
|
||||
y4.set('stuff', 'c3')
|
||||
await flushAll()
|
||||
|
||||
for (var key in this.users) {
|
||||
var u = this.users[key];
|
||||
expect(u.root.get("stuff")).toBeUndefined();
|
||||
var u = this.users[key]
|
||||
expect(u.root.get('stuff')).toBeUndefined()
|
||||
}
|
||||
await compareAllUsers(this.users);
|
||||
done();
|
||||
});
|
||||
it("throws add & update & delete events (with type and primitive content)", async function(done){
|
||||
var event;
|
||||
await flushAll();
|
||||
y1.observe(function(e){
|
||||
event = e; // just put it on event, should be thrown synchronously anyway
|
||||
});
|
||||
y1.set("stuff", 4);
|
||||
await compareAllUsers(this.users)
|
||||
done()
|
||||
})
|
||||
it('throws add & update & delete events (with type and primitive content)', async function (done) {
|
||||
var event
|
||||
await flushAll()
|
||||
y1.observe(function (e) {
|
||||
event = e // just put it on event, should be thrown synchronously anyway
|
||||
})
|
||||
y1.set('stuff', 4)
|
||||
expect(event).toEqual([{
|
||||
type: "add",
|
||||
type: 'add',
|
||||
object: y1,
|
||||
name: "stuff"
|
||||
}]);
|
||||
name: 'stuff'
|
||||
}])
|
||||
// update, oldValue is in contents
|
||||
await y1.set("stuff", Y.Array);
|
||||
await y1.set('stuff', Y.Array)
|
||||
expect(event).toEqual([{
|
||||
type: "update",
|
||||
type: 'update',
|
||||
object: y1,
|
||||
name: "stuff",
|
||||
name: 'stuff',
|
||||
oldValue: 4
|
||||
}]);
|
||||
y1.get("stuff").then(function(replacedArray){
|
||||
}])
|
||||
y1.get('stuff').then(function (replacedArray) {
|
||||
// update, oldValue is in opContents
|
||||
y1.set("stuff", 5);
|
||||
var getYArray = event[0].oldValue;
|
||||
expect(typeof getYArray.constructor === "function").toBeTruthy();
|
||||
getYArray().then(function(array){
|
||||
expect(array).toEqual(replacedArray);
|
||||
y1.set('stuff', 5)
|
||||
var getYArray = event[0].oldValue
|
||||
expect(typeof getYArray.constructor === 'function').toBeTruthy()
|
||||
getYArray().then(function (array) {
|
||||
expect(array).toEqual(replacedArray)
|
||||
|
||||
// delete
|
||||
y1.delete("stuff");
|
||||
y1.delete('stuff')
|
||||
expect(event).toEqual([{
|
||||
type: "delete",
|
||||
name: "stuff",
|
||||
type: 'delete',
|
||||
name: 'stuff',
|
||||
object: y1,
|
||||
oldValue: 5
|
||||
}]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe(`${numberOfYMapTests} Random tests`, function(){
|
||||
}])
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
describe(`${numberOfYMapTests} Random tests`, function () {
|
||||
var randomMapTransactions = [
|
||||
function set (map) {
|
||||
map.set("somekey", getRandomNumber());
|
||||
map.set('somekey', getRandomNumber())
|
||||
},
|
||||
function delete_ (map) {
|
||||
map.delete("somekey");
|
||||
map.delete('somekey')
|
||||
}
|
||||
];
|
||||
function compareMapValues(maps){
|
||||
var firstMap;
|
||||
]
|
||||
function compareMapValues (maps) {
|
||||
var firstMap
|
||||
for (var map of maps) {
|
||||
var val = map.get();
|
||||
var val = map.get()
|
||||
if (firstMap == null) {
|
||||
firstMap = val;
|
||||
firstMap = val
|
||||
} else {
|
||||
expect(val).toEqual(firstMap);
|
||||
expect(val).toEqual(firstMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
beforeEach(async function(done){
|
||||
await y1.set("Map", Y.Map);
|
||||
await flushAll();
|
||||
beforeEach(async function (done) {
|
||||
await y1.set('Map', Y.Map)
|
||||
await flushAll()
|
||||
|
||||
var promises = [];
|
||||
var promises = []
|
||||
for (var u = 0; u < this.users.length; u++) {
|
||||
promises.push(this.users[u].root.get("Map"));
|
||||
promises.push(this.users[u].root.get('Map'))
|
||||
}
|
||||
this.maps = await Promise.all(promises);
|
||||
done();
|
||||
});
|
||||
it(`succeed after ${numberOfYMapTests} actions`, async function(done){
|
||||
await applyRandomTransactions(this.users, this.maps, randomMapTransactions, numberOfYMapTests);
|
||||
await flushAll();
|
||||
await compareMapValues(this.maps);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
this.maps = await Promise.all(promises)
|
||||
done()
|
||||
})
|
||||
it(`succeed after ${numberOfYMapTests} actions`, async function (done) {
|
||||
await applyRandomTransactions(this.users, this.maps, randomMapTransactions, numberOfYMapTests)
|
||||
await flushAll()
|
||||
await compareMapValues(this.maps)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,287 +1,288 @@
|
||||
/* global Y, CustomType */
|
||||
|
||||
(function(){
|
||||
class YTextBind extends Y.Array.class {
|
||||
;(function () {
|
||||
class YTextBind extends Y . Array . class {
|
||||
constructor (os, _model, idArray, valArray) {
|
||||
super(os, _model, idArray, valArray);
|
||||
this.textfields = [];
|
||||
super(os, _model, idArray, valArray)
|
||||
this.textfields = []
|
||||
}
|
||||
toString () {
|
||||
return this.valArray.join("");
|
||||
return this.valArray.join('')
|
||||
}
|
||||
insert (pos, content) {
|
||||
super(pos, content.split(""));
|
||||
super(pos, content.split(''))
|
||||
}
|
||||
bind (textfield, domRoot) {
|
||||
domRoot = domRoot || window; //eslint-disable-line
|
||||
if (domRoot.getSelection == null) {
|
||||
domRoot = window;//eslint-disable-line
|
||||
}
|
||||
domRoot = domRoot || window; // eslint-disable-line
|
||||
if (domRoot.getSelection == null) {
|
||||
domRoot = window;// eslint-disable-line
|
||||
}
|
||||
|
||||
// don't duplicate!
|
||||
for (var t in this.textfields) {
|
||||
if (this.textfields[t] === textfield) {
|
||||
return;
|
||||
// don't duplicate!
|
||||
for (var t in this.textfields) {
|
||||
if (this.textfields[t] === textfield) {
|
||||
return
|
||||
}
|
||||
}
|
||||
var creatorToken = false
|
||||
|
||||
var word = this
|
||||
textfield.value = this.toString()
|
||||
this.textfields.push(textfield)
|
||||
var createRange, writeRange, writeContent
|
||||
if (textfield.selectionStart != null && textfield.setSelectionRange != null) {
|
||||
createRange = function (fix) {
|
||||
var left = textfield.selectionStart
|
||||
var right = textfield.selectionEnd
|
||||
if (fix != null) {
|
||||
left = fix(left)
|
||||
right = fix(right)
|
||||
}
|
||||
return {
|
||||
left: left,
|
||||
right: right
|
||||
}
|
||||
}
|
||||
var creatorToken = false;
|
||||
writeRange = function (range) {
|
||||
writeContent(word.toString())
|
||||
textfield.setSelectionRange(range.left, range.right)
|
||||
}
|
||||
writeContent = function (content) {
|
||||
textfield.value = content
|
||||
}
|
||||
} else {
|
||||
createRange = function (fix) {
|
||||
var range = {}
|
||||
var s = domRoot.getSelection()
|
||||
var clength = textfield.textContent.length
|
||||
range.left = Math.min(s.anchorOffset, clength)
|
||||
range.right = Math.min(s.focusOffset, clength)
|
||||
if (fix != null) {
|
||||
range.left = fix(range.left)
|
||||
range.right = fix(range.right)
|
||||
}
|
||||
var editedElement = s.focusNode
|
||||
if (editedElement === textfield || editedElement === textfield.childNodes[0]) {
|
||||
range.isReal = true
|
||||
} else {
|
||||
range.isReal = false
|
||||
}
|
||||
return range
|
||||
}
|
||||
|
||||
var word = this;
|
||||
textfield.value = this.toString();
|
||||
this.textfields.push(textfield);
|
||||
var createRange, writeRange, writeContent;
|
||||
if(textfield.selectionStart != null && textfield.setSelectionRange != null) {
|
||||
createRange = function (fix) {
|
||||
var left = textfield.selectionStart;
|
||||
var right = textfield.selectionEnd;
|
||||
if (fix != null) {
|
||||
left = fix(left);
|
||||
right = fix(right);
|
||||
writeRange = function (range) {
|
||||
writeContent(word.toString())
|
||||
var textnode = textfield.childNodes[0]
|
||||
if (range.isReal && textnode != null) {
|
||||
if (range.left < 0) {
|
||||
range.left = 0
|
||||
}
|
||||
return {
|
||||
left: left,
|
||||
right: right
|
||||
};
|
||||
};
|
||||
writeRange = function (range) {
|
||||
writeContent(word.toString());
|
||||
textfield.setSelectionRange(range.left, range.right);
|
||||
};
|
||||
writeContent = function (content){
|
||||
textfield.value = content;
|
||||
};
|
||||
range.right = Math.max(range.left, range.right)
|
||||
if (range.right > textnode.length) {
|
||||
range.right = textnode.length
|
||||
}
|
||||
range.left = Math.min(range.left, range.right)
|
||||
var r = document.createRange(); // eslint-disable-line
|
||||
r.setStart(textnode, range.left)
|
||||
r.setEnd(textnode, range.right)
|
||||
var s = window.getSelection(); // eslint-disable-line
|
||||
s.removeAllRanges()
|
||||
s.addRange(r)
|
||||
}
|
||||
}
|
||||
writeContent = function (content) {
|
||||
var contentArray = content.replace(new RegExp('\n', 'g'), ' ').split(' ');// eslint-disable-line
|
||||
textfield.innerText = ''
|
||||
for (var i in contentArray) {
|
||||
var c = contentArray[i]
|
||||
textfield.innerText += c
|
||||
if (i !== contentArray.length - 1) {
|
||||
textfield.innerHTML += ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
writeContent(this.toString())
|
||||
|
||||
this.observe(function (events) {
|
||||
for (var e in events) {
|
||||
var event = events[e]
|
||||
if (!creatorToken) {
|
||||
var oPos, fix
|
||||
if (event.type === 'insert') {
|
||||
oPos = event.index
|
||||
fix = function (cursor) {// eslint-disable-line
|
||||
if (cursor <= oPos) {
|
||||
return cursor
|
||||
} else {
|
||||
cursor += 1
|
||||
return cursor
|
||||
}
|
||||
}
|
||||
var r = createRange(fix)
|
||||
writeRange(r)
|
||||
} else if (event.type === 'delete') {
|
||||
oPos = event.index
|
||||
fix = function (cursor) {// eslint-disable-line
|
||||
if (cursor < oPos) {
|
||||
return cursor
|
||||
} else {
|
||||
cursor -= 1
|
||||
return cursor
|
||||
}
|
||||
}
|
||||
r = createRange(fix)
|
||||
writeRange(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// consume all text-insert changes.
|
||||
textfield.onkeypress = function (event) {
|
||||
if (word.is_deleted) {
|
||||
// if word is deleted, do not do anything ever again
|
||||
textfield.onkeypress = null
|
||||
return true
|
||||
}
|
||||
creatorToken = true
|
||||
var char
|
||||
if (event.keyCode === 13) {
|
||||
char = '\n'
|
||||
} else if (event.key != null) {
|
||||
if (event.charCode === 32) {
|
||||
char = ' '
|
||||
} else {
|
||||
char = event.key
|
||||
}
|
||||
} else {
|
||||
createRange = function (fix) {
|
||||
var range = {};
|
||||
var s = domRoot.getSelection();
|
||||
var clength = textfield.textContent.length;
|
||||
range.left = Math.min(s.anchorOffset, clength);
|
||||
range.right = Math.min(s.focusOffset, clength);
|
||||
if(fix != null){
|
||||
range.left = fix(range.left);
|
||||
range.right = fix(range.right);
|
||||
}
|
||||
var editedElement = s.focusNode;
|
||||
if(editedElement === textfield || editedElement === textfield.childNodes[0]){
|
||||
range.isReal = true;
|
||||
} else {
|
||||
range.isReal = false;
|
||||
}
|
||||
return range;
|
||||
};
|
||||
|
||||
writeRange = function (range) {
|
||||
writeContent(word.toString());
|
||||
var textnode = textfield.childNodes[0];
|
||||
if(range.isReal && textnode != null) {
|
||||
if(range.left < 0){
|
||||
range.left = 0;
|
||||
}
|
||||
range.right = Math.max(range.left, range.right);
|
||||
if (range.right > textnode.length) {
|
||||
range.right = textnode.length;
|
||||
}
|
||||
range.left = Math.min(range.left, range.right);
|
||||
var r = document.createRange(); //eslint-disable-line
|
||||
r.setStart(textnode, range.left);
|
||||
r.setEnd(textnode, range.right);
|
||||
var s = window.getSelection(); //eslint-disable-line
|
||||
s.removeAllRanges();
|
||||
s.addRange(r);
|
||||
}
|
||||
};
|
||||
writeContent = function (content) {
|
||||
var contentArray = content.replace(new RegExp("\n", 'g')," ").split(" ");//eslint-disable-line
|
||||
textfield.innerText = "";
|
||||
for(var i in contentArray){
|
||||
var c = contentArray[i];
|
||||
textfield.innerText += c;
|
||||
if(i !== contentArray.length - 1){
|
||||
textfield.innerHTML += " ";
|
||||
}
|
||||
}
|
||||
};
|
||||
char = window.String.fromCharCode(event.keyCode); // eslint-disable-line
|
||||
}
|
||||
writeContent(this.toString());
|
||||
|
||||
this.observe(function (events) {
|
||||
for(var e in events) {
|
||||
var event = events[e];
|
||||
if (!creatorToken) {
|
||||
var oPos, fix;
|
||||
if( event.type === "insert") {
|
||||
oPos = event.index;
|
||||
fix = function (cursor) {//eslint-disable-line
|
||||
if (cursor <= oPos) {
|
||||
return cursor;
|
||||
} else {
|
||||
cursor += 1;
|
||||
return cursor;
|
||||
}
|
||||
};
|
||||
var r = createRange(fix);
|
||||
writeRange(r);
|
||||
} else if (event.type === "delete") {
|
||||
oPos = event.index;
|
||||
fix = function (cursor){//eslint-disable-line
|
||||
if (cursor < oPos) {
|
||||
return cursor;
|
||||
} else {
|
||||
cursor -= 1;
|
||||
return cursor;
|
||||
}
|
||||
};
|
||||
r = createRange(fix);
|
||||
writeRange(r);
|
||||
if (char.length > 1) {
|
||||
return true
|
||||
} else if (char.length > 0) {
|
||||
var r = createRange()
|
||||
var pos = Math.min(r.left, r.right, word.length)
|
||||
var diff = Math.abs(r.right - r.left)
|
||||
word.delete(pos, diff)
|
||||
word.insert(pos, char)
|
||||
r.left = pos + char.length
|
||||
r.right = r.left
|
||||
writeRange(r)
|
||||
}
|
||||
event.preventDefault()
|
||||
creatorToken = false
|
||||
return false
|
||||
}
|
||||
textfield.onpaste = function (event) {
|
||||
if (word.is_deleted) {
|
||||
// if word is deleted, do not do anything ever again
|
||||
textfield.onpaste = null
|
||||
return true
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
textfield.oncut = function (event) {
|
||||
if (word.is_deleted) {
|
||||
// if word is deleted, do not do anything ever again
|
||||
textfield.oncut = null
|
||||
return true
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
//
|
||||
// consume deletes. Note that
|
||||
// chrome: won't consume deletions on keypress event.
|
||||
// keyCode is deprecated. BUT: I don't see another way.
|
||||
// since event.key is not implemented in the current version of chrome.
|
||||
// Every browser supports keyCode. Let's stick with it for now..
|
||||
//
|
||||
textfield.onkeydown = function (event) {
|
||||
creatorToken = true
|
||||
if (word.is_deleted) {
|
||||
// if word is deleted, do not do anything ever again
|
||||
textfield.onkeydown = null
|
||||
return true
|
||||
}
|
||||
var r = createRange()
|
||||
var pos = Math.min(r.left, r.right, word.toString().length)
|
||||
var diff = Math.abs(r.left - r.right)
|
||||
if (event.keyCode != null && event.keyCode === 8) { // Backspace
|
||||
if (diff > 0) {
|
||||
word.delete(pos, diff)
|
||||
r.left = pos
|
||||
r.right = pos
|
||||
writeRange(r)
|
||||
} else {
|
||||
if (event.ctrlKey != null && event.ctrlKey) {
|
||||
var val = word.toString()
|
||||
var newPos = pos
|
||||
var delLength = 0
|
||||
if (pos > 0) {
|
||||
newPos--
|
||||
delLength++
|
||||
}
|
||||
while (newPos > 0 && val[newPos] !== ' ' && val[newPos] !== '\n') {
|
||||
newPos--
|
||||
delLength++
|
||||
}
|
||||
word.delete(newPos, pos - newPos)
|
||||
r.left = newPos
|
||||
r.right = newPos
|
||||
writeRange(r)
|
||||
} else {
|
||||
if (pos > 0) {
|
||||
word.delete(pos - 1, 1)
|
||||
r.left = pos - 1
|
||||
r.right = pos - 1
|
||||
writeRange(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// consume all text-insert changes.
|
||||
textfield.onkeypress = function (event) {
|
||||
if (word.is_deleted) {
|
||||
// if word is deleted, do not do anything ever again
|
||||
textfield.onkeypress = null;
|
||||
return true;
|
||||
}
|
||||
creatorToken = true;
|
||||
var char;
|
||||
if (event.keyCode === 13) {
|
||||
char = "\n";
|
||||
} else if (event.key != null) {
|
||||
if (event.charCode === 32) {
|
||||
char = " ";
|
||||
} else {
|
||||
char = event.key;
|
||||
}
|
||||
event.preventDefault()
|
||||
creatorToken = false
|
||||
return false
|
||||
} else if (event.keyCode != null && event.keyCode === 46) { // Delete
|
||||
if (diff > 0) {
|
||||
word.delete(pos, diff)
|
||||
r.left = pos
|
||||
r.right = pos
|
||||
writeRange(r)
|
||||
} else {
|
||||
char = window.String.fromCharCode(event.keyCode); //eslint-disable-line
|
||||
word.delete(pos, 1)
|
||||
r.left = pos
|
||||
r.right = pos
|
||||
writeRange(r)
|
||||
}
|
||||
if (char.length > 1) {
|
||||
return true;
|
||||
} else if (char.length > 0) {
|
||||
var r = createRange();
|
||||
var pos = Math.min(r.left, r.right, word.length);
|
||||
var diff = Math.abs(r.right - r.left);
|
||||
word.delete(pos, diff);
|
||||
word.insert(pos, char);
|
||||
r.left = pos + char.length;
|
||||
r.right = r.left;
|
||||
writeRange(r);
|
||||
}
|
||||
event.preventDefault();
|
||||
creatorToken = false;
|
||||
return false;
|
||||
};
|
||||
textfield.onpaste = function (event) {
|
||||
if (word.is_deleted) {
|
||||
// if word is deleted, do not do anything ever again
|
||||
textfield.onpaste = null;
|
||||
return true;
|
||||
}
|
||||
event.preventDefault();
|
||||
};
|
||||
textfield.oncut = function (event) {
|
||||
if (word.is_deleted) {
|
||||
// if word is deleted, do not do anything ever again
|
||||
textfield.oncut = null;
|
||||
return true;
|
||||
}
|
||||
event.preventDefault();
|
||||
};
|
||||
//
|
||||
// consume deletes. Note that
|
||||
// chrome: won't consume deletions on keypress event.
|
||||
// keyCode is deprecated. BUT: I don't see another way.
|
||||
// since event.key is not implemented in the current version of chrome.
|
||||
// Every browser supports keyCode. Let's stick with it for now..
|
||||
//
|
||||
textfield.onkeydown = function (event) {
|
||||
creatorToken = true;
|
||||
if (word.is_deleted) {
|
||||
// if word is deleted, do not do anything ever again
|
||||
textfield.onkeydown = null;
|
||||
return true;
|
||||
}
|
||||
var r = createRange();
|
||||
var pos = Math.min(r.left, r.right, word.toString().length);
|
||||
var diff = Math.abs(r.left - r.right);
|
||||
if (event.keyCode != null && event.keyCode === 8) { // Backspace
|
||||
if (diff > 0) {
|
||||
word.delete(pos, diff);
|
||||
r.left = pos;
|
||||
r.right = pos;
|
||||
writeRange(r);
|
||||
} else {
|
||||
if (event.ctrlKey != null && event.ctrlKey) {
|
||||
var val = word.toString();
|
||||
var newPos = pos;
|
||||
var delLength = 0;
|
||||
if (pos > 0) {
|
||||
newPos--;
|
||||
delLength++;
|
||||
}
|
||||
while (newPos > 0 && val[newPos] !== " " && val[newPos] !== "\n") {
|
||||
newPos--;
|
||||
delLength++;
|
||||
}
|
||||
word.delete(newPos, pos - newPos);
|
||||
r.left = newPos;
|
||||
r.right = newPos;
|
||||
writeRange(r);
|
||||
} else {
|
||||
if (pos > 0) {
|
||||
word.delete(pos - 1, 1);
|
||||
r.left = pos - 1;
|
||||
r.right = pos - 1;
|
||||
writeRange(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
creatorToken = false;
|
||||
return false;
|
||||
} else if (event.keyCode != null && event.keyCode === 46) { // Delete
|
||||
if (diff > 0) {
|
||||
word.delete(pos, diff);
|
||||
r.left = pos;
|
||||
r.right = pos;
|
||||
writeRange(r);
|
||||
} else {
|
||||
word.delete(pos, 1);
|
||||
r.left = pos;
|
||||
r.right = pos;
|
||||
writeRange(r);
|
||||
}
|
||||
event.preventDefault();
|
||||
creatorToken = false;
|
||||
return false;
|
||||
} else {
|
||||
creatorToken = false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
event.preventDefault()
|
||||
creatorToken = false
|
||||
return false
|
||||
} else {
|
||||
creatorToken = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Y.TextBind = new CustomType({
|
||||
class: YTextBind,
|
||||
createType: function* YTextBindCreator () {
|
||||
createType: function * YTextBindCreator () {
|
||||
var model = {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "List",
|
||||
type: "TextBind",
|
||||
struct: 'List',
|
||||
type: 'TextBind',
|
||||
id: this.store.getNextOpId()
|
||||
};
|
||||
yield* this.applyCreatedOperations([model]);
|
||||
return yield* this.createType(model);
|
||||
}
|
||||
yield* this.applyCreatedOperations([model])
|
||||
return yield* this.createType(model)
|
||||
},
|
||||
initType: function* YTextBindInitializer(os, model){
|
||||
var valArray = [];
|
||||
var idArray = yield* Y.Struct.List.map.call(this, model, function(c){
|
||||
valArray.push(c.content);
|
||||
return JSON.stringify(c.id);
|
||||
});
|
||||
return new YTextBind(os, model.id, idArray, valArray);
|
||||
initType: function * YTextBindInitializer (os, model) {
|
||||
var valArray = []
|
||||
var idArray = yield* Y.Struct.List.map.call(this, model, function (c) {
|
||||
valArray.push(c.content)
|
||||
return JSON.stringify(c.id)
|
||||
})
|
||||
return new YTextBind(os, model.id, idArray, valArray)
|
||||
}
|
||||
});
|
||||
})();
|
||||
})
|
||||
})()
|
||||
|
85
src/Utils.js
85
src/Utils.js
@ -1,100 +1,101 @@
|
||||
/* global copyObject, compareIds */
|
||||
|
||||
var GeneratorFunction = (function*(){}).constructor;//eslint-disable-line
|
||||
var GeneratorFunction = (function *() {}).constructor;// eslint-disable-line
|
||||
|
||||
class EventHandler { //eslint-disable-line
|
||||
class EventHandler { // eslint-disable-line
|
||||
constructor (onevent) {
|
||||
this.waiting = [];
|
||||
this.awaiting = 0;
|
||||
this.onevent = onevent;
|
||||
this.userEventListeners = [];
|
||||
this.waiting = []
|
||||
this.awaiting = 0
|
||||
this.onevent = onevent
|
||||
this.userEventListeners = []
|
||||
}
|
||||
receivedOp (op) {
|
||||
if (this.awaiting <= 0) {
|
||||
this.onevent([op]);
|
||||
this.onevent([op])
|
||||
} else {
|
||||
this.waiting.push(copyObject(op));
|
||||
this.waiting.push(copyObject(op))
|
||||
}
|
||||
}
|
||||
awaitAndPrematurelyCall (ops) {
|
||||
this.awaiting++;
|
||||
this.onevent(ops);
|
||||
this.awaiting++
|
||||
this.onevent(ops)
|
||||
}
|
||||
addUserEventListener (f) {
|
||||
this.userEventListeners.push(f);
|
||||
this.userEventListeners.push(f)
|
||||
}
|
||||
removeUserEventListener (f) {
|
||||
this.userEventListeners = this.userEventListeners.filter(function(g){
|
||||
return f !== g;
|
||||
});
|
||||
this.userEventListeners = this.userEventListeners.filter(function (g) {
|
||||
return f !== g
|
||||
})
|
||||
}
|
||||
removeAllUserEventListeners () {
|
||||
this.userEventListeners = [];
|
||||
this.userEventListeners = []
|
||||
}
|
||||
callUserEventListeners (event) {
|
||||
for (var i in this.userEventListeners) {
|
||||
try {
|
||||
this.userEventListeners[i](event);
|
||||
this.userEventListeners[i](event)
|
||||
} catch (e) {
|
||||
console.log("User events must not throw Errors!");//eslint-disable-line
|
||||
console.log('User events must not throw Errors!');// eslint-disable-line
|
||||
}
|
||||
}
|
||||
}
|
||||
awaitedLastInserts (n) {
|
||||
var ops = this.waiting.splice(this.waiting.length - n);
|
||||
var ops = this.waiting.splice(this.waiting.length - n)
|
||||
for (var oid = 0; oid < ops.length; oid++) {
|
||||
var op = ops[oid];
|
||||
var op = ops[oid]
|
||||
for (var i = this.waiting.length - 1; i >= 0; i--) {
|
||||
let w = this.waiting[i];
|
||||
let w = this.waiting[i]
|
||||
if (compareIds(op.left, w.id)) {
|
||||
// include the effect of op in w
|
||||
w.right = op.id;
|
||||
w.right = op.id
|
||||
// exclude the effect of w in op
|
||||
op.left = w.left;
|
||||
op.left = w.left
|
||||
} else if (compareIds(op.right, w.id)) {
|
||||
// similar..
|
||||
w.left = op.id;
|
||||
op.right = w.right;
|
||||
w.left = op.id
|
||||
op.right = w.right
|
||||
}
|
||||
}
|
||||
}
|
||||
this.tryCallEvents();
|
||||
this.tryCallEvents()
|
||||
}
|
||||
awaitedLastDeletes (n, newLeft) {
|
||||
var ops = this.waiting.splice(this.waiting.length - n);
|
||||
var ops = this.waiting.splice(this.waiting.length - n)
|
||||
for (var j in ops) {
|
||||
var del = ops[j];
|
||||
var del = ops[j]
|
||||
if (newLeft != null) {
|
||||
for (var i in this.waiting) {
|
||||
let w = this.waiting[i];
|
||||
let w = this.waiting[i]
|
||||
// We will just care about w.left
|
||||
if (compareIds(del.target, w.left)) {
|
||||
del.left = newLeft;
|
||||
del.left = newLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.tryCallEvents();
|
||||
this.tryCallEvents()
|
||||
}
|
||||
tryCallEvents () {
|
||||
this.awaiting--;
|
||||
this.awaiting--
|
||||
if (this.awaiting <= 0 && this.waiting.length > 0) {
|
||||
var events = this.waiting;
|
||||
this.waiting = [];
|
||||
this.onevent(events);
|
||||
var events = this.waiting
|
||||
this.waiting = []
|
||||
this.onevent(events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CustomType { //eslint-disable-line
|
||||
class CustomType { // eslint-disable-line
|
||||
constructor (def) {
|
||||
if (def.createType == null
|
||||
|| def.initType == null
|
||||
|| def.class == null
|
||||
if (def.createType == null ||
|
||||
def.initType == null ||
|
||||
def.class == null
|
||||
) {
|
||||
throw new Error("Custom type was not initialized correctly!");
|
||||
throw new Error('Custom type was not initialized correctly!')
|
||||
}
|
||||
this.createType = def.createType;
|
||||
this.initType = def.initType;
|
||||
this.class = def.class;
|
||||
this.createType = def.createType
|
||||
this.initType = def.initType
|
||||
this.class = def.class
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user