delete support for Array & Map

This commit is contained in:
Kevin Jahns 2015-07-14 20:50:53 +02:00
parent 66a7d2720d
commit 6b153896dd
10 changed files with 256 additions and 90 deletions

View File

@ -130,8 +130,8 @@ gulp.task("build_jasmine_browser", function(){
gulp.task("develop", ["build_jasmine_browser", "build"], function(){ gulp.task("develop", ["build_jasmine_browser", "build"], function(){
gulp.watch(files.test, ["build_jasmine_browser"]); gulp.watch(files.test, ["build_jasmine_browser"]);
gulp.watch(files.test, ["test"]); //gulp.watch(files.test, ["test"]);
gulp.watch(files.test, ["build"]); //gulp.watch(files.test, ["build"]);
return gulp.src("build/jasmine_browser.js") return gulp.src("build/jasmine_browser.js")
.pipe(watch("build/jasmine_browser.js")) .pipe(watch("build/jasmine_browser.js"))

View File

@ -2,7 +2,6 @@ var globalRoom = {
users: {}, users: {},
buffers: {}, buffers: {},
removeUser: function(user : AbstractConnector){ removeUser: function(user : AbstractConnector){
for (var i in this.users) { for (var i in this.users) {
this.users[i].userLeft(user); this.users[i].userLeft(user);
} }

View File

@ -73,10 +73,9 @@ function compareAllUsers(users){//eslint-disable-line
} }
function createUsers(self, numberOfUsers, done) {//eslint-disable-line function createUsers(self, numberOfUsers, done) {//eslint-disable-line
if (self.users != null) { //destroy old users
for (var y of self.users) { for (var u in globalRoom.users) {//eslint-disable-line
y.destroy(); globalRoom.users[u].y.destroy()//eslint-disable-line
}
} }
self.users = []; self.users = [];

View File

@ -9,7 +9,7 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars
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].create(this.store, op); t = yield* Y[op.type].create.call(this, this.store, op);
this.store.initializedTypes[sid] = t; this.store.initializedTypes[sid] = t;
} }
} }
@ -28,7 +28,7 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars
state.clock++; state.clock++;
yield* this.setState(state); yield* this.setState(state);
this.os.add(op); this.os.add(op);
this.store.operationAdded(op); yield* this.store.operationAdded(this, op);
return true; return true;
} else if (op.id[1] < state.clock) { } else if (op.id[1] < state.clock) {
return false; return false;
@ -41,7 +41,7 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars
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* Struct[op.struct].execute.call(this, op); yield* Struct[op.struct].execute.call(this, op);
send.push(Struct[op.struct].encode(op)); send.push(copyObject(Struct[op.struct].encode(op)));
} }
this.store.y.connector.broadcast({ this.store.y.connector.broadcast({
type: "update", type: "update",
@ -161,7 +161,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
}); });
} }
// called by a transaction when an operation is added // called by a transaction when an operation is added
operationAdded (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];
@ -178,7 +178,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
// 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) {
t._changed(op); yield* t._changed(transaction, copyObject(op));
} }
} }
removeParentListener (id, f) { removeParentListener (id, f) {

View File

@ -120,7 +120,7 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
} }
requestTransaction (makeGen : Function) { requestTransaction (makeGen : Function) {
var t = new Transaction(this); var t = new Transaction(this);
var gen = makeGen.call(t, t.getType(["_", 0]).next().value); 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") {

View File

@ -35,7 +35,7 @@ function compareIds(id1, id2) {
} }
var Struct = { var Struct = {
/* /* This Operations does _not_ have an id!
{ {
target: Id target: Id
} }
@ -48,12 +48,16 @@ var Struct = {
return [op.target]; return [op.target];
}, },
execute: function* (op) { execute: function* (op) {
if ((yield* this.addOperation(op)) === false) {
return;
}
var target = yield* this.getOperation(op.target); var target = yield* this.getOperation(op.target);
target.deleted = true; if (!target.deleted) {
yield* this.setOperation(target); target.deleted = true;
yield* this.setOperation(target);
var t = this.store.initializedTypes[JSON.stringify(target.parent)];
if (t != null) {
yield* t._changed(this, copyObject(op));
}
}
} }
}, },
Insert: { Insert: {
@ -280,14 +284,6 @@ var Struct = {
} }
return res; return res;
}, },
delete: function* (op, pos) {
var ref = yield* Struct.List.ref.call(this, op, pos);
if (ref != null) {
yield* Struct.Delete.create.call(this, {
target: ref.id
});
}
},
map: function* (o : Op, f : Function) : Array<any> { map: function* (o : Op, f : Function) : Array<any> {
o = o.start; o = o.start;
var res = []; var res = [];
@ -299,31 +295,6 @@ var Struct = {
o = operation.right; o = operation.right;
} }
return res; return res;
},
insert: function* (op, pos : number, contents : Array<any>) {
var left, right;
if (pos === 0) {
left = null;
right = op.start;
} else {
var ref = yield* Struct.List.ref.call(this, op, pos - 1);
if (ref === null) {
left = op.end;
right = null;
} else {
left = ref.id;
right = ref.right;
}
}
for (var key in contents) {
var insert = {
left: left,
right: right,
content: contents[key],
parent: op.id
};
left = (yield* Struct.Insert.create.call(this, insert)).id;
}
} }
}, },
Map: { Map: {

View File

@ -12,18 +12,28 @@
this.eventHandler = new EventHandler( ops =>{ this.eventHandler = new EventHandler( ops =>{
for (var i in ops) { for (var i in ops) {
var op = ops[i]; var op = ops[i];
var pos; if (op.struct === "Insert") {
if (op.right === null) { let pos;
pos = this.idArray.length; // we check op.left only!,
// because op.right might not be defined when this is called
if (op.left === null) {
pos = 0;
} else {
var sid = JSON.stringify(op.left);
pos = this.idArray.indexOf(sid) + 1;
if (pos <= 0) {
throw new Error("Unexpected operation!");
}
}
this.idArray.splice(pos, 0, JSON.stringify(op.id));
this.valArray.splice(pos, 0, op.content);
} else if (op.struct === "Delete") {
let pos = this.idArray.indexOf(JSON.stringify(op.target));
this.idArray.splice(pos, 1);
this.valArray.splice(pos, 1);
} else { } else {
var sid = JSON.stringify(op.right); throw new Error("Unexpected struct!");
pos = this.idArray.indexOf(sid);
} }
if (pos < 0) {
throw new Error("Unexpected operation!");
}
this.idArray.splice(pos, 0, JSON.stringify(op.id));
this.valArray.splice(pos, 0, op.content);
} }
}); });
} }
@ -50,7 +60,6 @@
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 mostRight = pos === this.idArray.length ? null : JSON.parse(this.idArray[pos]);
var ops = []; var ops = [];
var prevId = mostLeft; var prevId = mostLeft;
@ -58,7 +67,9 @@
var op = { var op = {
left: prevId, left: prevId,
origin: prevId, origin: prevId,
right: mostRight, // right: mostRight,
// NOTE: I intentionally do not define right here, because it could be deleted
// 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",
@ -70,19 +81,58 @@
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.
var mostRight;
if (mostLeft != null) {
mostRight = (yield* this.getOperation(mostLeft)).right;
} else {
mostRight = (yield* this.getOperation(ops[0].parent)).start;
}
for (var j in ops) {
ops[j].right = mostRight;
}
yield* this.applyCreatedOperations(ops); yield* this.applyCreatedOperations(ops);
eventHandler.awaitedLastOp(ops.length); eventHandler.awaitedLastInserts(ops.length);
}); });
} }
*delete (pos) { delete (pos, length = 1) {
if (typeof length !== "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!");
} }
var t = yield "transaction"; if (pos + length > this.idArray.length || pos < 0 || length < 0) {
var model = yield* t.getOperation(this._model); throw new Error("The deletion range exceeds the range of the array!");
yield* Y.Struct.Array.delete.call(t, model, pos); }
var eventHandler = this.eventHandler;
var newLeft = pos > 0 ? JSON.parse(this.idArray[pos - 1]) : null;
var dels = [];
for (var i = 0; i < length; i++) {
dels.push({
target: JSON.parse(this.idArray[pos + i]),
struct: "Delete"
});
}
eventHandler.awaitAndPrematurelyCall(dels);
this.os.requestTransaction(function*(){
yield* this.applyCreatedOperations(dels);
eventHandler.awaitedLastDeletes(dels.length, newLeft);
});
} }
_changed (op) { *_changed (transaction, op) {
if (op.struct === "Insert") {
var l = op.left;
var left;
while (l != null) {
left = yield* transaction.getOperation(l);
if (!left.deleted) {
break;
}
l = left.left;
}
op.left = l;
}
this.eventHandler.receivedOp(op); this.eventHandler.receivedOp(op);
} }
} }

View File

@ -1,10 +1,10 @@
/* @flow */ /* @flow */
/*eslint-env browser,jasmine */ /*eslint-env browser,jasmine */
var numberOfYArrayTests = 10; var numberOfYArrayTests = 20;
describe("Array Type", function(){ describe("Array Type", function(){
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
beforeEach(function(done){ beforeEach(function(done){
createUsers(this, 5, done); createUsers(this, 5, done);
}); });
@ -42,11 +42,64 @@ describe("Array Type", function(){
done(); done();
}); });
}); });
it("Basic insert&delete in array (handle three conflicts)", function(done){
var y = this.users[0];
var l1, l2, l3;
y.root.set("Array", Y.Array).then((array)=>{
l1 = array;
l1.insert(0, ["x", "y", "z"]);
y.connector.flushAll();
l1.insert(1, [0]);
return this.users[1].root.get("Array");
}).then((array)=>{
l2 = array;
l2.delete(0);
l2.delete(1);
return this.users[2].root.get("Array");
}).then((array)=>{
l3 = array;
l3.insert(1, [2]);
y.connector.flushAll();
expect(l1.toArray()).toEqual(l2.toArray());
expect(l2.toArray()).toEqual(l3.toArray());
expect(l2.toArray()).toEqual([0, 2, "y"]);
compareAllUsers(this.users);
done();
});
});
it("Basic insert. Then delete the whole array", function(done){
var y = this.users[0];
var l1, l2, l3;
y.root.set("Array", Y.Array).then((array)=>{
l1 = array;
l1.insert(0, ["x", "y", "z"]);
y.connector.flushAll();
l1.delete(0, 3);
return this.users[1].root.get("Array");
}).then((array)=>{
l2 = array;
return this.users[2].root.get("Array");
}).then((array)=>{
l3 = array;
y.connector.flushAll();
expect(l1.toArray()).toEqual(l2.toArray());
expect(l2.toArray()).toEqual(l3.toArray());
expect(l2.toArray()).toEqual([]);
compareAllUsers(this.users);
done();
});
});
}); });
describe("Random tests", function(){ describe(`${numberOfYArrayTests} Random tests`, function(){
var randomMapTransactions = [ 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) {
var length = array.toArray().length;
if (length > 0) {
array.delete(getRandomNumber(length - 1));
}
} }
]; ];
function compareArrayValues(arrays){ function compareArrayValues(arrays){
@ -82,7 +135,7 @@ describe("Array Type", function(){
expect(this.arrays.length).toEqual(this.users.length); expect(this.arrays.length).toEqual(this.users.length);
}); });
it(`succeed after ${numberOfYArrayTests} actions`, function(done){ it(`succeed after ${numberOfYArrayTests} actions`, function(done){
applyRandomTransactions(this.users, this.arrays, randomMapTransactions, numberOfYArrayTests); applyRandomTransactions(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests);
setTimeout(()=>{ setTimeout(()=>{
compareAllUsers(this.users); compareAllUsers(this.users);
compareArrayValues(this.arrays); compareArrayValues(this.arrays);

View File

@ -18,12 +18,12 @@ class EventHandler {
this.awaiting++; this.awaiting++;
this.onevent(ops); this.onevent(ops);
} }
awaitedLastOp (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--) {
var 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;
@ -36,7 +36,25 @@ class EventHandler {
} }
} }
} }
this.tryCallEvents();
}
awaitedLastDeletes (n, newLeft) {
var ops = this.waiting.splice(this.waiting.length - n);
for (var j in ops) {
var del = ops[j];
if (newLeft != null) {
for (var i in this.waiting) {
let w = this.waiting[i];
// We will just care about w.left
if (compareIds(del.target, w.left)) {
del.left = newLeft;
}
}
}
}
this.tryCallEvents();
}
tryCallEvents () {
this.awaiting--; this.awaiting--;
if (this.awaiting <= 0) { if (this.awaiting <= 0) {
var events = this.waiting; var events = this.waiting;
@ -44,25 +62,39 @@ class EventHandler {
this.onevent(events); this.onevent(events);
} }
} }
}
}
(function(){ (function(){
class Map { class Map {
constructor (os, model) { constructor (os, model) {
this._model = model.id; this._model = model.id;
this.os = os; this.os = os;
this.map = 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 =>{
for (var i in ops) { for (var i in ops) {
var op = ops[i]; var op = ops[i];
if (op.left === null) { if (op.struct === "Insert"){
if (op.opContent != null) { if (op.left === null) {
this.opContents[op.parentSub] = op.opContent; if (op.opContent != null) {
} else { this.opContents[op.parentSub] = op.opContent;
this.contents[op.parentSub] = op.content; } else {
this.contents[op.parentSub] = op.content;
}
this.map[op.parentSub] = op.id;
} }
} else if (op.struct === "Delete") {
var key = op.key;
if (compareIds(this.map[key], op.target)) {
if (this.contents[key] != null) {
delete this.contents[key];
} else {
delete this.opContents[key];
}
}
} else {
throw new Error("Unexpected Operation!");
} }
} }
}); });
@ -82,6 +114,23 @@ class EventHandler {
return def.promise; return def.promise;
} }
} }
delete (key) {
var right = this.map[key];
if (right != null) {
var del = {
target: right,
struct: "Delete"
};
var eventHandler = this.eventHandler;
var modDel = copyObject(del);
modDel.key = key;
eventHandler.awaitAndPrematurelyCall([modDel]);
this.os.requestTransaction(function*(){
yield* this.applyCreatedOperations([del]);
eventHandler.awaitedLastDeletes(1);
});
}
}
set (key, value) { set (key, value) {
// set property. // set property.
// if property is a type, return a promise // if property is a type, return a promise
@ -114,7 +163,7 @@ class EventHandler {
this.os.requestTransaction(function*(){ this.os.requestTransaction(function*(){
yield* this.applyCreatedOperations([insert]); yield* this.applyCreatedOperations([insert]);
eventHandler.awaitedLastOp(1); eventHandler.awaitedLastInserts(1);
}); });
def.resolve(value); def.resolve(value);
} }
@ -126,7 +175,10 @@ class EventHandler {
var model = yield* t.getOperation(this._model); var model = yield* t.getOperation(this._model);
yield* Y.Struct.Map.delete.call(t, model, key); yield* Y.Struct.Map.delete.call(t, model, key);
}*/ }*/
_changed (op) { *_changed (transaction, op) {
if (op.struct === "Delete") {
op.key = (yield* transaction.getOperation(op.target)).parentSub;
}
this.eventHandler.receivedOp(op); this.eventHandler.receivedOp(op);
} }
} }

View File

@ -1,7 +1,7 @@
/* @flow */ /* @flow */
/*eslint-env browser,jasmine */ /*eslint-env browser,jasmine */
var numberOfYMapTests = 5; var numberOfYMapTests = 70;
describe("Map Type", function(){ describe("Map Type", function(){
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
@ -77,6 +77,25 @@ describe("Map Type", function(){
done(); done();
}, 50); }, 50);
}); });
it("Basic get&set&delete of Map property (handle conflict)", function(done){
var y = this.users[0];
y.connector.flushAll();
y.root.set("stuff", "c0");
y.root.delete("stuff");
this.users[1].root.set("stuff", "c1");
y.connector.flushAll();
setTimeout( () => {
for (var key in this.users) {
var u = this.users[key];
expect(u.root.get("stuff")).toBeUndefined();
compareAllUsers(this.users);
}
done();
}, 50);
});
it("Basic get&set of Map property (handle three conflicts)", function(done){ it("Basic get&set of Map property (handle three conflicts)", function(done){
var y = this.users[0]; var y = this.users[0];
this.users[0].root.set("stuff", "c0"); this.users[0].root.set("stuff", "c0");
@ -94,13 +113,36 @@ describe("Map Type", function(){
done(); done();
}, 50); }, 50);
}); });
it("Basic get&set&delete of Map property (handle three conflicts)", function(done){
var y = this.users[0];
this.users[0].root.set("stuff", "c0");
this.users[1].root.set("stuff", "c1");
this.users[2].root.set("stuff", "c2");
this.users[3].root.set("stuff", "c3");
y.connector.flushAll();
this.users[0].root.set("stuff", "deleteme");
this.users[0].root.delete("stuff");
this.users[1].root.set("stuff", "c1");
this.users[2].root.set("stuff", "c2");
this.users[3].root.set("stuff", "c3");
y.connector.flushAll();
setTimeout( () => {
for (var key in this.users) {
var u = this.users[key];
expect(u.root.get("stuff")).toBeUndefined();
}
compareAllUsers(this.users);
done();
}, 50);
});
}); });
describe("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");
} }
]; ];