switched to *standard* coding style

This commit is contained in:
Kevin Jahns 2015-07-21 17:14:03 +02:00
parent ee116b8ca4
commit ee983ceff6
26 changed files with 2067 additions and 2204 deletions

View File

@ -1,4 +0,0 @@
build/
y.js
y.js.map
interfaces/

View File

@ -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
}
}

View File

@ -1,13 +0,0 @@
[ignore]
.*/node_modules/.*
.*/build/.*
./y.js
./y.js.map
[include]
[libs]
./interfaces
./src
[options]

View File

@ -1,29 +1,30 @@
/* global Y */
Y({ Y({
db: { db: {
name: "Memory" name: 'Memory'
}, },
connector: { connector: {
name: "WebRTC", name: 'WebRTC',
room: "mineeeeeee", room: 'mineeeeeee',
debug: true debug: true
} }
}).then(function(yconfig){ }).then(function (yconfig) {
window.y = yconfig.root; window.y = yconfig.root
window.yconfig = yconfig; window.yconfig = yconfig
var textarea = document.getElementById("textfield"); var textarea = document.getElementById('textfield')
var contenteditable = document.getElementById("contenteditable"); var contenteditable = document.getElementById('contenteditable')
yconfig.root.observe(function(events){ yconfig.root.observe(function (events) {
for (var e in events) { for (var e in events) {
var event = events[e]; var event = events[e]
if (event.name === "text" && (event.type === "add" || event.type === "update")) { if (event.name === 'text' && (event.type === 'add' || event.type === 'update')) {
event.object.get(event.name).then(function(text){ //eslint-disable-line event.object.get(event.name).then(function (text) { // eslint-disable-line
text.bind(textarea); text.bind(textarea)
text.bind(contenteditable); text.bind(contenteditable)
window.ytext = text; window.ytext = text
}); })
} }
} }
}); })
yconfig.root.set("text", Y.TextBind); yconfig.root.set('text', Y.TextBind)
}); })

View File

@ -1,4 +1,4 @@
/*eslint-env node */ /* eslint-env node */
/** Gulp Commands /** Gulp Commands
@ -38,47 +38,42 @@
Builds the test suite Builds the test suite
- test: - test:
Test this library Test this library
- lint:
Lint this library. A successful lint is required for committing to this repository!
*/ */
var gulp = require('gulp')
var gulp = require("gulp"); var sourcemaps = require('gulp-sourcemaps')
var sourcemaps = require("gulp-sourcemaps"); var babel = require('gulp-babel')
var babel = require("gulp-babel"); var uglify = require('gulp-uglify')
var uglify = require("gulp-uglify"); var minimist = require('minimist')
var minimist = require("minimist"); var jasmine = require('gulp-jasmine')
var eslint = require("gulp-eslint"); var jasmineBrowser = require('gulp-jasmine-browser')
var jasmine = require("gulp-jasmine"); var concat = require('gulp-concat')
var jasmineBrowser = require("gulp-jasmine-browser"); var watch = require('gulp-watch')
var concat = require("gulp-concat");
var watch = require("gulp-watch");
var polyfills = [ 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), { var options = minimist(process.argv.slice(2), {
string: ["export", "name", "testport", "testfiles"], string: ['export', 'name', 'testport', 'testfiles'],
default: { default: {
export: "ignore", export: 'ignore',
name: "y.js", name: 'y.js',
testport: "8888", testport: '8888',
testfiles: "src/**/*.js" testfiles: 'src/**/*.js'
} }
}); })
var files = { var files = {
y: polyfills.concat(["src/y.js", "src/Connector.js", "src/OperationStore.js", "src/Struct.js", "src/Utils.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"]), 'src/OperationStores/RedBlackTree.js', 'src/**/*.js', '!src/**/*.spec.js']),
lint: ["src/**/*.js", "gulpfile.js"],
test: polyfills.concat([options.testfiles]), test: polyfills.concat([options.testfiles]),
build_test: ["build_test/y.js"] build_test: ['build_test/y.js']
}; }
gulp.task("build", function () { gulp.task('build', function () {
/*return gulp.src(files.y) /*
return gulp.src(files.y)
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(concat(options.name)) .pipe(concat(options.name))
.pipe(babel({ .pipe(babel({
@ -90,71 +85,62 @@ gulp.task("build", function () {
.pipe(sourcemaps.write(".")) .pipe(sourcemaps.write("."))
.pipe(gulp.dest("."));*/ .pipe(gulp.dest("."));*/
return gulp.src(files.y) return gulp.src(files.y)
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(concat(options.name)) .pipe(concat(options.name))
.pipe(babel({ .pipe(babel({
loose: "all", loose: 'all',
modules: "ignore", modules: 'ignore',
optional: ["es7.asyncFunctions"], optional: ['es7.asyncFunctions'],
blacklist: ["regenerator"], blacklist: ['regenerator'],
experimental: true experimental: true
})) }))
.pipe(sourcemaps.write()) .pipe(sourcemaps.write())
.pipe(gulp.dest(".")); .pipe(gulp.dest('.'))
}); })
gulp.task("lint", function(){ gulp.task('test', function () {
return gulp.src(files.lint)
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failOnError());
});
gulp.task("test", function () {
return gulp.src(files.test) return gulp.src(files.test)
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(concat("jasmine")) .pipe(concat('jasmine'))
.pipe(babel({ .pipe(babel({
loose: "all", loose: 'all',
optional: ["es7.asyncFunctions"], optional: ['es7.asyncFunctions'],
modules: "ignore", modules: 'ignore',
experimental: true experimental: true
})) }))
.pipe(uglify()) .pipe(uglify())
.pipe(sourcemaps.write()) .pipe(sourcemaps.write())
.pipe(gulp.dest("build")) .pipe(gulp.dest('build'))
.pipe(jasmine({ .pipe(jasmine({
verbose: true, verbose: true,
includeStuckTrace: true includeStuckTrace: true
})); }))
}); })
gulp.task("build_jasmine_browser", function(){ gulp.task('build_jasmine_browser', function () {
gulp.src(files.test) gulp.src(files.test)
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(concat("jasmine_browser.js")) .pipe(concat('jasmine_browser.js'))
.pipe(babel({ .pipe(babel({
loose: "all", loose: 'all',
modules: "ignore", modules: 'ignore',
optional: ["es7.asyncFunctions"], optional: ['es7.asyncFunctions'],
// blacklist: "regenerator", // blacklist: "regenerator",
experimental: true experimental: true
})) }))
.pipe(sourcemaps.write()) .pipe(sourcemaps.write())
.pipe(gulp.dest("build")); .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(){ return gulp.src('build/jasmine_browser.js')
.pipe(watch('build/jasmine_browser.js'))
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"))
.pipe(jasmineBrowser.specRunner()) .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'])

View File

@ -1,5 +0,0 @@
/* @flow */
declare var describe : Function;
declare var it : Function;
declare var expect : Function;

View File

@ -5,13 +5,21 @@
"main": "y.js", "main": "y.js",
"scripts": { "scripts": {
"test": "gulp test", "test": "gulp test",
"lint": "gulp lint", "lint": "standard",
"build": "gulp build" "build": "gulp build"
}, },
"pre-commit": [ "pre-commit": [
"lint", "lint",
"build" "build"
], ],
"standard": {
"parser": "babel-eslint",
"ignore": [
"build/**",
"./y.js",
"./y.js.map"
]
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/y-js/yjs.git" "url": "https://github.com/y-js/yjs.git"
@ -33,12 +41,10 @@
}, },
"homepage": "http://y-js.org", "homepage": "http://y-js.org",
"devDependencies": { "devDependencies": {
"babel-eslint": "^3.1.15", "babel-eslint": "^3.1.23",
"eslint": "^0.22.1",
"gulp": "^3.9.0", "gulp": "^3.9.0",
"gulp-babel": "^5.1.0", "gulp-babel": "^5.1.0",
"gulp-concat": "^2.5.2", "gulp-concat": "^2.5.2",
"gulp-eslint": "^0.13.2",
"gulp-jasmine": "^2.0.1", "gulp-jasmine": "^2.0.1",
"gulp-jasmine-browser": "^0.1.3", "gulp-jasmine-browser": "^0.1.3",
"gulp-sourcemaps": "^1.5.2", "gulp-sourcemaps": "^1.5.2",
@ -46,6 +52,7 @@
"gulp-util": "^3.0.5", "gulp-util": "^3.0.5",
"gulp-watch": "^4.2.4", "gulp-watch": "^4.2.4",
"minimist": "^1.1.1", "minimist": "^1.1.1",
"pre-commit": "^1.0.10" "pre-commit": "^1.0.10",
"standard": "^5.0.0-2"
} }
} }

View File

@ -1,192 +1,191 @@
class AbstractConnector { // eslint-disable-line no-unused-vars
class AbstractConnector { //eslint-disable-line no-unused-vars
/* /*
opts opts
.role : String Role of this client ("master" or "slave") .role : String Role of this client ("master" or "slave")
.userId : String that uniquely defines the user. .userId : String that uniquely defines the user.
*/ */
constructor (y, opts) { constructor (y, opts) {
this.y = y; this.y = y
if (opts == null){ if (opts == null) {
opts = {}; opts = {}
} }
if (opts.role == null || opts.role === "master") { if (opts.role == null || opts.role === 'master') {
this.role = "master"; this.role = 'master'
} else if (opts.role === "slave") { } else if (opts.role === 'slave') {
this.role = "slave"; this.role = 'slave'
} else { } 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.role = opts.role
this.connections = {}; this.connections = {}
this.userEventListeners = []; this.userEventListeners = []
this.whenSyncedListeners = []; this.whenSyncedListeners = []
this.currentSyncTarget = null; this.currentSyncTarget = null
this.syncingClients = []; this.syncingClients = []
this.forwardToSyncingClients = (opts.forwardToSyncingClients === false) ? false : true; this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
this.debug = opts.debug ? true : false; this.debug = opts.debug === true
this.broadcastedHB = false; this.broadcastedHB = false
} }
setUserId (userId) { setUserId (userId) {
this.userId = userId; this.userId = userId
this.y.db.setUserId(userId); this.y.db.setUserId(userId)
} }
onUserEvent (f) { onUserEvent (f) {
this.userEventListeners.push(f); this.userEventListeners.push(f)
} }
userLeft (user : string) { userLeft (user) {
delete this.connections[user]; delete this.connections[user]
if (user === this.currentSyncTarget){ if (user === this.currentSyncTarget) {
this.currentSyncTarget = null; this.currentSyncTarget = null
this.findNextSyncTarget(); this.findNextSyncTarget()
} }
for (var f of this.userEventListeners){ for (var f of this.userEventListeners) {
f({ f({
action: "userLeft", action: 'userLeft',
user: user user: user
}); })
} }
} }
userJoined (user, role) { userJoined (user, role) {
if(role == null){ if (role == null) {
throw new Error("You must specify the role of the joined user!"); throw new Error('You must specify the role of the joined user!')
} }
if (this.connections[user] != null) { if (this.connections[user] != null) {
throw new Error("This user already joined!"); throw new Error('This user already joined!')
} }
this.connections[user] = { this.connections[user] = {
isSynced: false, isSynced: false,
role: role role: role
}; }
for (var f of this.userEventListeners) { for (var f of this.userEventListeners) {
f({ f({
action: "userJoined", action: 'userJoined',
user: user, user: user,
role: role role: role
}); })
} }
if (this.currentSyncTarget == null) { if (this.currentSyncTarget == null) {
this.findNextSyncTarget(); this.findNextSyncTarget()
} }
} }
// Execute a function _when_ we are connected. // Execute a function _when_ we are connected.
// If not connected, wait until connected // If not connected, wait until connected
whenSynced (f) { whenSynced (f) {
if (this.isSynced === true) { if (this.isSynced === true) {
f(); f()
} else { } else {
this.whenSyncedListeners.push(f); this.whenSyncedListeners.push(f)
} }
} }
// returns false, if there is no sync target // returns false, if there is no sync target
// true otherwise // true otherwise
findNextSyncTarget () { findNextSyncTarget () {
if (this.currentSyncTarget != null) { 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) { for (var uid in this.connections) {
if (!this.connections[uid].isSynced) { if (!this.connections[uid].isSynced) {
syncUser = uid; syncUser = uid
break; break
} }
} }
if (syncUser != null){ if (syncUser != null) {
var conn = this; var conn = this
this.currentSyncTarget = syncUser; this.currentSyncTarget = syncUser
this.y.db.requestTransaction(function*(){ this.y.db.requestTransaction(function *() {
conn.send(syncUser, { conn.send(syncUser, {
type: "sync step 1", type: 'sync step 1',
stateVector: yield* this.getStateVector() stateVector: yield* this.getStateVector()
}); })
}); })
} }
// set the state to synced! // set the state to synced!
if (!this.isSynced) { if (!this.isSynced) {
this.isSynced = true; this.isSynced = true
for (var f of this.whenSyncedListeners) { for (var f of this.whenSyncedListeners) {
f(); f()
} }
this.whenSyncedListeners = null; this.whenSyncedListeners = null
} }
} }
send (uid, message) { send (uid, message) {
if (this.debug) { if (this.debug) {
console.log(`me -> ${uid}: ${message.type}`);//eslint-disable-line console.log(`me -> ${uid}: ${message.type}`);// eslint-disable-line
console.dir(m); //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. // 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) { if (sender === this.userId) {
return; return
} }
if (this.debug) { if (this.debug) {
console.log(`${sender} -> me: ${m.type}`);//eslint-disable-line console.log(`${sender} -> me: ${m.type}`);// eslint-disable-line
console.dir(m); //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 // TODO: make transaction, stream the ops
let conn = this; let conn = this
this.y.db.requestTransaction(function*(){ this.y.db.requestTransaction(function *() {
var ops = yield* this.getOperations(m.stateVector); var ops = yield* this.getOperations(m.stateVector)
var sv = yield* this.getStateVector(); var sv = yield* this.getStateVector()
conn.send(sender, { conn.send(sender, {
type: "sync step 2", type: 'sync step 2',
os: ops, os: ops,
stateVector: sv stateVector: sv
}); })
if (this.forwardToSyncingClients) { if (this.forwardToSyncingClients) {
conn.syncingClients.push(sender); conn.syncingClients.push(sender)
setTimeout(function(){ setTimeout(function () {
conn.syncingClients = conn.syncingClients.filter(function(cli){ conn.syncingClients = conn.syncingClients.filter(function (cli) {
return cli !== sender; return cli !== sender
}); })
conn.send(sender, { conn.send(sender, {
type: "sync done" type: 'sync done'
}); })
}, conn.syncingClientDuration); }, conn.syncingClientDuration)
} else { } else {
conn.send(sender, { conn.send(sender, {
type: "sync done" type: 'sync done'
}); })
} }
}); })
} else if (m.type === "sync step 2") { } else if (m.type === 'sync step 2') {
this.y.db.apply(m.os); this.y.db.apply(m.os)
let conn = this; let conn = this
var broadcastHB = !this.broadcastedHB; var broadcastHB = !this.broadcastedHB
this.broadcastedHB = true; this.broadcastedHB = true
this.y.db.requestTransaction(function*(){ this.y.db.requestTransaction(function *() {
var ops = yield* this.getOperations(m.stateVector); var ops = yield* this.getOperations(m.stateVector)
if (ops.length > 0) { if (ops.length > 0) {
m = { m = {
type: "update", type: 'update',
ops: ops ops: ops
}; }
if (!broadcastHB) { if (!broadcastHB) {
conn.send(sender, m); conn.send(sender, m)
} else { } else {
// broadcast only once! // broadcast only once!
conn.broadcast(m); conn.broadcast(m)
} }
} }
}); })
} else if (m.type === "sync done") { } else if (m.type === 'sync done') {
this.connections[sender].isSynced = true; this.connections[sender].isSynced = true
if (sender === this.currentSyncTarget) { if (sender === this.currentSyncTarget) {
this.currentSyncTarget = null; this.currentSyncTarget = null
this.findNextSyncTarget(); this.findNextSyncTarget()
} }
} else if (m.type === "update") { } else if (m.type === 'update') {
if (this.forwardToSyncingClients) { if (this.forwardToSyncingClients) {
for (var client of this.syncingClients) { for (var client of this.syncingClients) {
this.send(client, m); this.send(client, m)
} }
} }
this.y.db.apply(m.ops); this.y.db.apply(m.ops)
} }
} }
// Currently, the HB encodes operations as JSON. For the moment I want to keep it // 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 // expects an ltx (less than xml) object
parseMessageFromXml (m) { parseMessageFromXml (m) {
function parseArray (node) { function parseArray (node) {
for (var n of node.children){ for (var n of node.children) {
if (n.getAttribute("isArray") === "true") { if (n.getAttribute('isArray') === 'true') {
return parseArray(n); return parseArray(n)
} else { } else {
return parseObject(n); return parseObject(n)
} }
} }
} }
function parseObject (node) { function parseObject (node) {
var json = {}; var json = {}
for (var attrName in node.attrs) { for (var attrName in node.attrs) {
var value = node.attrs[attrName]; var value = node.attrs[attrName]
var int = parseInt(value); var int = parseInt(value, 10)
if (isNaN(int) || ("" + int) !== value){ if (isNaN(int) || ('' + int) !== value) {
json[attrName] = value; json[attrName] = value
} else { } else {
json[attrName] = int; json[attrName] = int
} }
} }
for (var n in node.children){ for (var n in node.children) {
var name = n.name; var name = n.name
if (n.getAttribute("isArray") === "true") { if (n.getAttribute('isArray') === 'true') {
json[name] = parseArray(n); json[name] = parseArray(n)
} else { } else {
json[name] = parseObject(n); json[name] = parseObject(n)
} }
} }
return json; return json
} }
parseObject(m); parseObject(m)
} }
// encode message in xml // encode message in xml
// we use string because Strophe only accepts an "xml-string".. // 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 // attributes is optional
function encodeObject (m, json) { function encodeObject (m, json) {
for (var name in json) { for (var name in json) {
var value = json[name]; var value = json[name]
if (name == null) { if (name == null) {
// nop // nop
} else if (value.constructor === Object) { } else if (value.constructor === Object) {
encodeObject(m.c(name), value); encodeObject(m.c(name), value)
} else if (value.constructor === Array) { } else if (value.constructor === Array) {
encodeArray(m.c(name), value); encodeArray(m.c(name), value)
} else { } else {
m.setAttribute(name, value); m.setAttribute(name, value)
} }
} }
} }
function encodeArray (m, array) { function encodeArray (m, array) {
m.setAttribute("isArray", "true"); m.setAttribute('isArray', 'true')
for (var e of array) { for (var e of array) {
if (e.constructor === Object) { if (e.constructor === Object) {
encodeObject(m.c("array-element"), e); encodeObject(m.c('array-element'), e)
} else { } else {
encodeArray(m.c("array-element"), e); encodeArray(m.c('array-element'), e)
} }
} }
} }
if (obj.constructor === Object) { 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) { } 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 { } else {
throw new Error("I can't encode this json!"); throw new Error("I can't encode this json!")
} }
} }
} }

View File

@ -1,102 +1,104 @@
/* global getRandom, AbstractConnector, Y, wait */
var globalRoom = { var globalRoom = {
users: {}, users: {},
buffers: {}, buffers: {},
removeUser: function(user : AbstractConnector){ removeUser: function (user) {
for (var i in this.users) { for (var i in this.users) {
this.users[i].userLeft(user); this.users[i].userLeft(user)
} }
delete this.users[user]; delete this.users[user]
delete this.buffers[user]; delete this.buffers[user]
}, },
addUser: function(connector){ addUser: function (connector) {
this.users[connector.userId] = connector; this.users[connector.userId] = connector
this.buffers[connector.userId] = []; this.buffers[connector.userId] = []
for (var uname in this.users) { for (var uname in this.users) {
if (uname !== connector.userId) { if (uname !== connector.userId) {
var u = this.users[uname]; var u = this.users[uname]
u.userJoined(connector.userId, "master"); u.userJoined(connector.userId, 'master')
connector.userJoined(u.userId, "master"); connector.userJoined(u.userId, 'master')
} }
} }
} }
}; }
function flushOne(){ function flushOne () {
var bufs = []; var bufs = []
for (var i in globalRoom.buffers) { for (var i in globalRoom.buffers) {
if (globalRoom.buffers[i].length > 0) { if (globalRoom.buffers[i].length > 0) {
bufs.push(i); bufs.push(i)
} }
} }
if (bufs.length > 0) { if (bufs.length > 0) {
var userId = getRandom(bufs); var userId = getRandom(bufs)
var m = globalRoom.buffers[userId].shift(); var m = globalRoom.buffers[userId].shift()
var user = globalRoom.users[userId]; var user = globalRoom.users[userId]
user.receiveMessage(m[0], m[1]); user.receiveMessage(m[0], m[1])
return true; return true
} else { } else {
return false; return false
} }
} }
// setInterval(flushOne, 10); // setInterval(flushOne, 10)
var userIdCounter = 0; var userIdCounter = 0
class Test extends AbstractConnector { class Test extends AbstractConnector {
constructor (y, options) { constructor (y, options) {
if(options === undefined){ if (options === undefined) {
throw new Error("Options must not be undefined!"); throw new Error('Options must not be undefined!')
} }
options.role = "master"; options.role = 'master'
options.forwardToSyncingClients = false; options.forwardToSyncingClients = false
super(y, options); super(y, options)
this.setUserId((userIdCounter++) + ""); this.setUserId((userIdCounter++) + '')
globalRoom.addUser(this); globalRoom.addUser(this)
this.globalRoom = globalRoom; this.globalRoom = globalRoom
} }
send (userId, message) { 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) { broadcast (message) {
for (var key in globalRoom.buffers) { 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 () { disconnect () {
globalRoom.removeUser(this.userId); globalRoom.removeUser(this.userId)
} }
flush() { flush () {
var buff = globalRoom.buffers[this.userId]; var buff = globalRoom.buffers[this.userId]
while (buff.length > 0) { while (buff.length > 0) {
var m = buff.shift(); var m = buff.shift()
this.receiveMessage(m[0], m[1]); this.receiveMessage(m[0], m[1])
} }
} }
flushAll () { flushAll () {
var def = Promise.defer(); var def = Promise.defer()
// flushes may result in more created operations, // flushes may result in more created operations,
// flush until there is nothing more to flush // flush until there is nothing more to flush
function nextFlush() { function nextFlush () {
var c = flushOne(); var c = flushOne()
if (c) { if (c) {
while(flushOne()) { while (flushOne()) {
//nop // nop
} }
wait().then(nextFlush); wait().then(nextFlush)
} else { } else {
wait().then(function(){ wait().then(function () {
def.resolve(); def.resolve()
}); })
} }
} }
// in the case that there are // in the case that there are
// still actions that want to be performed // still actions that want to be performed
wait(0).then(nextFlush); wait(0).then(nextFlush)
return def.promise; return def.promise
} }
flushOne() { flushOne () {
flushOne(); flushOne()
} }
} }
Y.Test = Test; Y.Test = Test

View File

@ -1,86 +1,87 @@
/* global AbstractConnector, Y */
class WebRTC extends AbstractConnector { class WebRTC extends AbstractConnector {
constructor (y, options) { constructor (y, options) {
if(options === undefined){ if (options === undefined) {
throw new Error("Options must not be undefined!"); throw new Error('Options must not be undefined!')
} }
if(options.room == null) { if (options.room == null) {
throw new Error("You must define a room name!"); throw new Error('You must define a room name!')
} }
options.role = "slave"; options.role = 'slave'
super(y, options); super(y, options)
var room = options.room; var room = options.room
var webrtcOptions = { var webrtcOptions = {
url: options.url || "https://yatta.ninja:8888", url: options.url || 'https://yatta.ninja:8888',
room: options.room room: options.room
}; }
var swr = new SimpleWebRTC(webrtcOptions); //eslint-disable-line no-undef var swr = new SimpleWebRTC(webrtcOptions) // eslint-disable-line no-undef
this.swr = swr; this.swr = swr
var self = this; var self = this
swr.once("connectionReady", function(userId){ swr.once('connectionReady', function (userId) {
// SimpleWebRTC (swr) is initialized // SimpleWebRTC (swr) is initialized
swr.joinRoom(room); swr.joinRoom(room)
swr.once("joinedRoom", function(){ swr.once('joinedRoom', function () {
self.setUserId(userId); self.setUserId(userId)
/* /*
var i; var i
// notify the connector class about all the users that already // notify the connector class about all the users that already
// joined the session // joined the session
for(i in self.swr.webrtc.peers){ 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 // The client received a message
// Check if the connector is already initialized, // Check if the connector is already initialized,
// only then forward the message to the connector class // only then forward the message to the connector class
if(message.type != null ){ if (message.type != null) {
self.receiveMessage(peer.id, message.payload); self.receiveMessage(peer.id, message.payload)
} }
}); })
}); })
swr.on("createdPeer", function(peer){ swr.on('createdPeer', function (peer) {
// a new peer/client joined the session. // a new peer/client joined the session.
// Notify the connector class, if the connector // Notify the connector class, if the connector
// is already initialized // 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. // a client left the session.
// Notify the connector class, if the connector // Notify the connector class, if the connector
// is already initialized // is already initialized
self.userLeft(peer.id); self.userLeft(peer.id)
}); })
}); })
} }
send (uid, message) { send (uid, message) {
var self = this; var self = this
// we have to make sure that the message is sent under all circumstances // 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 // check if the clients still exists
var peer = self.swr.webrtc.getPeers(uid)[0]; var peer = self.swr.webrtc.getPeers(uid)[0]
var success; var success
if(peer){ if (peer) {
// success is true, if the message is successfully sent // 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 // resend the message if it didn't work
setTimeout(send, 500); setTimeout(send, 500)
} }
}; }
// try to send the message // try to send the message
send(); send()
} }
broadcast (message) { broadcast (message) {
this.swr.sendDirectlyToAll("simplewebrtc", "yjs", message); this.swr.sendDirectlyToAll('simplewebrtc', 'yjs', message)
} }
} }
Y.WebRTC = WebRTC; Y.WebRTC = WebRTC

View File

@ -1,107 +1,107 @@
/* @flow */ /* global Y */
/*eslint-env browser,jasmine */ /* 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 function wait(t = 0) {//eslint-disable-line
var def = Promise.defer(); var def = Promise.defer()
setTimeout(function(){ setTimeout(function () {
def.resolve(); def.resolve()
}, t); }, t)
return def.promise; return def.promise
} }
// returns a random element of o // returns a random element of o
// works on Object, and Array // works on Object, and Array
function getRandom (o) { function getRandom (o) {
if (o instanceof Array) { 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) { } else if (o.constructor === Object) {
var ks = []; var ks = []
for (var key in o) { 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 function getRandomNumber(n) {//eslint-disable-line
if (n == null) { 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 async function applyRandomTransactions (users, objects, transactions, numberOfTransactions) {//eslint-disable-line
function randomTransaction (root) { function randomTransaction (root) {
var f = getRandom(transactions); var f = getRandom(transactions)
f(root); f(root)
} }
for(var i = 0; i < numberOfTransactions; i++) { for (var i = 0; i < numberOfTransactions; i++) {
var r = Math.random(); var r = Math.random()
if (r >= 0.9) { if (r >= 0.9) {
// 10% chance to flush // 10% chance to flush
users[0].connector.flushOne(); users[0].connector.flushOne()
} else { } else {
randomTransaction(getRandom(objects)); randomTransaction(getRandom(objects))
} }
wait(); wait()
} }
} }
async function compareAllUsers(users){//eslint-disable-line async function compareAllUsers(users){//eslint-disable-line
var s1, s2; var s1, s2
var db1 = []; var db1 = []
function* t1(){ function * t1 () {
s1 = yield* this.getStateSet(); s1 = yield* this.getStateSet()
} }
function* t2(){ function * t2 () {
s2 = yield* this.getStateSet(); s2 = yield* this.getStateSet()
} }
await users[0].connector.flushAll(); await users[0].connector.flushAll()
for (var uid = 0; uid < users.length; uid++) { for (var uid = 0; uid < users.length; uid++) {
if (s1 == null) { if (s1 == null) {
var u = users[uid]; var u = users[uid]
u.db.requestTransaction(t1); u.db.requestTransaction(t1)
await wait(); await wait()
u.db.os.iterate(null, null, function(o){//eslint-disable-line u.db.os.iterate(null, null, function(o){//eslint-disable-line
db1.push(o); db1.push(o)
}); })
} else { } else {
var u2 = users[uid]; var u2 = users[uid]
u2.db.requestTransaction(t2); u2.db.requestTransaction(t2)
await wait(); await wait()
expect(s1).toEqual(s2); expect(s1).toEqual(s2)
var count = 0; var count = 0
u2.db.os.iterate(null, null, function(o){//eslint-disable-line 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 async function createUsers(self, numberOfUsers) {//eslint-disable-line
if (globalRoom.users[0] != null) {//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 for (var u in globalRoom.users) {//eslint-disable-line
globalRoom.users[u].y.destroy()//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++) { for (var i = 0; i < numberOfUsers; i++) {
promises.push(Y({ promises.push(Y({
db: { db: {
name: "Memory" name: 'Memory'
}, },
connector: { connector: {
name: "Test", name: 'Test',
debug: false debug: false
} }
})); }))
} }
self.users = await Promise.all(promises); self.users = await Promise.all(promises)
} }

View File

@ -1,58 +1,52 @@
/* @flow */ /* global Y, copyObject, Struct, RBTree */
class AbstractTransaction { //eslint-disable-line no-unused-vars
constructor (store : OperationStore) { class AbstractTransaction { // eslint-disable-line no-unused-vars
this.store = store; constructor (store) {
this.store = store
} }
*getType (id) { * getType (id) {
var sid = JSON.stringify(id); var sid = JSON.stringify(id)
var t = this.store.initializedTypes[sid]; var t = this.store.initializedTypes[sid]
if (t == null) { if (t == null) {
var op = yield* this.getOperation(id); var op = yield* this.getOperation(id)
if (op != null) { if (op != null) {
t = yield* Y[op.type].initType.call(this, this.store, op); t = yield* Y[op.type].initType.call(this, this.store, op)
this.store.initializedTypes[sid] = t; this.store.initializedTypes[sid] = t
} }
} }
return t; return t
} }
*createType (model) { * createType (model) {
var sid = JSON.stringify(model.id); var sid = JSON.stringify(model.id)
var t = yield* Y[model.type].initType.call(this, this.store, model); var t = yield* Y[model.type].initType.call(this, this.store, model)
this.store.initializedTypes[sid] = t; this.store.initializedTypes[sid] = t
return t; return t
} }
*applyCreatedOperations (ops) { * applyCreatedOperations (ops) {
var send = []; var send = []
for (var i = 0; i < ops.length; i++) { for (var i = 0; i < ops.length; i++) {
var op = ops[i]; var op = ops[i]
yield* this.store.tryExecute.call(this, op); yield* this.store.tryExecute.call(this, op)
send.push(copyObject(Struct[op.struct].encode(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({ this.store.y.connector.broadcast({
type: "update", type: 'update',
ops: send ops: send
}); })
} }
} }
} }
type Listener = { class AbstractOperationStore { // eslint-disable-line no-unused-vars
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
constructor (y) { constructor (y) {
this.y = y; this.y = y
// E.g. this.listenersById[id] : Array<Listener> // E.g. this.listenersById[id] : Array<Listener>
this.listenersById = {}; this.listenersById = {}
// Execute the next time a transaction is requested // Execute the next time a transaction is requested
this.listenersByIdExecuteNow = []; this.listenersByIdExecuteNow = []
// A transaction is requested // A transaction is requested
this.listenersByIdRequestPending = false; this.listenersByIdRequestPending = false
/* To make things more clear, the following naming conventions: /* To make things more clear, the following naming conventions:
* ls : we put this.listenersById on ls * ls : we put this.listenersById on ls
* l : Array<Listener> * l : Array<Listener>
@ -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, // TODO: Use ES7 Weak Maps. This way types that are no longer user,
// wont be kept in memory. // wont be kept in memory.
this.initializedTypes = {}; this.initializedTypes = {}
this.whenUserIdSetListener = null; this.whenUserIdSetListener = null
this.waitingOperations = new RBTree(); this.waitingOperations = new RBTree()
} }
setUserId (userId) { setUserId (userId) {
this.userId = userId; this.userId = userId
this.opClock = 0; this.opClock = 0
if (this.whenUserIdSetListener != null) { if (this.whenUserIdSetListener != null) {
this.whenUserIdSetListener(); this.whenUserIdSetListener()
this.whenUserIdSetListener = null; this.whenUserIdSetListener = null
} }
} }
whenUserIdSet (f) { whenUserIdSet (f) {
if (this.userId != null) { if (this.userId != null) {
f(); f()
} else { } else {
this.whenUserIdSetListener = f; this.whenUserIdSetListener = f
} }
} }
getNextOpId () { getNextOpId () {
if (this.userId == null) { 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) { apply (ops) {
for (var key in ops) { for (var key in ops) {
var o = ops[key]; var o = ops[key]
var required = Struct[o.struct].requiredOps(o); var required = Struct[o.struct].requiredOps(o)
this.whenOperationsExist(required, o); this.whenOperationsExist(required, o)
} }
} }
// op is executed as soon as every operation requested is available. // op is executed as soon as every operation requested is available.
// Note that Transaction can (and should) buffer requests. // Note that Transaction can (and should) buffer requests.
whenOperationsExist (ids : Array<Id>, op : Operation) { whenOperationsExist (ids, op) {
if (ids.length > 0) { if (ids.length > 0) {
let listener : Listener = { let listener = {
op: op, op: op,
missing: ids.length missing: ids.length
}; }
for (let key in ids) { for (let key in ids) {
let id = ids[key]; let id = ids[key]
let sid = JSON.stringify(id); let sid = JSON.stringify(id)
let l = this.listenersById[sid]; let l = this.listenersById[sid]
if (l == null){ if (l == null) {
l = []; l = []
this.listenersById[sid] = l; this.listenersById[sid] = l
} }
l.push(listener); l.push(listener)
} }
} else { } else {
this.listenersByIdExecuteNow.push({ this.listenersByIdExecuteNow.push({
op: op op: op
}); })
} }
if (this.listenersByIdRequestPending){ if (this.listenersByIdRequestPending) {
return; return
} }
this.listenersByIdRequestPending = true; this.listenersByIdRequestPending = true
var store = this; var store = this
this.requestTransaction(function*(){ this.requestTransaction(function *() {
var exeNow = store.listenersByIdExecuteNow; var exeNow = store.listenersByIdExecuteNow
store.listenersByIdExecuteNow = []; store.listenersByIdExecuteNow = []
var ls = store.listenersById; var ls = store.listenersById
store.listenersById = {}; store.listenersById = {}
store.listenersByIdRequestPending = false; store.listenersByIdRequestPending = false
for (let key in exeNow) { for (let key in exeNow) {
let o = exeNow[key].op; let o = exeNow[key].op
yield* store.tryExecute.call(this, o); yield* store.tryExecute.call(this, o)
} }
for (var sid in ls){ for (var sid in ls) {
var l = ls[sid]; var l = ls[sid]
var id = JSON.parse(sid); var id = JSON.parse(sid)
if ((yield* this.getOperation(id)) == null){ if ((yield* this.getOperation(id)) == null) {
store.listenersById[sid] = l; store.listenersById[sid] = l
} else { } else {
for (let key in l) { for (let key in l) {
let listener = l[key]; let listener = l[key]
let o = listener.op; let o = listener.op
if (--listener.missing === 0){ if (--listener.missing === 0) {
yield* store.tryExecute.call(this, o); yield* store.tryExecute.call(this, o)
} }
} }
} }
} }
}); })
} }
*tryExecute (op) { * tryExecute (op) {
if (op.struct === "Delete") { if (op.struct === 'Delete') {
yield* Struct.Delete.execute.call(this, op); yield* Struct.Delete.execute.call(this, op)
} else { } else {
while (op != null) { while (op != null) {
var state = yield* this.getState(op.id[0]); var state = yield* this.getState(op.id[0])
if (op.id[1] === state.clock){ if (op.id[1] === state.clock) {
state.clock++; state.clock++
yield* this.setState.call(this, state); yield* this.setState(state)
yield* Struct[op.struct].execute.call(this, op); yield* Struct[op.struct].execute.call(this, op)
yield* this.addOperation(op); yield* this.addOperation(op)
yield* this.store.operationAdded(this, op); yield* this.store.operationAdded(this, op)
// find next operation to execute // 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) { if (op != null) {
this.store.waitingOperations.delete([op.id[0], state.clock]); this.store.waitingOperations.delete([op.id[0], state.clock])
} }
} else { } else {
if (op.id[1] > state.clock) { if (op.id[1] > state.clock) {
// has to be executed at some point later // 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 // called by a transaction when an operation is added
*operationAdded (transaction, op) { * operationAdded (transaction, op) {
var sid = JSON.stringify(op.id); var sid = JSON.stringify(op.id)
var l = this.listenersById[sid]; var l = this.listenersById[sid]
delete this.listenersById[sid]; delete this.listenersById[sid]
// notify whenOperation listeners (by id) // notify whenOperation listeners (by id)
if (l != null) { if (l != null) {
for (var key in l){ for (var key in l) {
var listener = l[key]; var listener = l[key]
if (--listener.missing === 0){ if (--listener.missing === 0) {
this.whenOperationsExist([], listener.op); this.whenOperationsExist([], listener.op)
} }
} }
} }
// notify parent, if it has been initialized as a custom type // 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) { if (t != null) {
yield* t._changed(transaction, copyObject(op)); yield* t._changed(transaction, copyObject(op))
} }
} }
removeParentListener (id, f) { removeParentListener (id, f) {
var ls = this.parentListeners[id]; var ls = this.parentListeners[id]
if (ls != null) { if (ls != null) {
this.parentListeners[id] = ls.filter(function(g){ this.parentListeners[id] = ls.filter(function (g) {
return (f !== g); return (f !== g)
}); })
} }
} }
addParentListener (id, f) { addParentListener (id, f) {
var ls = this.parentListeners[JSON.stringify(id)]; var ls = this.parentListeners[JSON.stringify(id)]
if (ls == null) { if (ls == null) {
ls = []; ls = []
this.parentListeners[JSON.stringify(id)] = ls; this.parentListeners[JSON.stringify(id)] = ls
} }
ls.push(f); ls.push(f)
} }
} }

View File

@ -1,5 +0,0 @@
/* @flow */
/*eslint-env browser,jasmine,console */
describe("OperationStore", function() {
});

View File

@ -1,208 +1,179 @@
Y.IndexedDB = (function () { // eslint-disable-line
type State = { class Transaction extends AbstractTransaction { // eslint-disable-line
user: string, constructor (store) {
clock: number super(store)
}; this.transaction = store.db.transaction(['OperationStore', 'StateVector'], 'readwrite')
this.sv = this.transaction.objectStore('StateVector')
type StateVector = Array<State>; this.os = this.transaction.objectStore('OperationStore')
this.buffer = {}
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 = {};
} }
*setOperation (op) { * setOperation (op) {
yield this.os.put(op); yield this.os.put(op)
this.buffer[JSON.stringify(op.id)] = op; this.buffer[JSON.stringify(op.id)] = op
return op; return op
} }
*getOperation (id) { * getOperation (id) {
var op = this.buffer[JSON.stringify(id)]; var op = this.buffer[JSON.stringify(id)]
if (op == null) { if (op == null) {
op = yield this.os.get(id); op = yield this.os.get(id)
this.buffer[JSON.stringify(id)] = op; this.buffer[JSON.stringify(id)] = op
} }
return op; return op
} }
*removeOperation (id) { * removeOperation (id) {
this.buffer[JSON.stringify(id)] = null; this.buffer[JSON.stringify(id)] = null
return yield this.os.delete(id); return yield this.os.delete(id)
} }
*setState (state : State) : State { * setState (state) {
return yield this.sv.put(state); return yield this.sv.put(state)
} }
*getState (user : string) : State { * getState (user) {
var state; var state
if ((state = yield this.sv.get(user)) != null){ if ((state = yield this.sv.get(user)) != null) {
return state; return state
} else { } else {
return { return {
user: user, user: user,
clock: 0 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 class OperationStore extends AbstractOperationStore { // eslint-disable-line no-undef
namespace: string;
ready: Promise;
whenReadyListeners: Array<Function>;
constructor (y, opts) { constructor (y, opts) {
super(y); super(y)
if (opts == null) { if (opts == null) {
opts = {}; opts = {}
} }
if (opts.namespace == null || typeof opts.namespace !== "string") { if (opts.namespace == null || typeof opts.namespace !== 'string') {
throw new Error("IndexedDB: expect a string (opts.namespace)!"); throw new Error('IndexedDB: expect a string (opts.namespace)!')
} else { } else {
this.namespace = opts.namespace; this.namespace = opts.namespace
} }
if (opts.idbVersion != null) { if (opts.idbVersion != null) {
this.idbVersion = opts.idbVersion; this.idbVersion = opts.idbVersion
} else { } else {
this.idbVersion = 5; this.idbVersion = 5
} }
this.transactionQueue = { this.transactionQueue = {
queue: [], queue: [],
onRequest: null onRequest: null
}; }
var store = this; var store = this
var tGen = (function *transactionGen(){ var tGen = (function * transactionGen () {
store.db = yield indexedDB.open(opts.namespace, store.idbVersion); store.db = yield window.indexedDB.open(opts.namespace, store.idbVersion)
var transactionQueue = store.transactionQueue; var transactionQueue = store.transactionQueue
var transaction = null; var transaction = null
var cont = true; var cont = true
while (cont) { while (cont) {
var request = yield transactionQueue; var request = yield transactionQueue
transaction = new Transaction(store); transaction = new Transaction(store)
yield* request.call(transaction, request);/* yield* request.call(transaction, request) /*
while (transactionQueue.queue.length > 0) { 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 function handleTransactions (t) { // eslint-disable-line no-unused-vars
var request = t.value; var request = t.value
if (t.done){ if (t.done) {
return; return
} else if (request.constructor === IDBRequest } else if (request.constructor === window.IDBRequest || request.constructor === window.IDBCursor) {
|| request.constructor === IDBCursor ) { request.onsuccess = function () {
request.onsuccess = function(){ handleTransactions(tGen.next(request.result))
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()));
};
} }
} else if ( request.constructor === IDBOpenDBRequest ) { request.onerror = function (err) {
request.onsuccess = function(event){ tGen.throw(err)
var db = event.target.result; }
handleTransactions(tGen.next(db)); } else if (request === store.transactionQueue) {
}; if (request.queue.length > 0) {
request.onerror = function(){ handleTransactions(tGen.next(request.queue.shift()))
tGen.throw("Couldn't open IndexedDB database!"); } else {
}; request.onRequest = function () {
request.onupgradeneeded = function(event){ request.onRequest = null
var db = event.target.result; handleTransactions(tGen.next(request.queue.shift()))
try {
db.createObjectStore("OperationStore", {keyPath: "id"});
db.createObjectStore("StateVector", {keyPath: "user"});
} catch (e) {
// console.log("Store already exists!");
} }
}; }
} 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 { } 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) { requestTransaction (makeGen) {
this.transactionQueue.queue.push(makeGen); this.transactionQueue.queue.push(makeGen)
if (this.transactionQueue.onRequest != null) { if (this.transactionQueue.onRequest != null) {
this.transactionQueue.onRequest(); this.transactionQueue.onRequest()
} }
} }
*removeDatabase () { * removeDatabase () {
this.db.close(); this.db.close()
yield indexedDB.deleteDatabase(this.namespace); yield window.indexedDB.deleteDatabase(this.namespace)
} }
} }
return OperationStore; return OperationStore
})(); })()

View File

@ -1,117 +1,117 @@
/* @flow */ /* global Y */
/*eslint-env browser,jasmine */ /* eslint-env browser,jasmine */
if(typeof window !== "undefined"){ if (typeof window !== 'undefined') {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000
describe("IndexedDB", function() { describe('IndexedDB', function () {
var ob; var ob
beforeAll(function(){ beforeAll(function () {
ob = new Y.IndexedDB(null, {namespace: "Test"}); ob = new Y.IndexedDB(null, {namespace: 'Test'})
}); })
it("can add and get operation", function(done) { it('can add and get operation', function (done) {
ob.requestTransaction(function*(){ ob.requestTransaction(function *() {
var op = yield* this.setOperation({ var op = yield* this.setOperation({
"id": ["1", 0], 'id': ['1', 0],
"stuff": true 'stuff': true
}); })
expect(yield* this.getOperation(["1", 0])) expect(yield* this.getOperation(['1', 0]))
.toEqual(op); .toEqual(op)
done(); done()
}); })
}); })
it("can remove operation", function(done) { it('can remove operation', function (done) {
ob.requestTransaction(function*(){ ob.requestTransaction(function *() {
var op = yield* this.setOperation({ var op = yield* this.setOperation({
"id": ["1", 0], 'id': ['1', 0],
"stuff": true 'stuff': true
}); })
expect(yield* this.getOperation(["1", 0])) expect(yield* this.getOperation(['1', 0]))
.toEqual(op); .toEqual(op)
yield* this.removeOperation(["1", 0]); yield* this.removeOperation(['1', 0])
expect(yield* this.getOperation(["1", 0])) expect(yield* this.getOperation(['1', 0]))
.toBeUndefined(); .toBeUndefined()
done(); done()
}); })
}); })
it("getOperation(op) returns undefined if op does not exist", function(done){ it('getOperation(op) returns undefined if op does not exist', function (done) {
ob.requestTransaction(function*(){ ob.requestTransaction(function *() {
var op = yield* this.getOperation("plzDon'tBeThere"); var op = yield* this.getOperation("plzDon'tBeThere")
expect(op).toBeUndefined(); expect(op).toBeUndefined()
done(); done()
}); })
}); })
it("yield throws if request is unknown", function(done){ it('yield throws if request is unknown', function (done) {
ob.requestTransaction(function*(){ ob.requestTransaction(function *() {
try { try {
yield* this.setOperation(); yield* this.setOperation()
} catch (e) { } catch (e) {
expect(true).toEqual(true); expect(true).toEqual(true)
done(); done()
return; return
} }
expect("Expected an Error!").toEqual(true); expect('Expected an Error!').toEqual(true)
done(); done()
}); })
}); })
it("sets and gets stateVector", function(done){ it('sets and gets stateVector', function (done) {
ob.requestTransaction(function*(){ ob.requestTransaction(function *() {
var s1 = {user: "1", clock: 1}; var s1 = {user: '1', clock: 1}
var s2 = {user: "2", clock: 3}; var s2 = {user: '2', clock: 3}
yield* this.setState(s1); yield* this.setState(s1)
yield* this.setState(s2); yield* this.setState(s2)
var sv = yield* this.getStateVector(); var sv = yield* this.getStateVector()
expect(sv).not.toBeUndefined(); expect(sv).not.toBeUndefined()
expect(sv).toEqual([s1, s2]); expect(sv).toEqual([s1, s2])
done(); done()
}); })
}); })
it("gets stateSet", function(done){ it('gets stateSet', function (done) {
ob.requestTransaction(function*(){ ob.requestTransaction(function *() {
var s1 = {user: "1", clock: 1}; var s1 = {user: '1', clock: 1}
var s2 = {user: "2", clock: 3}; var s2 = {user: '2', clock: 3}
yield* this.setState(s1); yield* this.setState(s1)
yield* this.setState(s2); yield* this.setState(s2)
var sv = yield* this.getStateSet(); var sv = yield* this.getStateSet()
expect(sv).not.toBeUndefined(); expect(sv).not.toBeUndefined()
expect(sv).toEqual({ expect(sv).toEqual({
"1": 1, '1': 1,
"2": 3 '2': 3
}); })
done(); done()
}); })
}); })
it("getOperations returns operations (no parameters)", function(done){ it('getOperations returns operations (no parameters)', function (done) {
ob.requestTransaction(function*(){ ob.requestTransaction(function *() {
var s1 = {user: "1", clock: 55}; var s1 = {user: '1', clock: 55}
yield* this.setState(s1); yield* this.setState(s1)
var op1 = yield* this.setOperation({ var op1 = yield* this.setOperation({
"id": ["1", 0], 'id': ['1', 0],
"stuff": true 'stuff': true
}); })
var op2 = yield* this.setOperation({ var op2 = yield* this.setOperation({
"id": ["1", 3], 'id': ['1', 3],
"stuff": true 'stuff': true
}); })
var ops = yield* this.getOperations(); var ops = yield* this.getOperations()
expect(ops.length).toBeGreaterThan(1); expect(ops.length).toBeGreaterThan(1)
expect(ops[0]).toEqual(op1); expect(ops[0]).toEqual(op1)
expect(ops[1]).toEqual(op2); expect(ops[1]).toEqual(op2)
done(); done()
}); })
}); })
afterAll(function(done){ afterAll(function (done) {
ob.requestTransaction(function*(){ ob.requestTransaction(function *() {
yield* ob.removeDatabase(); yield* ob.removeDatabase()
ob = null; ob = null
done(); done()
}); })
}); })
}); })
} }

View File

@ -1,155 +1,144 @@
/* global Struct, RBTree, Y */
type State = {
user: string,
clock: number
};
function copyObject (o) { function copyObject (o) {
var c = {}; var c = {}
for (var key in o) { for (var key in o) {
c[key] = o[key]; c[key] = o[key]
} }
return c; return c
} }
type StateVector = Array<State>; Y.Memory = (function () { // eslint-disable-line no-unused-vars
type StateSet = Object; class Transaction extends AbstractTransaction { // eslint-disable-line
Y.Memory = (function(){ //eslint-disable-line no-unused-vars constructor (store) {
class Transaction extends AbstractTransaction { //eslint-disable-line super(store)
ss: StateSet; this.ss = store.ss
os: RBTree; this.os = store.os
store: OperationStore;
constructor (store : OperationStore) {
super(store);
this.ss = store.ss;
this.os = store.os;
} }
*setOperation (op) { * setOperation (op) { // eslint-disable-line
// TODO: you can remove this step! probs.. // TODO: you can remove this step! probs..
var n = this.os.findNode(op.id); var n = this.os.findNode(op.id)
n.val = op; n.val = op
return op; return op
} }
*addOperation (op) { * addOperation (op) { // eslint-disable-line
this.os.add(op); this.os.add(op)
} }
*getOperation (id) { * getOperation (id) { // eslint-disable-line
if (id == null) { 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) { * removeOperation (id) { // eslint-disable-line
this.os.delete(id); this.os.delete(id)
} }
*setState (state : State) : State { * setState (state) { // eslint-disable-line
this.ss[state.user] = state.clock; this.ss[state.user] = state.clock
} }
*getState (user : string) : State { * getState (user) { // eslint-disable-line
var clock = this.ss[user]; var clock = this.ss[user]
if (clock == null){ if (clock == null) {
clock = 0; clock = 0
} }
return { return {
user: user, user: user,
clock: clock clock: clock
}; }
} }
*getStateVector () : StateVector { * getStateVector () { // eslint-disable-line
var stateVector = []; var stateVector = []
for (var user in this.ss) { for (var user in this.ss) {
var clock = this.ss[user]; var clock = this.ss[user]
stateVector.push({ stateVector.push({
user: user, user: user,
clock: clock clock: clock
}); })
} }
return stateVector; return stateVector
} }
*getStateSet () : StateSet { * getStateSet () { // eslint-disable-line
return this.ss; return this.ss
} }
*getOperations (startSS : StateSet) { * getOperations (startSS) {
// TODO: use bounds here! // TODO: use bounds here!
if (startSS == null){ if (startSS == null) {
startSS = {}; startSS = {}
} }
var ops = []; var ops = []
var endSV : StateVector = yield* this.getStateVector(); var endSV = yield* this.getStateVector()
for (var endState of endSV) { for (var endState of endSV) {
var user = endState.user; var user = endState.user
if (user === "_") { if (user === '_') {
continue; continue
} }
var startPos = startSS[user] || 0; var startPos = startSS[user] || 0
var endPos = endState.clock; var endPos = endState.clock
this.os.iterate([user, startPos], [user, endPos], function(op){//eslint-disable-line this.os.iterate([user, startPos], [user, endPos], function (op) {// eslint-disable-line
ops.push(Struct[op.struct].encode(op)); ops.push(Struct[op.struct].encode(op))
}); })
} }
var res = []; var res = []
for (var op of ops) { 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) // instead of ss, you could use currSS (a ss that increments when you add an operation)
var clock; var clock
var o = op; var o = op
while (o.right != null){ while (o.right != null) {
// while unknown, go to the right // while unknown, go to the right
clock = ss[o.right[0]]; clock = ss[o.right[0]]
if (clock != null && o.right[1] < clock) { 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 = copyObject(op)
op.right = o.right; op.right = o.right
return op; return op
} }
} }
class OperationStore extends AbstractOperationStore { //eslint-disable-line no-undef class OperationStore extends AbstractOperationStore { // eslint-disable-line no-undef
constructor (y) { constructor (y) {
super(y); super(y)
this.os = new RBTree(); this.os = new RBTree()
this.ss = {}; this.ss = {}
this.waitingTransactions = []; this.waitingTransactions = []
this.transactionInProgress = false; this.transactionInProgress = false
} }
requestTransaction (_makeGen : Function) { requestTransaction (_makeGen) {
if (!this.transactionInProgress) { if (!this.transactionInProgress) {
this.transactionInProgress = true; this.transactionInProgress = true
setTimeout(() => { setTimeout(() => {
var makeGen = _makeGen; var makeGen = _makeGen
while (makeGen != null) { while (makeGen != null) {
var t = new Transaction(this); var t = new Transaction(this)
var gen = makeGen.call(t); var gen = makeGen.call(t)
var res = gen.next(); var res = gen.next()
while(!res.done){ while (!res.done) {
if (res.value === "transaction") { if (res.value === 'transaction') {
res = gen.next(t); res = gen.next(t)
} else { } 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; this.transactionInProgress = false
}, 0); }, 0)
} else { } else {
this.waitingTransactions.push(_makeGen); this.waitingTransactions.push(_makeGen)
} }
} }
*removeDatabase () { * removeDatabase () { // eslint-disable-line
delete this.os; delete this.os
} }
} }
return OperationStore; return OperationStore
})(); })()

View File

@ -1,367 +1,367 @@
/* global compareIds */
function smaller (a, b) { 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 { class N {
// A created node is always red! // A created node is always red!
constructor (val) { constructor (val) {
this.val = val; this.val = val
this.color = true; this.color = true
this._left = null; this._left = null
this._right = null; this._right = null
this._parent = null; this._parent = null
if (val.id === null) { if (val.id === null) {
throw new Error("You must define id!"); throw new Error('You must define id!')
} }
} }
isRed () { return this.color; } isRed () { return this.color }
isBlack () { return !this.color; } isBlack () { return !this.color }
redden () { this.color = true; return this; } redden () { this.color = true; return this }
blacken () { this.color = false; return this; } blacken () { this.color = false; return this }
get grandparent () { get grandparent () {
return this.parent.parent; return this.parent.parent
} }
get parent () { get parent () {
return this._parent; return this._parent
} }
get sibling () { get sibling () {
return (this === this.parent.left) ? return (this === this.parent.left) ?
this.parent.right : this.parent.left; this.parent.right : this.parent.left
} }
get left () { get left () {
return this._left; return this._left
} }
get right () { get right () {
return this._right; return this._right
} }
set left (n) { set left (n) {
if (n !== null) { if (n !== null) {
n._parent = this; n._parent = this
} }
this._left = n; this._left = n
} }
set right (n) { set right (n) {
if (n !== null) { if (n !== null) {
n._parent = this; n._parent = this
} }
this._right = n; this._right = n
} }
rotateLeft (tree) { rotateLeft (tree) {
var parent = this.parent; var parent = this.parent
var newParent = this.right; var newParent = this.right
var newRight = this.right.left; var newRight = this.right.left
newParent.left = this; newParent.left = this
this.right = newRight; this.right = newRight
if (parent === null) { if (parent === null) {
tree.root = newParent; tree.root = newParent
newParent._parent = null; newParent._parent = null
} else if (parent.left === this) { } else if (parent.left === this) {
parent.left = newParent; parent.left = newParent
} else if (parent.right === this) { } else if (parent.right === this) {
parent.right = newParent; parent.right = newParent
} else { } else {
throw new Error("The elements are wrongly connected!"); throw new Error('The elements are wrongly connected!')
} }
} }
next () { next () {
if ( this.right !== null ) { if (this.right !== null) {
// search the most left node in the right tree // search the most left node in the right tree
var o = this.right; var o = this.right
while (o.left !== null) { while (o.left !== null) {
o = o.left; o = o.left
} }
return o; return o
} else { } else {
var p = this; var p = this
while (p.parent !== null && p !== p.parent.left) { while (p.parent !== null && p !== p.parent.left) {
p = p.parent; p = p.parent
} }
return p.parent; return p.parent
} }
} }
rotateRight (tree) { rotateRight (tree) {
var parent = this.parent; var parent = this.parent
var newParent = this.left; var newParent = this.left
var newLeft = this.left.right; var newLeft = this.left.right
newParent.right = this; newParent.right = this
this.left = newLeft; this.left = newLeft
if (parent === null) { if (parent === null) {
tree.root = newParent; tree.root = newParent
newParent._parent = null; newParent._parent = null
} else if (parent.left === this) { } else if (parent.left === this) {
parent.left = newParent; parent.left = newParent
} else if (parent.right === this) { } else if (parent.right === this) {
parent.right = newParent; parent.right = newParent
} else { } else {
throw new Error("The elements are wrongly connected!"); throw new Error('The elements are wrongly connected!')
} }
} }
getUncle () { getUncle () {
// we can assume that grandparent exists when this is called! // we can assume that grandparent exists when this is called!
if (this.parent === this.parent.parent.left) { if (this.parent === this.parent.parent.left) {
return this.parent.parent.right; return this.parent.parent.right
} else { } 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 () { constructor () {
this.root = null; this.root = null
this.length = 0; this.length = 0
} }
findNodeWithLowerBound (from) { findNodeWithLowerBound (from) {
if (from === void 0) { 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) { if (o === null) {
return false; return false
} else { } else {
while (true) { while (true) {
if ((from === null || smaller(from, o.val.id)) && o.left !== null) { if ((from === null || smaller(from, o.val.id)) && o.left !== null) {
// o is included in the bound // o is included in the bound
// try to find an element that is closer to 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)) { } else if (from !== null && smaller(o.val.id, from)) {
// o is not within the bound, maybe one of the right elements is.. // o is not within the bound, maybe one of the right elements is..
if (o.right !== null) { if (o.right !== null) {
o = o.right; o = o.right
} else { } else {
// there is no right element. Search for the next bigger element, // there is no right element. Search for the next bigger element,
// this should be within the bounds // this should be within the bounds
return o.next(); return o.next()
} }
} else { } else {
return o; return o
} }
} }
} }
} }
iterate (from, to, f) { iterate (from, to, f) {
var o = this.findNodeWithLowerBound(from); var o = this.findNodeWithLowerBound(from)
while ( o !== null && (to === null || smaller(o.val.id, to) || compareIds(o.val.id, to)) ) { while (o !== null && (to === null || smaller(o.val.id, to) || compareIds(o.val.id, to))) {
f(o.val); f(o.val)
o = o.next(); o = o.next()
} }
return true; return true
} }
find (id) { find (id) {
return this.findNode(id).val; return this.findNode(id).val
} }
findNode (id) { findNode (id) {
if (id == null || id.constructor !== Array) { 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) { if (o === null) {
return false; return false
} else { } else {
while (true) { while (true) {
if (o === null) { if (o === null) {
return false; return false
} }
if (smaller(id, o.val.id)) { if (smaller(id, o.val.id)) {
o = o.left; o = o.left
} else if (smaller(o.val.id, id)) { } else if (smaller(o.val.id, id)) {
o = o.right; o = o.right
} else { } else {
return o; return o
} }
} }
} }
} }
delete (id) { delete (id) {
if (id == null || id.constructor !== Array) { 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) { 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) { if (d.left !== null && d.right !== null) {
// switch d with the greates element in the left subtree. // switch d with the greates element in the left subtree.
// o should have at most one child. // o should have at most one child.
var o = d.left; var o = d.left
// find // find
while (o.right !== null) { while (o.right !== null) {
o = o.right; o = o.right
} }
// switch // switch
d.val = o.val; d.val = o.val
d = o; d = o
} }
// d has at most one child // d has at most one child
// let n be the node that replaces d // let n be the node that replaces d
var isFakeChild; var isFakeChild
var child = d.left || d.right; var child = d.left || d.right
if ( child === null) { if (child === null) {
isFakeChild = true; isFakeChild = true
child = new N({id: 0}); child = new N({id: 0})
child.blacken(); child.blacken()
d.right = child; d.right = child
} else { } else {
isFakeChild = false; isFakeChild = false
} }
if (d.parent === null) { if (d.parent === null) {
if (!isFakeChild) { if (!isFakeChild) {
this.root = child; this.root = child
child.blacken(); child.blacken()
child._parent = null; child._parent = null
} else { } else {
this.root = null; this.root = null
} }
return; return
} else if (d.parent.left === d) { } else if (d.parent.left === d) {
d.parent.left = child; d.parent.left = child
} else if (d.parent.right === d) { } else if (d.parent.right === d) {
d.parent.right = child; d.parent.right = child
} else { } else {
throw new Error("Impossible!"); throw new Error('Impossible!')
} }
if ( d.isBlack() ) { if (d.isBlack()) {
if ( child.isRed() ) { if (child.isRed()) {
child.blacken(); child.blacken()
} else { } else {
this._fixDelete(child); this._fixDelete(child)
} }
} }
this.root.blacken(); this.root.blacken()
if (isFakeChild) { if (isFakeChild) {
if (child.parent.left === child) { if (child.parent.left === child) {
child.parent.left = null; child.parent.left = null
} else if (child.parent.right === child) { } else if (child.parent.right === child) {
child.parent.right = null; child.parent.right = null
} else { } else {
throw new Error("Impossible #3"); throw new Error('Impossible #3')
} }
} }
} }
_fixDelete (n) { _fixDelete (n) {
function isBlack (node) { function isBlack (node) {
return node !== null ? node.isBlack() : true; return node !== null ? node.isBlack() : true
} }
function isRed(node) { function isRed (node) {
return node !== null ? node.isRed() : false; return node !== null ? node.isRed() : false
} }
if (n.parent === null) { if (n.parent === null) {
// this can only be called after the first iteration of fixDelete. // this can only be called after the first iteration of fixDelete.
return; return
} }
// d was already replaced by the child // d was already replaced by the child
// d is not the root // d is not the root
// d and child are black // d and child are black
var sibling = n.sibling; var sibling = n.sibling
if (isRed(sibling)) { if (isRed(sibling)) {
// make sibling the grandfather // make sibling the grandfather
n.parent.redden(); n.parent.redden()
sibling.blacken(); sibling.blacken()
if (n === n.parent.left) { if (n === n.parent.left) {
n.parent.rotateLeft(this); n.parent.rotateLeft(this)
} else if (n === n.parent.right) { } else if (n === n.parent.right) {
n.parent.rotateRight(this); n.parent.rotateRight(this)
} else { } 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 // parent, sibling, and children of n are black
if ( n.parent.isBlack() && if (n.parent.isBlack() &&
sibling.isBlack() && sibling.isBlack() &&
isBlack(sibling.left) && isBlack(sibling.left) &&
isBlack(sibling.right) isBlack(sibling.right)
) { ) {
sibling.redden(); sibling.redden()
this._fixDelete(n.parent); this._fixDelete(n.parent)
} else if ( n.parent.isRed() && } else if (n.parent.isRed() &&
sibling.isBlack() && sibling.isBlack() &&
isBlack(sibling.left) && isBlack(sibling.left) &&
isBlack(sibling.right) isBlack(sibling.right)
) { ) {
sibling.redden(); sibling.redden()
n.parent.blacken(); n.parent.blacken()
} else { } else {
if ( n === n.parent.left && if (n === n.parent.left &&
sibling.isBlack() && sibling.isBlack() &&
isRed(sibling.left) && isRed(sibling.left) &&
isBlack(sibling.right) isBlack(sibling.right)
) { ) {
sibling.redden(); sibling.redden()
sibling.left.blacken(); sibling.left.blacken()
sibling.rotateRight(this); sibling.rotateRight(this)
sibling = n.sibling; sibling = n.sibling
} else if ( n === n.parent.right && } else if (n === n.parent.right &&
sibling.isBlack() && sibling.isBlack() &&
isRed(sibling.right) && isRed(sibling.right) &&
isBlack(sibling.left) isBlack(sibling.left)
) { ) {
sibling.redden(); sibling.redden()
sibling.right.blacken(); sibling.right.blacken()
sibling.rotateLeft(this); sibling.rotateLeft(this)
sibling = n.sibling; sibling = n.sibling
} }
sibling.color = n.parent.color; sibling.color = n.parent.color
n.parent.blacken(); n.parent.blacken()
if (n === n.parent.left) { if (n === n.parent.left) {
sibling.right.blacken(); sibling.right.blacken()
n.parent.rotateLeft(this); n.parent.rotateLeft(this)
} else { } else {
sibling.left.blacken(); sibling.left.blacken()
n.parent.rotateRight(this); n.parent.rotateRight(this)
} }
} }
} }
add (v) { add (v) {
if (v == null || v.id == null || v.id.constructor !== Array) { 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) { if (this.root !== null) {
var p = this.root; // p abbrev. parent var p = this.root // p abbrev. parent
while (true) { while (true) {
if (smaller(node.val.id, p.val.id)) { if (smaller(node.val.id, p.val.id)) {
if (p.left === null) { if (p.left === null) {
p.left = node; p.left = node
break; break
} else { } else {
p = p.left; p = p.left
} }
} else if (smaller(p.val.id, node.val.id)) { } else if (smaller(p.val.id, node.val.id)) {
if (p.right === null) { if (p.right === null) {
p.right = node; p.right = node
break; break
} else { } else {
p = p.right; p = p.right
} }
} else { } else {
return false; return false
} }
} }
this._fixInsert(node); this._fixInsert(node)
} else { } else {
this.root = node; this.root = node
} }
this.length++; this.length++
this.root.blacken(); this.root.blacken()
} }
_fixInsert (n) { _fixInsert (n) {
if (n.parent === null) { if (n.parent === null) {
n.blacken(); n.blacken()
return; return
} else if (n.parent.isBlack()) { } else if (n.parent.isBlack()) {
return; return
} }
var uncle = n.getUncle(); var uncle = n.getUncle()
if (uncle !== null && uncle.isRed()) { if (uncle !== null && uncle.isRed()) {
// Note: parent: red, uncle: red // Note: parent: red, uncle: red
n.parent.blacken(); n.parent.blacken()
uncle.blacken(); uncle.blacken()
n.grandparent.redden(); n.grandparent.redden()
this._fixInsert(n.grandparent); this._fixInsert(n.grandparent)
} else { } else {
// Note: parent: red, uncle: black or null // Note: parent: red, uncle: black or null
// Now we transform the tree in such a way that // 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 // and grandparent.left.left.isRed
// 2) grandparent.right.isRed // 2) grandparent.right.isRed
// and grandparent.right.right.isRed // and grandparent.right.right.isRed
if (n === n.parent.right if (n === n.parent.right && n.parent === n.grandparent.left) {
&& n.parent === n.grandparent.left) { n.parent.rotateLeft(this)
n.parent.rotateLeft(this); // Since we rotated and want to use the previous
// Since we rotated and want to use the previous // cases, we need to set n in such a way that
// cases, we need to set n in such a way that // n.parent.isRed again
// n.parent.isRed again n = n.left
n = n.left; } else if (n === n.parent.left && n.parent === n.grandparent.right) {
} else if (n === n.parent.left n.parent.rotateRight(this)
&& n.parent === n.grandparent.right) { // see above
n.parent.rotateRight(this); n = n.right
// see above
n = n.right;
} }
// Case 1) or 2) hold from here on. // Case 1) or 2) hold from here on.
// Now traverse grandparent, make parent a black node // Now traverse grandparent, make parent a black node
// on the highest level which holds two red nodes. // on the highest level which holds two red nodes.
n.parent.blacken(); n.parent.blacken()
n.grandparent.redden(); n.grandparent.redden()
if (n === n.parent.left) { if (n === n.parent.left) {
// Case 1 // Case 1
n.grandparent.rotateRight(this); n.grandparent.rotateRight(this)
} else { } else {
// Case 2 // Case 2
n.grandparent.rotateLeft(this); n.grandparent.rotateLeft(this)
} }
} }
} }

View File

@ -1,209 +1,209 @@
/* @flow */ /* global RBTree, smaller, compareIds */
/*eslint-env browser,jasmine,console */ /* eslint-env browser,jasmine,console */
var numberOfRBTreeTests = 1000; var numberOfRBTreeTests = 1000
function itRedNodesDoNotHaveBlackChildren (tree) { function itRedNodesDoNotHaveBlackChildren (tree) {
it("Red nodes do not have black children", function(){ it('Red nodes do not have black children', function () {
function traverse (n) { function traverse (n) {
if (n == null) { if (n == null) {
return; return
} }
if (n.isRed()) { if (n.isRed()) {
if (n.left != null) { if (n.left != null) {
expect(n.left.isRed()).not.toBeTruthy(); expect(n.left.isRed()).not.toBeTruthy()
} }
if (n.right != null) { if (n.right != null) {
expect(n.right.isRed()).not.toBeTruthy(); expect(n.right.isRed()).not.toBeTruthy()
} }
} }
traverse(n.left); traverse(n.left)
traverse(n.right); traverse(n.right)
} }
traverse(tree.root); traverse(tree.root)
}); })
} }
function itBlackHeightOfSubTreesAreEqual (tree){ function itBlackHeightOfSubTreesAreEqual (tree) {
it("Black-height of sub-trees are equal", function(){ it('Black-height of sub-trees are equal', function () {
function traverse (n) { function traverse (n) {
if (n == null) { if (n == null) {
return 0; return 0
} }
var sub1 = traverse(n.left); var sub1 = traverse(n.left)
var sub2 = traverse(n.right); var sub2 = traverse(n.right)
expect(sub1).toEqual(sub2); expect(sub1).toEqual(sub2)
if(n.isRed()) { if (n.isRed()) {
return sub1; return sub1
} else { } else {
return sub1 + 1; return sub1 + 1
} }
} }
traverse(tree.root); traverse(tree.root)
}); })
} }
function itRootNodeIsBlack(tree) { function itRootNodeIsBlack (tree) {
it("root node is black", function(){ it('root node is black', function () {
expect(tree.root == null || tree.root.isBlack()).toBeTruthy(); expect(tree.root == null || tree.root.isBlack()).toBeTruthy()
}); })
} }
describe("RedBlack Tree", function(){ describe('RedBlack Tree', function () {
beforeEach(function(){ beforeEach(function () {
this.tree = new RBTree(); this.tree = new RBTree()
}); })
it("can add&retrieve 5 elements", function(){ it('can add&retrieve 5 elements', function () {
this.tree.add({val: "four", id: [4]}); this.tree.add({val: 'four', id: [4]})
this.tree.add({val: "one", id: [1]}); this.tree.add({val: 'one', id: [1]})
this.tree.add({val: "three", id: [3]}); this.tree.add({val: 'three', id: [3]})
this.tree.add({val: "two", id: [2]}); this.tree.add({val: 'two', id: [2]})
this.tree.add({val: "five", id: [5]}); this.tree.add({val: 'five', id: [5]})
expect(this.tree.find([1]).val).toEqual("one"); expect(this.tree.find([1]).val).toEqual('one')
expect(this.tree.find([2]).val).toEqual("two"); expect(this.tree.find([2]).val).toEqual('two')
expect(this.tree.find([3]).val).toEqual("three"); expect(this.tree.find([3]).val).toEqual('three')
expect(this.tree.find([4]).val).toEqual("four"); expect(this.tree.find([4]).val).toEqual('four')
expect(this.tree.find([5]).val).toEqual("five"); expect(this.tree.find([5]).val).toEqual('five')
}); })
it("5 elements do not exist anymore after deleting them", function(){ it('5 elements do not exist anymore after deleting them', function () {
this.tree.add({val: "four", id: [4]}); this.tree.add({val: 'four', id: [4]})
this.tree.add({val: "one", id: [1]}); this.tree.add({val: 'one', id: [1]})
this.tree.add({val: "three", id: [3]}); this.tree.add({val: 'three', id: [3]})
this.tree.add({val: "two", id: [2]}); this.tree.add({val: 'two', id: [2]})
this.tree.add({val: "five", id: [5]}); this.tree.add({val: 'five', id: [5]})
this.tree.delete([4]); this.tree.delete([4])
expect(this.tree.find([4])).not.toBeTruthy(); expect(this.tree.find([4])).not.toBeTruthy()
this.tree.delete([3]); this.tree.delete([3])
expect(this.tree.find([3])).not.toBeTruthy(); expect(this.tree.find([3])).not.toBeTruthy()
this.tree.delete([2]); this.tree.delete([2])
expect(this.tree.find([2])).not.toBeTruthy(); expect(this.tree.find([2])).not.toBeTruthy()
this.tree.delete([1]); this.tree.delete([1])
expect(this.tree.find([1])).not.toBeTruthy(); expect(this.tree.find([1])).not.toBeTruthy()
this.tree.delete([5]); this.tree.delete([5])
expect(this.tree.find([5])).not.toBeTruthy(); expect(this.tree.find([5])).not.toBeTruthy()
}); })
it("debug #1", function(){ it('debug #1', function () {
this.tree.add({id: [2]}); this.tree.add({id: [2]})
this.tree.add({id: [0]}); this.tree.add({id: [0]})
this.tree.delete([2]); this.tree.delete([2])
this.tree.add({id: [1]}); this.tree.add({id: [1]})
expect(this.tree.find([0])).not.toBeUndefined(); expect(this.tree.find([0])).not.toBeUndefined()
expect(this.tree.find([1])).not.toBeUndefined(); expect(this.tree.find([1])).not.toBeUndefined()
expect(this.tree.find([2])).toBeUndefined(); expect(this.tree.find([2])).toBeUndefined()
}); })
describe("debug #2", function(){ describe('debug #2', function () {
var tree = new RBTree(); var tree = new RBTree()
tree.add({id: [8433]}); tree.add({id: [8433]})
tree.add({id: [12844]}); tree.add({id: [12844]})
tree.add({id: [1795]}); tree.add({id: [1795]})
tree.add({id: [30302]}); tree.add({id: [30302]})
tree.add({id: [64287]}); tree.add({id: [64287]})
tree.delete([8433]); tree.delete([8433])
tree.add({id: [28996]}); tree.add({id: [28996]})
tree.delete([64287]); tree.delete([64287])
tree.add({id: [22721]}); tree.add({id: [22721]})
itRootNodeIsBlack(tree, []); itRootNodeIsBlack(tree, [])
itBlackHeightOfSubTreesAreEqual(tree, []); itBlackHeightOfSubTreesAreEqual(tree, [])
}); })
describe(`After adding&deleting (0.8/0.2) ${numberOfRBTreeTests} times`, function () { describe(`After adding&deleting (0.8/0.2) ${numberOfRBTreeTests} times`, function () {
var elements = []; var elements = []
var tree = new RBTree(); var tree = new RBTree()
for(var i = 0; i < numberOfRBTreeTests; i++) { for (var i = 0; i < numberOfRBTreeTests; i++) {
var r = Math.random(); var r = Math.random()
if (r < 0.8) { if (r < 0.8) {
var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)]; var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)]
elements.push(obj); elements.push(obj)
tree.add({id: obj}); tree.add({id: obj})
} else if (elements.length > 0) { } else if (elements.length > 0) {
var elemid = Math.floor(Math.random() * elements.length); var elemid = Math.floor(Math.random() * elements.length)
var elem = elements[elemid]; var elem = elements[elemid]
elements = elements.filter(function(e){return !compareIds(e,elem); }); //eslint-disable-line elements = elements.filter(function (e) {return !compareIds(e, elem); }); // eslint-disable-line
tree.delete(elem); tree.delete(elem)
} }
} }
itRootNodeIsBlack(tree); itRootNodeIsBlack(tree)
it("can find every object", function(){ it('can find every object', function () {
for(var id of elements) { for (var id of elements) {
expect(tree.find(id).id).toEqual(id); expect(tree.find(id).id).toEqual(id)
} }
}); })
it("can find every object with lower bound search", function(){ it('can find every object with lower bound search', function () {
for(var id of elements) { for (var id of elements) {
expect(tree.findNodeWithLowerBound(id).val.id).toEqual(id); 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(){ 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 lowerBound = elements[Math.floor(Math.random() * elements.length)]
var expectedResults = elements.filter(function(e, pos){ var expectedResults = elements.filter(function (e, pos) {
return (smaller(lowerBound, e) || compareIds(e, lowerBound)) && elements.indexOf(e) === pos; return (smaller(lowerBound, e) || compareIds(e, lowerBound)) && elements.indexOf(e) === pos
}).length; }).length
var actualResults = 0; var actualResults = 0
tree.iterate(lowerBound, null, function(val){ tree.iterate(lowerBound, null, function (val) {
expect(val).not.toBeUndefined(); expect(val).not.toBeUndefined()
actualResults++; actualResults++
}); })
expect(expectedResults).toEqual(actualResults); expect(expectedResults).toEqual(actualResults)
}); })
it("iterating over a tree without bounds yield the right amount of results", function(){ it('iterating over a tree without bounds yield the right amount of results', function () {
var lowerBound = null; var lowerBound = null
var expectedResults = elements.filter(function(e, pos){ var expectedResults = elements.filter(function (e, pos) {
return elements.indexOf(e) === pos; return elements.indexOf(e) === pos
}).length; }).length
var actualResults = 0; var actualResults = 0
tree.iterate(lowerBound, null, function(val){ tree.iterate(lowerBound, null, function (val) {
expect(val).not.toBeUndefined(); expect(val).not.toBeUndefined()
actualResults++; actualResults++
}); })
expect(expectedResults).toEqual(actualResults); expect(expectedResults).toEqual(actualResults)
}); })
it("iterating over a tree with upper bound yields the right amount of results", function(){ 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 upperBound = elements[Math.floor(Math.random() * elements.length)]
var expectedResults = elements.filter(function(e, pos){ var expectedResults = elements.filter(function (e, pos) {
return (smaller(e, upperBound) || compareIds(e, upperBound)) && elements.indexOf(e) === pos; return (smaller(e, upperBound) || compareIds(e, upperBound)) && elements.indexOf(e) === pos
}).length; }).length
var actualResults = 0; var actualResults = 0
tree.iterate(null, upperBound, function(val){ tree.iterate(null, upperBound, function (val) {
expect(val).not.toBeUndefined(); expect(val).not.toBeUndefined()
actualResults++; actualResults++
}); })
expect(expectedResults).toEqual(actualResults); expect(expectedResults).toEqual(actualResults)
}); })
it("iterating over a tree with upper and lower bounds yield the right amount of results", function(){ 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 b1 = elements[Math.floor(Math.random() * elements.length)]
var b2 = elements[Math.floor(Math.random() * elements.length)]; var b2 = elements[Math.floor(Math.random() * elements.length)]
var upperBound, lowerBound; var upperBound, lowerBound
if (smaller(b1, b2)) { if (smaller(b1, b2)) {
lowerBound = b1; lowerBound = b1
upperBound = b2; upperBound = b2
} else { } else {
lowerBound = b2; lowerBound = b2
upperBound = b1; upperBound = b1
} }
var expectedResults = elements.filter(function(e, pos){ var expectedResults = elements.filter(function (e, pos) {
return (smaller(lowerBound, e) || compareIds(e, lowerBound)) return (smaller(lowerBound, e) || compareIds(e, lowerBound)) &&
&& (smaller(e, upperBound) || compareIds(e, upperBound)) && elements.indexOf(e) === pos; (smaller(e, upperBound) || compareIds(e, upperBound)) && elements.indexOf(e) === pos
}).length; }).length
var actualResults = 0; var actualResults = 0
tree.iterate(lowerBound, upperBound, function(val){ tree.iterate(lowerBound, upperBound, function (val) {
expect(val).not.toBeUndefined(); expect(val).not.toBeUndefined()
actualResults++; actualResults++
}); })
expect(expectedResults).toEqual(actualResults); expect(expectedResults).toEqual(actualResults)
}); })
}); })
}); })

View File

@ -1,36 +1,16 @@
/* @flow */ /* global copyObject, Y*/
// 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) {
function compareIds (id1, id2) {
if (id1 == null || id2 == null) { if (id1 == null || id2 == null) {
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]) { if (id1[0] === id2[0] && id1[1] === id2[1]) {
return true; return true
} else { } else {
return false; return false
} }
} }
@ -42,26 +22,26 @@ var Struct = {
*/ */
Delete: { Delete: {
encode: function (op) { encode: function (op) {
return op; return op
}, },
requiredOps: function (op) { requiredOps: function (op) {
return [op.target]; return [op.target]
}, },
execute: function* (op) { execute: function * (op) {
var target = yield* this.getOperation(op.target); var target = yield* this.getOperation(op.target)
if (!target.deleted) { if (!target.deleted) {
target.deleted = true; target.deleted = true
yield* this.setOperation(target); yield* this.setOperation(target)
var t = this.store.initializedTypes[JSON.stringify(target.parent)]; var t = this.store.initializedTypes[JSON.stringify(target.parent)]
if (t != null) { if (t != null) {
yield* t._changed(this, copyObject(op)); yield* t._changed(this, copyObject(op))
} }
} }
} }
}, },
Insert: { Insert: {
/*{ /* {
content: any, content: any,
left: Id, left: Id,
right: Id, right: Id,
@ -71,8 +51,9 @@ var Struct = {
id: this.os.getNextOpId() id: this.os.getNextOpId()
} }
*/ */
encode: function(op){ encode: function (op) {
/*var e = { /*
var e = {
id: op.id, id: op.id,
left: op.left, left: op.left,
right: op.right, right: op.right,
@ -80,44 +61,44 @@ var Struct = {
parent: op.parent, parent: op.parent,
content: op.content, content: op.content,
struct: "Insert" struct: "Insert"
}; }
if (op.parentSub != null){ if (op.parentSub != null){
e.parentSub = op.parentSub; e.parentSub = op.parentSub
} }
return e;*/ return e;*/
return op; return op
}, },
requiredOps: function(op){ requiredOps: function (op) {
var ids = []; var ids = []
if(op.left != null){ if (op.left != null) {
ids.push(op.left); ids.push(op.left)
} }
if(op.right != null){ if (op.right != null) {
ids.push(op.right); ids.push(op.right)
} }
//if(op.right == null && op.left == null) {} // if(op.right == null && op.left == null) {}
ids.push(op.parent); ids.push(op.parent)
if (op.opContent != null) { 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) { if (op.left == null) {
return 0; return 0
} else { } else {
var d = 0; var d = 0
var o = yield* this.getOperation(op.left); var o = yield* this.getOperation(op.left)
while (!compareIds(op.origin, (o ? o.id : null))) { while (!compareIds(op.origin, (o ? o.id : null))) {
d++; d++
if (o.left == null) { if (o.left == null) {
break; break
} else { } 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 # case 3: $origin > $o.origin
# $this insert_position is to the left of $o (forever!) # $this insert_position is to the left of $o (forever!)
*/ */
execute: function*(op){ execute: function *(op) {
var i; // loop counter var i // loop counter
var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op); // most cases: 0 (starts from 0) var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
var o; var o
var parent; var parent
var start; var start
// find o. o is the first conflicting operation // find o. o is the first conflicting operation
if (op.left != null) { if (op.left != null) {
o = yield* this.getOperation(op.left); o = yield* this.getOperation(op.left)
o = (o.right == null) ? null : yield* this.getOperation(o.right); o = (o.right == null) ? null : yield* this.getOperation(o.right)
} else { // left == null } else { // left == null
parent = yield* this.getOperation(op.parent); parent = yield* this.getOperation(op.parent)
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start; let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
start = startId == null ? null : yield* this.getOperation(startId); start = startId == null ? null : yield* this.getOperation(startId)
o = start; o = start
} }
// handle conflicts // handle conflicts
while (true) { while (true) {
if (o != null && !compareIds(o.id, op.right)){ if (o != null && !compareIds(o.id, op.right)) {
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o); var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
if (oOriginDistance === i) { if (oOriginDistance === i) {
// case 1 // case 1
if (o.id[0] < op.id[0]) { if (o.id[0] < op.id[0]) {
op.left = o.id; op.left = o.id
distanceToOrigin = i + 1; distanceToOrigin = i + 1
} }
} else if (oOriginDistance < i) { } else if (oOriginDistance < i) {
// case 2 // case 2
if (i - distanceToOrigin <= oOriginDistance) { if (i - distanceToOrigin <= oOriginDistance) {
op.left = o.id; op.left = o.id
distanceToOrigin = i + 1; distanceToOrigin = i + 1
} }
} else { } else {
break; break
} }
i++; i++
o = o.right ? yield* this.getOperation(o.right) : null; o = o.right ? yield* this.getOperation(o.right) : null
} else { } else {
break; break
} }
} }
// reconnect.. // reconnect..
var left = null; var left = null
var right = null; var right = null
parent = parent || (yield* this.getOperation(op.parent)); parent = parent || (yield* this.getOperation(op.parent))
// reconnect left and set right of op // reconnect left and set right of op
if (op.left != null) { if (op.left != null) {
left = yield* this.getOperation(op.left); left = yield* this.getOperation(op.left)
op.right = left.right; op.right = left.right
left.right = op.id; left.right = op.id
yield* this.setOperation(left); yield* this.setOperation(left)
} else { } 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 // reconnect right
if (op.right != null) { if (op.right != null) {
right = yield* this.getOperation(op.right); right = yield* this.getOperation(op.right)
right.left = op.id; right.left = op.id
yield* this.setOperation(right); yield* this.setOperation(right)
} }
// notify parent // notify parent
if (op.parentSub != null) { if (op.parentSub != null) {
if (left == null) { if (left == null) {
parent.map[op.parentSub] = op.id; parent.map[op.parentSub] = op.id
yield* this.setOperation(parent); yield* this.setOperation(parent)
} }
} else { } else {
if (right == null || left == null) { if (right == null || left == null) {
if (right == null) { if (right == null) {
parent.end = op.id; parent.end = op.id
} }
if (left == null) { 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() id: this.os.getNextOpId()
} }
*/ */
encode: function(op){ encode: function (op) {
return { return {
struct: "List", struct: 'List',
id: op.id, id: op.id,
type: op.type type: op.type
}; }
}, },
requiredOps: function(){ requiredOps: function () {
/* /*
var ids = []; var ids = []
if (op.start != null) { if (op.start != null) {
ids.push(op.start); ids.push(op.start)
} }
if (op.end != null){ if (op.end != null){
ids.push(op.end); ids.push(op.end)
} }
return ids; return ids
*/ */
return []; return []
}, },
execute: function* (op) { execute: function * (op) { // eslint-disable-line
op.start = null; op.start = null
op.end = null; op.end = null
}, },
ref: function* (op : Op, pos : number) : Insert { ref: function * (op, pos) {
if (op.start == null) { if (op.start == null) {
return null; return null
} }
var res = null; var res = null
var o = yield* this.getOperation(op.start); var o = yield* this.getOperation(op.start)
while ( true ) { while (true) {
if (!o.deleted) { if (!o.deleted) {
res = o; res = o
pos--; pos--
} }
if (pos >= 0 && o.right != null) { if (pos >= 0 && o.right != null) {
o = (yield* this.getOperation(o.right)); o = (yield* this.getOperation(o.right))
} else { } else {
break; break
} }
} }
return res; return res
}, },
map: function* (o : Op, f : Function) : Array<any> { map: function * (o, f) {
o = o.start; o = o.start
var res = []; var res = []
while ( o !== null) { while (o !== null) {
var operation = yield* this.getOperation(o); var operation = yield* this.getOperation(o)
if (!operation.deleted) { if (!operation.deleted) {
res.push(f(operation)); res.push(f(operation))
} }
o = operation.right; o = operation.right
} }
return res; return res
} }
}, },
Map: { Map: {
@ -295,42 +276,41 @@ var Struct = {
id: this.os.getNextOpId() id: this.os.getNextOpId()
} }
*/ */
encode: function(op){ encode: function (op) {
return { return {
struct: "Map", struct: 'Map',
type: op.type, type: op.type,
id: op.id, id: op.id,
map: {} // overwrite map!! map: {} // overwrite map!!
}; }
}, },
requiredOps: function(){ requiredOps: function () {
/* /*
var ids = []; var ids = []
for (var end in op.map) { for (var end in op.map) {
ids.push(op.map[end]); ids.push(op.map[end])
} }
return ids; return ids
*/ */
return []; return []
}, },
execute: function* () { execute: function * () {},
}, get: function * (op, name) {
get: function* (op, name) { var oid = op.map[name]
var oid = op.map[name];
if (oid != null) { 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 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) { delete: function * (op, name) {
var v = op.map[name] || null; var v = op.map[name] || null
if (v != null) { if (v != null) {
yield* Struct.Delete.create.call(this, { yield* Struct.Delete.create.call(this, {
target: v target: v
}); })
} }
} }
} }
}; }
Y.Struct = Struct; Y.Struct = Struct

View File

View File

@ -1,86 +1,85 @@
/* global EventHandler, Y, CustomType, Struct */
;(function () {
(function(){
class YArray { class YArray {
constructor (os, _model, idArray, valArray) { constructor (os, _model, idArray, valArray) {
this.os = os; this.os = os
this._model = _model; this._model = _model
// Array of all the operation id's // Array of all the operation id's
this.idArray = idArray; this.idArray = idArray
// Array of all the values // Array of all the values
this.valArray = valArray; this.valArray = valArray
this.eventHandler = new EventHandler( ops =>{ this.eventHandler = new EventHandler(ops => {
var userEvents = []; var userEvents = []
for (var i in ops) { for (var i in ops) {
var op = ops[i]; var op = ops[i]
if (op.struct === "Insert") { if (op.struct === 'Insert') {
let pos; let pos
// we check op.left only!, // we check op.left only!,
// because op.right might not be defined when this is called // because op.right might not be defined when this is called
if (op.left === null) { if (op.left === null) {
pos = 0; pos = 0
} else { } else {
var sid = JSON.stringify(op.left); var sid = JSON.stringify(op.left)
pos = this.idArray.indexOf(sid) + 1; pos = this.idArray.indexOf(sid) + 1
if (pos <= 0) { if (pos <= 0) {
throw new Error("Unexpected operation!"); throw new Error('Unexpected operation!')
} }
} }
this.idArray.splice(pos, 0, JSON.stringify(op.id)); this.idArray.splice(pos, 0, JSON.stringify(op.id))
this.valArray.splice(pos, 0, op.content); this.valArray.splice(pos, 0, op.content)
userEvents.push({ userEvents.push({
type: "insert", type: 'insert',
object: this, object: this,
index: pos, index: pos,
length: 1 length: 1
}); })
} else if (op.struct === "Delete") { } else if (op.struct === 'Delete') {
let pos = this.idArray.indexOf(JSON.stringify(op.target)); let pos = this.idArray.indexOf(JSON.stringify(op.target))
this.idArray.splice(pos, 1); this.idArray.splice(pos, 1)
this.valArray.splice(pos, 1); this.valArray.splice(pos, 1)
userEvents.push({ userEvents.push({
type: "delete", type: 'delete',
object: this, object: this,
index: pos, index: pos,
length: 1 length: 1
}); })
} else { } else {
throw new Error("Unexpected struct!"); throw new Error('Unexpected struct!')
} }
} }
this.eventHandler.callUserEventListeners(userEvents); this.eventHandler.callUserEventListeners(userEvents)
}); })
} }
get length () { get length () {
return this.idArray.length; return this.idArray.length
} }
get (pos) { get (pos) {
if (pos == null || typeof pos !== "number") { if (pos == null || typeof pos !== 'number') {
throw new Error("pos must be a number!"); throw new Error('pos must be a number!')
} }
return this.valArray[pos]; return this.valArray[pos]
} }
toArray() { toArray () {
return this.valArray.slice(); return this.valArray.slice()
} }
insert (pos, contents) { insert (pos, contents) {
if (typeof pos !== "number") { if (typeof pos !== 'number') {
throw new Error("pos must be a number!"); throw new Error('pos must be a number!')
} }
if (!(contents instanceof Array)) { 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) { if (contents.length === 0) {
return; return
} }
if (pos > this.idArray.length || pos < 0) { 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 ops = []
var prevId = mostLeft; var prevId = mostLeft
for (var i = 0; i < contents.length; i++) { for (var i = 0; i < contents.length; i++) {
var op = { var op = {
left: prevId, left: prevId,
@ -90,97 +89,97 @@
// at the time of creating this operation, and is therefore not defined in idArray // at the time of creating this operation, and is therefore not defined in idArray
parent: this._model, parent: this._model,
content: contents[i], content: contents[i],
struct: "Insert", struct: 'Insert',
id: this.os.getNextOpId() id: this.os.getNextOpId()
}; }
ops.push(op); ops.push(op)
prevId = op.id; prevId = op.id
} }
var eventHandler = this.eventHandler; var eventHandler = this.eventHandler
eventHandler.awaitAndPrematurelyCall(ops); eventHandler.awaitAndPrematurelyCall(ops)
this.os.requestTransaction(function*(){ this.os.requestTransaction(function *() {
// now we can set the right reference. // now we can set the right reference.
var mostRight; var mostRight
if (mostLeft != null) { if (mostLeft != null) {
mostRight = (yield* this.getOperation(mostLeft)).right; mostRight = (yield* this.getOperation(mostLeft)).right
} else { } else {
mostRight = (yield* this.getOperation(ops[0].parent)).start; mostRight = (yield* this.getOperation(ops[0].parent)).start
} }
for (var j in ops) { for (var j in ops) {
ops[j].right = mostRight; ops[j].right = mostRight
} }
yield* this.applyCreatedOperations(ops); yield* this.applyCreatedOperations(ops)
eventHandler.awaitedLastInserts(ops.length); eventHandler.awaitedLastInserts(ops.length)
}); })
} }
delete (pos, length = 1) { delete (pos, length = 1) {
if (typeof length !== "number") { if (typeof length !== 'number') {
throw new Error("pos must be a number!"); throw new Error('pos must be a number!')
} }
if (typeof pos !== "number") { if (typeof pos !== 'number') {
throw new Error("pos must be a number!"); throw new Error('pos must be a number!')
} }
if (pos + length > this.idArray.length || pos < 0 || length < 0) { 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) { if (length === 0) {
return; return
} }
var eventHandler = this.eventHandler; var eventHandler = this.eventHandler
var newLeft = pos > 0 ? JSON.parse(this.idArray[pos - 1]) : null; var newLeft = pos > 0 ? JSON.parse(this.idArray[pos - 1]) : null
var dels = []; var dels = []
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
dels.push({ dels.push({
target: JSON.parse(this.idArray[pos + i]), target: JSON.parse(this.idArray[pos + i]),
struct: "Delete" struct: 'Delete'
}); })
} }
eventHandler.awaitAndPrematurelyCall(dels); eventHandler.awaitAndPrematurelyCall(dels)
this.os.requestTransaction(function*(){ this.os.requestTransaction(function *() {
yield* this.applyCreatedOperations(dels); yield* this.applyCreatedOperations(dels)
eventHandler.awaitedLastDeletes(dels.length, newLeft); eventHandler.awaitedLastDeletes(dels.length, newLeft)
}); })
} }
observe (f) { observe (f) {
this.eventHandler.addUserEventListener(f); this.eventHandler.addUserEventListener(f)
} }
*_changed (transaction, op) { * _changed (transaction, op) {
if (op.struct === "Insert") { if (op.struct === 'Insert') {
var l = op.left; var l = op.left
var left; var left
while (l != null) { while (l != null) {
left = yield* transaction.getOperation(l); left = yield* transaction.getOperation(l)
if (!left.deleted) { 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({ Y.Array = new CustomType({
class: YArray, class: YArray,
createType: function* YArrayCreator () { createType: function * YArrayCreator () {
var model = { var model = {
start: null, start: null,
end: null, end: null,
struct: "List", struct: 'List',
type: "Array", type: 'Array',
id: this.store.getNextOpId() id: this.store.getNextOpId()
}; }
yield* this.applyCreatedOperations([model]); yield* this.applyCreatedOperations([model])
return yield* this.createType(model); return yield* this.createType(model)
}, },
initType: function* YArrayInitializer(os, model){ initType: function * YArrayInitializer (os, model) {
var valArray = []; var valArray = []
var idArray = yield* Y.Struct.List.map.call(this, model, function(c){ var idArray = yield* Struct.List.map.call(this, model, function (c) {
valArray.push(c.content); valArray.push(c.content)
return JSON.stringify(c.id); return JSON.stringify(c.id)
}); })
return new YArray(os, model.id, idArray, valArray); return new YArray(os, model.id, idArray, valArray)
} }
}); })
})(); })()

View File

@ -1,143 +1,145 @@
/* @flow */ /* global createUsers, wait, Y, compareAllUsers, getRandomNumber, applyRandomTransactions */
/*eslint-env browser,jasmine */ /* eslint-env browser,jasmine */
var numberOfYArrayTests = 80; var numberOfYArrayTests = 80
describe("Array Type", function(){ describe('Array Type', function () {
var y1, y2, y3, flushAll; var y1, y2, y3, flushAll
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000
beforeEach(async function(done){ beforeEach(async function (done) {
await createUsers(this, 5); await createUsers(this, 5)
y1 = this.users[0].root; y1 = this.users[0].root
y2 = this.users[1].root; y2 = this.users[1].root
y3 = this.users[2].root; y3 = this.users[2].root
flushAll = this.users[0].connector.flushAll; flushAll = this.users[0].connector.flushAll
done(); done()
}); })
afterEach(async function(done) { afterEach(async function(done) {
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}); })
describe("Basic tests", function(){ describe('Basic tests', function () {
it("insert three elements, try re-get property", async function(done){ it('insert three elements, try re-get property', async function (done) {
var array = await y1.set("Array", Y.Array); var array = await y1.set('Array', Y.Array)
array.insert(0, [1, 2, 3]); array.insert(0, [1, 2, 3])
array = await y1.get("Array"); // re-get property array = await y1.get('Array') // re-get property
expect(array.toArray()).toEqual([1, 2, 3]); expect(array.toArray()).toEqual([1, 2, 3])
done(); done()
}); })
it("Basic insert in array (handle three conflicts)", async function(done){ it('Basic insert in array (handle three conflicts)', async function (done) {
var l1, l2, l3; await y1.set('Array', Y.Array)
await y1.set("Array", Y.Array); await flushAll()
await flushAll(); var l1 = await y1.get('Array')
(l1 = await y1.get("Array")).insert(0, [0]); l1.insert(0, [0])
(l2 = await y2.get("Array")).insert(0, [1]); var l2 = await y2.get('Array')
(l3 = await y3.get("Array")).insert(0, [2]); l2.insert(0, [1])
await flushAll(); var l3 = await y3.get('Array')
expect(l1.toArray()).toEqual(l2.toArray()); l3.insert(0, [2])
expect(l2.toArray()).toEqual(l3.toArray()); await flushAll()
done(); expect(l1.toArray()).toEqual(l2.toArray())
}); expect(l2.toArray()).toEqual(l3.toArray())
it("Basic insert&delete in array (handle three conflicts)", async function(done){ done()
var l1, l2, l3; })
l1 = await y1.set("Array", Y.Array); it('Basic insert&delete in array (handle three conflicts)', async function (done) {
l1.insert(0, ["x", "y", "z"]); var l1, l2, l3
await flushAll(); l1 = await y1.set('Array', Y.Array)
l1.insert(1, [0]); l1.insert(0, ['x', 'y', 'z'])
l2 = await y2.get("Array"); await flushAll()
l2.delete(0); l1.insert(1, [0])
l2.delete(1); l2 = await y2.get('Array')
l3 = await y3.get("Array"); l2.delete(0)
l3.insert(1, [2]); l2.delete(1)
await flushAll(); l3 = await y3.get('Array')
expect(l1.toArray()).toEqual(l2.toArray()); l3.insert(1, [2])
expect(l2.toArray()).toEqual(l3.toArray()); await flushAll()
expect(l2.toArray()).toEqual([0, 2, "y"]); expect(l1.toArray()).toEqual(l2.toArray())
done(); expect(l2.toArray()).toEqual(l3.toArray())
}); expect(l2.toArray()).toEqual([0, 2, 'y'])
it("Basic insert. Then delete the whole array", async function(done){ done()
var l1, l2, l3; })
l1 = await y1.set("Array", Y.Array); it('Basic insert. Then delete the whole array', async function (done) {
l1.insert(0, ["x", "y", "z"]); var l1, l2, l3
await flushAll(); l1 = await y1.set('Array', Y.Array)
l1.delete(0, 3); l1.insert(0, ['x', 'y', 'z'])
l2 = await y2.get("Array"); await flushAll()
l3 = await y3.get("Array"); l1.delete(0, 3)
await flushAll(); l2 = await y2.get('Array')
expect(l1.toArray()).toEqual(l2.toArray()); l3 = await y3.get('Array')
expect(l2.toArray()).toEqual(l3.toArray()); await flushAll()
expect(l2.toArray()).toEqual([]); expect(l1.toArray()).toEqual(l2.toArray())
done(); expect(l2.toArray()).toEqual(l3.toArray())
}); expect(l2.toArray()).toEqual([])
it("throw insert & delete events", async function(done){ done()
var array = await this.users[0].root.set("array", Y.Array); })
var event; it('throw insert & delete events', async function (done) {
array.observe(function(e){ var array = await this.users[0].root.set('array', Y.Array)
event = e; var event
}); array.observe(function (e) {
array.insert(0, [0]); event = e
})
array.insert(0, [0])
expect(event).toEqual([{ expect(event).toEqual([{
type: "insert", type: 'insert',
object: array, object: array,
index: 0, index: 0,
length: 1 length: 1
}]); }])
array.delete(0); array.delete(0)
expect(event).toEqual([{ expect(event).toEqual([{
type: "delete", type: 'delete',
object: array, object: array,
index: 0, index: 0,
length: 1 length: 1
}]); }])
await wait(50); await wait(50)
done(); done()
}); })
}); })
describe(`${numberOfYArrayTests} Random tests`, function(){ describe(`${numberOfYArrayTests} Random tests`, function () {
var randomArrayTransactions = [ var randomArrayTransactions = [
function insert (array) { function insert (array) {
array.insert(getRandomNumber(array.toArray().length), [getRandomNumber()]); array.insert(getRandomNumber(array.toArray().length), [getRandomNumber()])
}, },
function _delete (array) { function _delete (array) {
var length = array.toArray().length; var length = array.toArray().length
if (length > 0) { if (length > 0) {
array.delete(getRandomNumber(length - 1)); array.delete(getRandomNumber(length - 1))
} }
} }
]; ]
function compareArrayValues(arrays){ function compareArrayValues (arrays) {
var firstArray; var firstArray
for (var l of arrays) { for (var l of arrays) {
var val = l.toArray(); var val = l.toArray()
if (firstArray == null) { if (firstArray == null) {
firstArray = val; firstArray = val
} else { } else {
expect(val).toEqual(firstArray); expect(val).toEqual(firstArray)
} }
} }
} }
beforeEach(async function(done){ beforeEach(async function (done) {
await this.users[0].root.set("Array", Y.Array); await this.users[0].root.set('Array', Y.Array)
await flushAll(); await flushAll()
var promises = []; var promises = []
for (var u = 0; u < this.users.length; u++) { 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); this.arrays = await Promise.all(promises)
done(); done()
}); })
it("arrays.length equals users.length", async function(done){ it('arrays.length equals users.length', async function (done) { // eslint-disable-line
expect(this.arrays.length).toEqual(this.users.length); expect(this.arrays.length).toEqual(this.users.length)
done(); done()
}); })
it(`succeed after ${numberOfYArrayTests} actions`, async function(done){ it(`succeed after ${numberOfYArrayTests} actions`, async function (done) {
await applyRandomTransactions(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests); await applyRandomTransactions(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests)
await flushAll(); await flushAll()
await compareArrayValues(this.arrays); await compareArrayValues(this.arrays)
done(); done()
}); })
}); })
}); })

View File

@ -1,76 +1,78 @@
(function(){ /* global EventHandler, Y, CustomType, copyObject, compareIds */
;(function () {
class YMap { class YMap {
constructor (os, model) { constructor (os, model) {
this._model = model.id; this._model = model.id
this.os = os; this.os = os
this.map = copyObject(model.map); this.map = copyObject(model.map)
this.contents = {}; this.contents = {}
this.opContents = {}; this.opContents = {}
this.eventHandler = new EventHandler( ops =>{ this.eventHandler = new EventHandler(ops => {
var userEvents = []; var userEvents = []
for (var i in ops) { for (var i in ops) {
var op = ops[i]; var op = ops[i]
var oldValue; var oldValue
// key is the name to use to access (op)content // 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 // compute oldValue
if (this.opContents[key] != null) { if (this.opContents[key] != null) {
let prevType = this.opContents[key]; let prevType = this.opContents[key]
oldValue = () => { //eslint-disable-line oldValue = () => {// eslint-disable-line
let def = Promise.defer(); let def = Promise.defer()
this.os.requestTransaction(function*(){//eslint-disable-line this.os.requestTransaction(function *() {// eslint-disable-line
def.resolve(yield* this.getType(prevType)); def.resolve(yield* this.getType(prevType))
}); })
return def.promise; return def.promise
}; }
} else { } else {
oldValue = this.contents[key]; oldValue = this.contents[key]
} }
// compute op event // compute op event
if (op.struct === "Insert"){ if (op.struct === 'Insert') {
if (op.left === null) { if (op.left === null) {
if (op.opContent != null) { if (op.opContent != null) {
delete this.contents[key]; delete this.contents[key]
this.opContents[key] = op.opContent; this.opContents[key] = op.opContent
} else { } else {
delete this.opContents[key]; delete this.opContents[key]
this.contents[key] = op.content; this.contents[key] = op.content
} }
this.map[key] = op.id; this.map[key] = op.id
var insertEvent = { var insertEvent = {
name: key, name: key,
object: this 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 (compareIds(this.map[key], op.target)) {
if (this.opContents[key] != null) { if (this.opContents[key] != null) {
delete this.opContents[key]; delete this.opContents[key]
} else { } else {
delete this.contents[key]; delete this.contents[key]
} }
var deleteEvent = { var deleteEvent = {
name: key, name: key,
object: this, object: this,
oldValue: oldValue, oldValue: oldValue,
type: "delete" type: 'delete'
}; }
userEvents.push(deleteEvent); userEvents.push(deleteEvent)
} }
} else { } else {
throw new Error("Unexpected Operation!"); throw new Error('Unexpected Operation!')
} }
} }
this.eventHandler.callUserEventListeners(userEvents); this.eventHandler.callUserEventListeners(userEvents)
}); })
} }
get (key) { get (key) {
// return property. // return property.
@ -78,34 +80,34 @@
// if property is a type, return a promise // if property is a type, return a promise
if (this.opContents[key] == null) { if (this.opContents[key] == null) {
if (key == null) { if (key == null) {
return copyObject(this.contents); return copyObject(this.contents)
} else { } else {
return this.contents[key]; return this.contents[key]
} }
} else { } else {
let def = Promise.defer(); let def = Promise.defer()
var oid = this.opContents[key]; var oid = this.opContents[key]
this.os.requestTransaction(function*(){ this.os.requestTransaction(function *() {
def.resolve(yield* this.getType(oid)); def.resolve(yield* this.getType(oid))
}); })
return def.promise; return def.promise
} }
} }
delete (key) { delete (key) {
var right = this.map[key]; var right = this.map[key]
if (right != null) { if (right != null) {
var del = { var del = {
target: right, target: right,
struct: "Delete" struct: 'Delete'
}; }
var eventHandler = this.eventHandler; var eventHandler = this.eventHandler
var modDel = copyObject(del); var modDel = copyObject(del)
modDel.key = key; modDel.key = key
eventHandler.awaitAndPrematurelyCall([modDel]); eventHandler.awaitAndPrematurelyCall([modDel])
this.os.requestTransaction(function*(){ this.os.requestTransaction(function *() {
yield* this.applyCreatedOperations([del]); yield* this.applyCreatedOperations([del])
eventHandler.awaitedLastDeletes(1); eventHandler.awaitedLastDeletes(1)
}); })
} }
} }
set (key, value) { set (key, value) {
@ -113,108 +115,108 @@
// if property is a type, return a promise // if property is a type, return a promise
// if not, apply immediately on this type an call event // if not, apply immediately on this type an call event
var right = this.map[key] || null; var right = this.map[key] || null
var insert = { var insert = {
left: null, left: null,
right: right, right: right,
origin: null, origin: null,
parent: this._model, parent: this._model,
parentSub: key, parentSub: key,
struct: "Insert" 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);
} }
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) { observe (f) {
this.eventHandler.addUserEventListener(f); this.eventHandler.addUserEventListener(f)
} }
unobserve (f) { unobserve (f) {
this.eventHandler.removeUserEventListener(f); this.eventHandler.removeUserEventListener(f)
} }
observePath (path, f) { observePath (path, f) {
var self = this; var self = this
if (path.length === 0) { if (path.length === 0) {
this.observe(f); this.observe(f)
return Promise.resolve(function(){ return Promise.resolve(function () {
self.unobserve(f); self.unobserve(f)
}); })
} else { } else {
var deleteChildObservers; var deleteChildObservers
var resetObserverPath = function(){ var resetObserverPath = function () {
var promise = self.get(path[0]); var promise = self.get(path[0])
if (!promise instanceof Promise) { if (!promise instanceof Promise) {
// its either not defined or a premitive value // 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 promise.then(function (map) {
return map.observePath(path.slice(1), f); return map.observePath(path.slice(1), f)
}).then(function(_deleteChildObservers){ }).then(function (_deleteChildObservers) {
deleteChildObservers = _deleteChildObservers; deleteChildObservers = _deleteChildObservers
return Promise.resolve(); return Promise.resolve()
}); })
}; }
var observer = function(events){ var observer = function (events) {
for (var e in events) { for (var e in events) {
var event = events[e]; var event = events[e]
if (event.name === path[0]) { if (event.name === path[0]) {
deleteChildObservers(); deleteChildObservers()
if (event.type === "add" || event.type === "update") { if (event.type === 'add' || event.type === 'update') {
resetObserverPath(); resetObserverPath()
} }
} }
} }
}; }
self.observe(observer); self.observe(observer)
return resetObserverPath().then( return resetObserverPath().then(
Promise.resolve(function(){ Promise.resolve(function () {
deleteChildObservers(); deleteChildObservers()
self.unobserve(observer); self.unobserve(observer)
}) })
); )
} }
} }
*_changed (transaction, op) { * _changed (transaction, op) {
if (op.struct === "Delete") { if (op.struct === 'Delete') {
op.key = (yield* transaction.getOperation(op.target)).parentSub; op.key = (yield* transaction.getOperation(op.target)).parentSub
} }
this.eventHandler.receivedOp(op); this.eventHandler.receivedOp(op)
} }
} }
Y.Map = new CustomType({ Y.Map = new CustomType({
class: YMap, class: YMap,
createType: function* YMapCreator(){ createType: function * YMapCreator () {
var model = { var model = {
map: {}, map: {},
struct: "Map", struct: 'Map',
type: "Map", type: 'Map',
id: this.store.getNextOpId() id: this.store.getNextOpId()
}; }
yield* this.applyCreatedOperations([model]); yield* this.applyCreatedOperations([model])
return yield* this.createType(model); return yield* this.createType(model)
}, },
initType: function* YMapInitializer(os, model){ initType: function * YMapInitializer (os, model) { // eslint-disable-line
return new YMap(os, model); return new YMap(os, model)
} }
}); })
})(); })()

View File

@ -1,207 +1,207 @@
/* @flow */ /* global createUsers, Y, compareAllUsers, getRandomNumber, applyRandomTransactions */
/*eslint-env browser,jasmine */ /* eslint-env browser,jasmine */
var numberOfYMapTests = 100; var numberOfYMapTests = 100
describe("Map Type", function(){ describe('Map Type', function () {
var y1, y2, y3, y4, flushAll; var y1, y2, y3, y4, flushAll
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000
beforeEach(async function(done){ beforeEach(async function (done) {
await createUsers(this, 5); await createUsers(this, 5)
y1 = this.users[0].root; y1 = this.users[0].root
y2 = this.users[1].root; y2 = this.users[1].root
y3 = this.users[2].root; y3 = this.users[2].root
y4 = this.users[3].root; y4 = this.users[3].root
flushAll = this.users[0].connector.flushAll; flushAll = this.users[0].connector.flushAll
done(); done()
}); })
afterEach(async function(done) { afterEach(async function(done) {
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}, 5000); }, 5000)
describe("Basic tests", function(){ describe('Basic tests', function () {
it("Basic get&set of Map property (converge via sync)", async function(done){ it('Basic get&set of Map property (converge via sync)', async function (done) {
y1.set("stuff", "stuffy"); y1.set('stuff', 'stuffy')
expect(y1.get("stuff")).toEqual("stuffy"); expect(y1.get('stuff')).toEqual('stuffy')
await flushAll(); await flushAll()
for (var key in this.users) { for (var key in this.users) {
var u = this.users[key].root; var u = this.users[key].root
expect(u.get("stuff")).toEqual("stuffy"); expect(u.get('stuff')).toEqual('stuffy')
} }
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}); })
it("Map can set custom types (Map)", async function(done){ it('Map can set custom types (Map)', async function (done) {
var map = await y1.set("Map", Y.Map); var map = await y1.set('Map', Y.Map)
map.set("one", 1); map.set('one', 1)
map = await y1.get("Map"); map = await y1.get('Map')
expect(map.get("one")).toEqual(1); expect(map.get('one')).toEqual(1)
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}); })
it("Map can set custom types (Array)", async function(done){ it('Map can set custom types (Array)', async function (done) {
var array = await y1.set("Array", Y.Array); var array = await y1.set('Array', Y.Array)
array.insert(0, [1, 2, 3]); array.insert(0, [1, 2, 3])
array = await y1.get("Array"); array = await y1.get('Array')
expect(array.toArray()).toEqual([1, 2, 3]); expect(array.toArray()).toEqual([1, 2, 3])
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}); })
it("Basic get&set of Map property (converge via update)", async function(done){ it('Basic get&set of Map property (converge via update)', async function (done) {
await flushAll(); await flushAll()
y1.set("stuff", "stuffy"); y1.set('stuff', 'stuffy')
expect(y1.get("stuff")).toEqual("stuffy"); expect(y1.get('stuff')).toEqual('stuffy')
await flushAll(); await flushAll()
for (var key in this.users) { for (var key in this.users) {
var r = this.users[key].root; var r = this.users[key].root
expect(r.get("stuff")).toEqual("stuffy"); expect(r.get('stuff')).toEqual('stuffy')
} }
done(); done()
}); })
it("Basic get&set of Map property (handle conflict)", async function(done){ it('Basic get&set of Map property (handle conflict)', async function (done) {
await flushAll(); await flushAll()
y1.set("stuff", "c0"); y1.set('stuff', 'c0')
y2.set("stuff", "c1"); y2.set('stuff', 'c1')
await flushAll(); await flushAll()
for (var key in this.users) { for (var key in this.users) {
var u = this.users[key]; var u = this.users[key]
expect(u.root.get("stuff")).toEqual("c0"); expect(u.root.get('stuff')).toEqual('c0')
} }
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}); })
it("Basic get&set&delete of Map property (handle conflict)", async function(done){ it('Basic get&set&delete of Map property (handle conflict)', async function (done) {
await flushAll(); await flushAll()
y1.set("stuff", "c0"); y1.set('stuff', 'c0')
y1.delete("stuff"); y1.delete('stuff')
y2.set("stuff", "c1"); y2.set('stuff', 'c1')
await flushAll(); await flushAll()
for (var key in this.users) { for (var key in this.users) {
var u = this.users[key]; var u = this.users[key]
expect(u.root.get("stuff")).toBeUndefined(); expect(u.root.get('stuff')).toBeUndefined()
} }
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}); })
it("Basic get&set of Map property (handle three conflicts)", async function(done){ it('Basic get&set of Map property (handle three conflicts)', async function (done) {
await flushAll(); await flushAll()
y1.set("stuff", "c0"); y1.set('stuff', 'c0')
y2.set("stuff", "c1"); y2.set('stuff', 'c1')
y2.set("stuff", "c2"); y2.set('stuff', 'c2')
y3.set("stuff", "c3"); y3.set('stuff', 'c3')
await flushAll(); await flushAll()
for (var key in this.users) { for (var key in this.users) {
var u = this.users[key]; var u = this.users[key]
expect(u.root.get("stuff")).toEqual("c0"); expect(u.root.get('stuff')).toEqual('c0')
} }
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}); })
it("Basic get&set&delete of Map property (handle three conflicts)", async function(done){ it('Basic get&set&delete of Map property (handle three conflicts)', async function (done) {
await flushAll(); await flushAll()
y1.set("stuff", "c0"); y1.set('stuff', 'c0')
y2.set("stuff", "c1"); y2.set('stuff', 'c1')
y2.set("stuff", "c2"); y2.set('stuff', 'c2')
y3.set("stuff", "c3"); y3.set('stuff', 'c3')
await flushAll(); await flushAll()
y1.set("stuff", "deleteme"); y1.set('stuff', 'deleteme')
y1.delete("stuff"); y1.delete('stuff')
y2.set("stuff", "c1"); y2.set('stuff', 'c1')
y3.set("stuff", "c2"); y3.set('stuff', 'c2')
y4.set("stuff", "c3"); y4.set('stuff', 'c3')
await flushAll(); await flushAll()
for (var key in this.users) { for (var key in this.users) {
var u = this.users[key]; var u = this.users[key]
expect(u.root.get("stuff")).toBeUndefined(); expect(u.root.get('stuff')).toBeUndefined()
} }
await compareAllUsers(this.users); await compareAllUsers(this.users)
done(); done()
}); })
it("throws add & update & delete events (with type and primitive content)", async function(done){ it('throws add & update & delete events (with type and primitive content)', async function (done) {
var event; var event
await flushAll(); await flushAll()
y1.observe(function(e){ y1.observe(function (e) {
event = e; // just put it on event, should be thrown synchronously anyway event = e // just put it on event, should be thrown synchronously anyway
}); })
y1.set("stuff", 4); y1.set('stuff', 4)
expect(event).toEqual([{ expect(event).toEqual([{
type: "add", type: 'add',
object: y1, object: y1,
name: "stuff" name: 'stuff'
}]); }])
// update, oldValue is in contents // update, oldValue is in contents
await y1.set("stuff", Y.Array); await y1.set('stuff', Y.Array)
expect(event).toEqual([{ expect(event).toEqual([{
type: "update", type: 'update',
object: y1, object: y1,
name: "stuff", name: 'stuff',
oldValue: 4 oldValue: 4
}]); }])
y1.get("stuff").then(function(replacedArray){ y1.get('stuff').then(function (replacedArray) {
// update, oldValue is in opContents // update, oldValue is in opContents
y1.set("stuff", 5); y1.set('stuff', 5)
var getYArray = event[0].oldValue; var getYArray = event[0].oldValue
expect(typeof getYArray.constructor === "function").toBeTruthy(); expect(typeof getYArray.constructor === 'function').toBeTruthy()
getYArray().then(function(array){ getYArray().then(function (array) {
expect(array).toEqual(replacedArray); expect(array).toEqual(replacedArray)
// delete // delete
y1.delete("stuff"); y1.delete('stuff')
expect(event).toEqual([{ expect(event).toEqual([{
type: "delete", type: 'delete',
name: "stuff", name: 'stuff',
object: y1, object: y1,
oldValue: 5 oldValue: 5
}]); }])
done(); done()
}); })
}); })
}); })
}); })
describe(`${numberOfYMapTests} Random tests`, function(){ describe(`${numberOfYMapTests} Random tests`, function () {
var randomMapTransactions = [ var randomMapTransactions = [
function set (map) { function set (map) {
map.set("somekey", getRandomNumber()); map.set('somekey', getRandomNumber())
}, },
function delete_ (map) { function delete_ (map) {
map.delete("somekey"); map.delete('somekey')
} }
]; ]
function compareMapValues(maps){ function compareMapValues (maps) {
var firstMap; var firstMap
for (var map of maps) { for (var map of maps) {
var val = map.get(); var val = map.get()
if (firstMap == null) { if (firstMap == null) {
firstMap = val; firstMap = val
} else { } else {
expect(val).toEqual(firstMap); expect(val).toEqual(firstMap)
} }
} }
} }
beforeEach(async function(done){ beforeEach(async function (done) {
await y1.set("Map", Y.Map); await y1.set('Map', Y.Map)
await flushAll(); await flushAll()
var promises = []; var promises = []
for (var u = 0; u < this.users.length; u++) { 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); this.maps = await Promise.all(promises)
done(); done()
}); })
it(`succeed after ${numberOfYMapTests} actions`, async function(done){ it(`succeed after ${numberOfYMapTests} actions`, async function (done) {
await applyRandomTransactions(this.users, this.maps, randomMapTransactions, numberOfYMapTests); await applyRandomTransactions(this.users, this.maps, randomMapTransactions, numberOfYMapTests)
await flushAll(); await flushAll()
await compareMapValues(this.maps); await compareMapValues(this.maps)
done(); done()
}); })
}); })
}); })

View File

@ -1,287 +1,288 @@
/* global Y, CustomType */
(function(){ ;(function () {
class YTextBind extends Y.Array.class { class YTextBind extends Y . Array . class {
constructor (os, _model, idArray, valArray) { constructor (os, _model, idArray, valArray) {
super(os, _model, idArray, valArray); super(os, _model, idArray, valArray)
this.textfields = []; this.textfields = []
} }
toString () { toString () {
return this.valArray.join(""); return this.valArray.join('')
} }
insert (pos, content) { insert (pos, content) {
super(pos, content.split("")); super(pos, content.split(''))
} }
bind (textfield, domRoot) { bind (textfield, domRoot) {
domRoot = domRoot || window; //eslint-disable-line domRoot = domRoot || window; // eslint-disable-line
if (domRoot.getSelection == null) { if (domRoot.getSelection == null) {
domRoot = window;//eslint-disable-line domRoot = window;// eslint-disable-line
} }
// don't duplicate! // don't duplicate!
for (var t in this.textfields) { for (var t in this.textfields) {
if (this.textfields[t] === textfield) { if (this.textfields[t] === textfield) {
return; 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; writeRange = function (range) {
textfield.value = this.toString(); writeContent(word.toString())
this.textfields.push(textfield); var textnode = textfield.childNodes[0]
var createRange, writeRange, writeContent; if (range.isReal && textnode != null) {
if(textfield.selectionStart != null && textfield.setSelectionRange != null) { if (range.left < 0) {
createRange = function (fix) { range.left = 0
var left = textfield.selectionStart;
var right = textfield.selectionEnd;
if (fix != null) {
left = fix(left);
right = fix(right);
} }
return { range.right = Math.max(range.left, range.right)
left: left, if (range.right > textnode.length) {
right: right range.right = textnode.length
}; }
}; range.left = Math.min(range.left, range.right)
writeRange = function (range) { var r = document.createRange(); // eslint-disable-line
writeContent(word.toString()); r.setStart(textnode, range.left)
textfield.setSelectionRange(range.left, range.right); r.setEnd(textnode, range.right)
}; var s = window.getSelection(); // eslint-disable-line
writeContent = function (content){ s.removeAllRanges()
textfield.value = content; 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 += '&nbsp;'
}
}
}
}
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 { } else {
createRange = function (fix) { char = window.String.fromCharCode(event.keyCode); // eslint-disable-line
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 += "&nbsp;";
}
}
};
} }
writeContent(this.toString()); if (char.length > 1) {
return true
this.observe(function (events) { } else if (char.length > 0) {
for(var e in events) { var r = createRange()
var event = events[e]; var pos = Math.min(r.left, r.right, word.length)
if (!creatorToken) { var diff = Math.abs(r.right - r.left)
var oPos, fix; word.delete(pos, diff)
if( event.type === "insert") { word.insert(pos, char)
oPos = event.index; r.left = pos + char.length
fix = function (cursor) {//eslint-disable-line r.right = r.left
if (cursor <= oPos) { writeRange(r)
return cursor; }
} else { event.preventDefault()
cursor += 1; creatorToken = false
return cursor; return false
} }
}; textfield.onpaste = function (event) {
var r = createRange(fix); if (word.is_deleted) {
writeRange(r); // if word is deleted, do not do anything ever again
} else if (event.type === "delete") { textfield.onpaste = null
oPos = event.index; return true
fix = function (cursor){//eslint-disable-line }
if (cursor < oPos) { event.preventDefault()
return cursor; }
} else { textfield.oncut = function (event) {
cursor -= 1; if (word.is_deleted) {
return cursor; // if word is deleted, do not do anything ever again
} textfield.oncut = null
}; return true
r = createRange(fix); }
writeRange(r); 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()
// consume all text-insert changes. creatorToken = false
textfield.onkeypress = function (event) { return false
if (word.is_deleted) { } else if (event.keyCode != null && event.keyCode === 46) { // Delete
// if word is deleted, do not do anything ever again if (diff > 0) {
textfield.onkeypress = null; word.delete(pos, diff)
return true; r.left = pos
} r.right = pos
creatorToken = true; writeRange(r)
var char;
if (event.keyCode === 13) {
char = "\n";
} else if (event.key != null) {
if (event.charCode === 32) {
char = " ";
} else {
char = event.key;
}
} else { } 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) { event.preventDefault()
return true; creatorToken = false
} else if (char.length > 0) { return false
var r = createRange(); } else {
var pos = Math.min(r.left, r.right, word.length); creatorToken = false
var diff = Math.abs(r.right - r.left); return true
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;
}
};
} }
} }
Y.TextBind = new CustomType({ Y.TextBind = new CustomType({
class: YTextBind, class: YTextBind,
createType: function* YTextBindCreator () { createType: function * YTextBindCreator () {
var model = { var model = {
start: null, start: null,
end: null, end: null,
struct: "List", struct: 'List',
type: "TextBind", type: 'TextBind',
id: this.store.getNextOpId() id: this.store.getNextOpId()
}; }
yield* this.applyCreatedOperations([model]); yield* this.applyCreatedOperations([model])
return yield* this.createType(model); return yield* this.createType(model)
}, },
initType: function* YTextBindInitializer(os, model){ initType: function * YTextBindInitializer (os, model) {
var valArray = []; var valArray = []
var idArray = yield* Y.Struct.List.map.call(this, model, function(c){ var idArray = yield* Y.Struct.List.map.call(this, model, function (c) {
valArray.push(c.content); valArray.push(c.content)
return JSON.stringify(c.id); return JSON.stringify(c.id)
}); })
return new YTextBind(os, model.id, idArray, valArray); return new YTextBind(os, model.id, idArray, valArray)
} }
}); })
})(); })()

View File

@ -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) { constructor (onevent) {
this.waiting = []; this.waiting = []
this.awaiting = 0; this.awaiting = 0
this.onevent = onevent; this.onevent = onevent
this.userEventListeners = []; this.userEventListeners = []
} }
receivedOp (op) { receivedOp (op) {
if (this.awaiting <= 0) { if (this.awaiting <= 0) {
this.onevent([op]); this.onevent([op])
} else { } else {
this.waiting.push(copyObject(op)); this.waiting.push(copyObject(op))
} }
} }
awaitAndPrematurelyCall (ops) { awaitAndPrematurelyCall (ops) {
this.awaiting++; this.awaiting++
this.onevent(ops); this.onevent(ops)
} }
addUserEventListener (f) { addUserEventListener (f) {
this.userEventListeners.push(f); this.userEventListeners.push(f)
} }
removeUserEventListener (f) { removeUserEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(function(g){ this.userEventListeners = this.userEventListeners.filter(function (g) {
return f !== g; return f !== g
}); })
} }
removeAllUserEventListeners () { removeAllUserEventListeners () {
this.userEventListeners = []; this.userEventListeners = []
} }
callUserEventListeners (event) { callUserEventListeners (event) {
for (var i in this.userEventListeners) { for (var i in this.userEventListeners) {
try { try {
this.userEventListeners[i](event); this.userEventListeners[i](event)
} catch (e) { } 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) { 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++) { 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--) { 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)) { if (compareIds(op.left, w.id)) {
// include the effect of op in w // include the effect of op in w
w.right = op.id; w.right = op.id
// exclude the effect of w in op // exclude the effect of w in op
op.left = w.left; op.left = w.left
} else if (compareIds(op.right, w.id)) { } else if (compareIds(op.right, w.id)) {
// similar.. // similar..
w.left = op.id; w.left = op.id
op.right = w.right; op.right = w.right
} }
} }
} }
this.tryCallEvents(); this.tryCallEvents()
} }
awaitedLastDeletes (n, newLeft) { 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) { for (var j in ops) {
var del = ops[j]; var del = ops[j]
if (newLeft != null) { if (newLeft != null) {
for (var i in this.waiting) { for (var i in this.waiting) {
let w = this.waiting[i]; let w = this.waiting[i]
// We will just care about w.left // We will just care about w.left
if (compareIds(del.target, w.left)) { if (compareIds(del.target, w.left)) {
del.left = newLeft; del.left = newLeft
} }
} }
} }
} }
this.tryCallEvents(); this.tryCallEvents()
} }
tryCallEvents () { tryCallEvents () {
this.awaiting--; this.awaiting--
if (this.awaiting <= 0 && this.waiting.length > 0) { if (this.awaiting <= 0 && this.waiting.length > 0) {
var events = this.waiting; var events = this.waiting
this.waiting = []; this.waiting = []
this.onevent(events); this.onevent(events)
} }
} }
} }
class CustomType { //eslint-disable-line class CustomType { // eslint-disable-line
constructor (def) { constructor (def) {
if (def.createType == null if (def.createType == null ||
|| def.initType == null def.initType == null ||
|| def.class == 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.createType = def.createType
this.initType = def.initType; this.initType = def.initType
this.class = def.class; this.class = def.class
} }
} }