created Array type that has a good time complexity for both insert and retrieval of objects

This commit is contained in:
Kevin Jahns 2015-07-12 03:45:12 +02:00
parent 8cc374cabb
commit d50d34dc12
8 changed files with 290 additions and 185 deletions

View File

@ -28,6 +28,7 @@
"Operation": true, "Operation": true,
"getRandom": true, "getRandom": true,
"RBTree": true, "RBTree": true,
"compareIds": true "compareIds": true,
"EventHandler": true
} }
} }

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.id); t = yield* Y[op.type].create(this.store, op);
this.store.initializedTypes[sid] = t; this.store.initializedTypes[sid] = t;
} }
} }
@ -36,6 +36,18 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars
throw new Error("Operations must arrive in order!"); throw new Error("Operations must arrive in order!");
} }
} }
*applyCreatedOperations (ops) {
var send = [];
for (var i = 0; i < ops.length; i++) {
var op = ops[i];
yield* Struct[op.struct].execute.call(this, op);
send.push(Struct[op.struct].encode(op));
}
this.store.y.connector.broadcast({
type: "update",
ops: send
});
}
} }
type Listener = { type Listener = {
@ -70,6 +82,13 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
} }
setUserId (userId) { setUserId (userId) {
this.userId = userId; this.userId = userId;
this.opClock = 0;
}
getNextOpId () {
if (this.userId == null) {
throw new Error("OperationStore not yet initialized!");
}
return [this.userId, this.opClock++];
} }
apply (ops) { apply (ops) {
for (var key in ops) { for (var key in ops) {

View File

@ -14,13 +14,12 @@ function copyObject (o) {
} }
type StateVector = Array<State>; type StateVector = Array<State>;
type OperationSet = Object; // os[Id] = op
type StateSet = Object; type StateSet = Object;
Y.Memory = (function(){ //eslint-disable-line no-unused-vars Y.Memory = (function(){ //eslint-disable-line no-unused-vars
class Transaction extends AbstractTransaction { //eslint-disable-line class Transaction extends AbstractTransaction { //eslint-disable-line
ss: StateSet; ss: StateSet;
os: OperationSet; os: RBTree;
store: OperationStore; store: OperationStore;
constructor (store : OperationStore) { constructor (store : OperationStore) {

View File

@ -35,28 +35,12 @@ function compareIds(id1, id2) {
} }
var Struct = { var Struct = {
Operation: { //eslint-disable-line no-unused-vars /*
create: function*(op : Op) : Struct.Operation { {
var user = this.store.y.connector.userId; target: Id
var state = yield* this.getState(user);
op.id = [user, state.clock];
yield* Struct[op.struct].execute.call(this, op);
this.store.y.connector.broadcast({
type: "update",
ops: [Struct[op.struct].encode(op)]
});
return op;
} }
}, */
Delete: { Delete: {
create: function* (op) {
if (op.target == null) {
throw new Error("You must define a delete target!");
}
op.struct = "Delete";
return yield* Struct.Operation.create.call(this, op);
},
encode: function (op) { encode: function (op) {
return op; return op;
}, },
@ -77,20 +61,12 @@ var Struct = {
content: any, content: any,
left: Id, left: Id,
right: Id, right: Id,
origin: id,
parent: Id, parent: Id,
parentSub: string (optional) parentSub: string (optional),
id: this.os.getNextOpId()
} }
*/ */
create: function* ( op: Op ) : Insert {
if ( op.left === undefined
|| op.right === undefined
|| op.parent === undefined ) {
throw new Error("You must define left, right, and parent!");
}
op.origin = op.left;
op.struct = "Insert";
return yield* Struct.Operation.create.call(this, op);
},
encode: function(op){ encode: function(op){
/*var e = { /*var e = {
id: op.id, id: op.id,
@ -248,12 +224,15 @@ var Struct = {
} }
}, },
List: { List: {
create: function* ( op : Op){ /*
op.start = null; {
op.end = null; start: null,
op.struct = "List"; end: null,
return yield* Struct.Operation.create.call(this, op); struct: "List",
}, type: "",
id: this.os.getNextOpId()
}
*/
encode: function(op){ encode: function(op){
return { return {
struct: "List", struct: "List",
@ -315,7 +294,7 @@ var Struct = {
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.content)); res.push(f(operation));
} }
o = operation.right; o = operation.right;
} }
@ -350,19 +329,18 @@ var Struct = {
Map: { Map: {
/* /*
{ {
// empty map: {},
struct: "Map",
type: "",
id: this.os.getNextOpId()
} }
*/ */
create: function* ( op : Op ){
op.map = {};
op.struct = "Map";
return yield* Struct.Operation.create.call(this, op);
},
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!!
}; };
}, },
requiredOps: function(){ requiredOps: function(){

109
src/Types/Array.js Normal file
View File

@ -0,0 +1,109 @@
(function(){
class YArray {
constructor (os, _model, idArray, valArray) {
this.os = os;
this._model = _model;
// Array of all the operation id's
this.idArray = idArray;
this.valArray = valArray;
this.eventHandler = new EventHandler( ops =>{
for (var i in ops) {
var op = ops[i];
var pos;
if (op.right === null) {
pos = this.idArray.length;
} else {
var sid = JSON.stringify(op.right);
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);
}
});
}
get (pos) {
if (pos == null || typeof pos !== "number") {
throw new Error("pos must be a number!");
}
return this.valArray[pos];
}
toArray() {
return this.valArray.slice();
}
insert (pos, contents) {
if (typeof pos !== "number") {
throw new Error("pos must be a number!");
}
if (!(contents instanceof Array)) {
throw new Error("contents must be an Array of objects!");
}
if (contents.length === 0) {
return;
}
if (pos > this.idArray.length || pos < 0) {
throw new Error("This position exceeds the range of the array!");
}
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 prevId = mostLeft;
for (var i = 0; i < contents.length; i++) {
var op = {
left: prevId,
origin: prevId,
right: mostRight,
parent: this._model,
content: contents[i],
struct: "Insert",
id: this.os.getNextOpId()
};
ops.push(op);
prevId = op.id;
}
var eventHandler = this.eventHandler;
eventHandler.awaitAndPrematurelyCall(ops);
this.os.requestTransaction(function*(){
yield* this.applyCreatedOperations(ops);
eventHandler.awaitedLastOp(ops.length);
});
}
*delete (pos) {
if (typeof pos !== "number") {
throw new Error("pos must be a number!");
}
var t = yield "transaction";
var model = yield* t.getOperation(this._model);
yield* Y.Struct.Array.delete.call(t, model, pos);
}
_changed (op) {
this.eventHandler.receivedOp(op);
}
}
Y.Array = function* _YArray(){
var model = {
start: null,
end: null,
struct: "List",
type: "Array",
id: this.store.getNextOpId()
};
yield* this.applyCreatedOperations([model]);
return yield* this.createType(model);
};
Y.Array.create = function* YArrayCreate(os, model){
var valArray = [];
var idArray = yield* Y.Struct.List.map.call(this, model, function(c){
valArray.push(c.content);
return JSON.stringify(c.id);
});
return new YArray(os, model.id, idArray, valArray);
};
})();

View File

@ -1,48 +0,0 @@
(function(){
class List {
constructor (_model) {
this._model = _model;
}
*val (pos) {
var t = yield "transaction";
var model = yield* t.getOperation(this._model);
if (pos != null) {
var o = yield* Y.Struct.List.ref.call(t, model, pos);
return o ? o.content : null;
} else {
return yield* Y.Struct.List.map.call(t, model, function(c){return c; });
}
}
*insert (pos, contents) {
if (typeof pos !== "number") {
throw new Error("pos must be a number!");
}
if (!(contents instanceof Array)) {
throw new Error("contents must be an Array of objects!");
}
var t = yield "transaction";
var model = yield* t.getOperation(this._model);
yield* Y.Struct.List.insert.call(t, model, pos, contents);
}
*delete (pos) {
if (typeof pos !== "number") {
throw new Error("pos must be a number!");
}
var t = yield "transaction";
var model = yield* t.getOperation(this._model);
yield* Y.Struct.List.delete.call(t, model, pos);
}
_changed () {
}
}
Y.List = function* YList(){
var t = yield "transaction";
var model = yield* Y.Struct.List.create.call(t, {type: "List"});
return t.createType(model);
};
Y.List.Create = List;
})();

View File

@ -14,12 +14,14 @@ class EventHandler {
this.waiting.push(copyObject(op)); this.waiting.push(copyObject(op));
} }
} }
awaitAndPrematurelyCall (op) { awaitAndPrematurelyCall (ops) {
this.awaiting++; this.awaiting++;
this.onevent([op]); this.onevent(ops);
} }
awaitedLastOp () { awaitedLastOp (n) {
var op = this.waiting.pop(); var ops = this.waiting.splice(this.waiting.length - n);
for (var oid = 0; oid < ops.length; oid++) {
var op = ops[oid];
for (var i = this.waiting.length - 1; i >= 0; i--) { for (var i = this.waiting.length - 1; i >= 0; i--) {
var w = this.waiting[i]; var w = this.waiting[i];
if (compareIds(op.left, w.id)) { if (compareIds(op.left, w.id)) {
@ -33,6 +35,8 @@ class EventHandler {
op.right = w.right; op.right = w.right;
} }
} }
}
this.awaiting--; this.awaiting--;
if (this.awaiting <= 0) { if (this.awaiting <= 0) {
var events = this.waiting; var events = this.waiting;
@ -87,8 +91,10 @@ class EventHandler {
var insert = { var insert = {
left: null, left: null,
right: right, right: right,
origin: null,
parent: this._model, parent: this._model,
parentSub: key parentSub: key,
struct: "Insert"
}; };
var def = Promise.defer(); var def = Promise.defer();
if ( value != null && value.constructor === GeneratorFunction) { if ( value != null && value.constructor === GeneratorFunction) {
@ -96,17 +102,19 @@ class EventHandler {
this.os.requestTransaction(function*(){ this.os.requestTransaction(function*(){
var type = yield* value.call(this); var type = yield* value.call(this);
insert.opContent = type._model; insert.opContent = type._model;
yield* Struct.Insert.create.call(this, insert); insert.id = this.store.getNextOpId();
yield* this.applyCreatedOperations([insert]);
def.resolve(type); def.resolve(type);
}); });
} else { } else {
insert.content = value; insert.content = value;
insert.id = this.os.getNextOpId();
var eventHandler = this.eventHandler; var eventHandler = this.eventHandler;
eventHandler.awaitAndPrematurelyCall(insert); eventHandler.awaitAndPrematurelyCall([insert]);
this.os.requestTransaction(function*(){ this.os.requestTransaction(function*(){
yield* Struct.Insert.create.call(this, insert); yield* this.applyCreatedOperations([insert]);
eventHandler.awaitedLastOp(); eventHandler.awaitedLastOp(1);
}); });
def.resolve(value); def.resolve(value);
} }
@ -124,7 +132,13 @@ class EventHandler {
} }
Y.Map = function* YMap(){ Y.Map = function* YMap(){
var model = yield* Y.Struct.Map.create.call(this, {type: "Map"}); var model = {
map: {},
struct: "Map",
type: "Map",
id: this.store.getNextOpId()
};
yield* this.applyCreatedOperations([model]);
return yield* this.createType(model); return yield* this.createType(model);
}; };
Y.Map.create = function* YMapCreate(os, model){ Y.Map.create = function* YMapCreate(os, model){

View File

@ -21,9 +21,9 @@ function getRandomNumber(n) {
return Math.floor(Math.random() * n); return Math.floor(Math.random() * n);
} }
var numberOfYMapTests = 30; var numberOfYMapTests = 5;
function applyRandomTransactions (users, transactions, numberOfTransactions) { function applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
function randomTransaction (root) { function randomTransaction (root) {
var f = getRandom(transactions); var f = getRandom(transactions);
f(root); f(root);
@ -34,7 +34,7 @@ function applyRandomTransactions (users, transactions, numberOfTransactions) {
// 10% chance to flush // 10% chance to flush
users[0].connector.flushOne(); users[0].connector.flushOne();
} else { } else {
randomTransaction(getRandom(users).root); randomTransaction(getRandom(objects));
} }
} }
} }
@ -70,7 +70,7 @@ function compareAllUsers(users){
} }
describe("Yjs", function(){ describe("Yjs", function(){
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500; jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
beforeEach(function(done){ beforeEach(function(done){
if (this.users != null) { if (this.users != null) {
for (var y of this.users) { for (var y of this.users) {
@ -121,6 +121,16 @@ describe("Yjs", function(){
done(); done();
}); });
}); });
it("Map can set custom types (Array)", function(done){
var y = this.users[0].root;
y.set("Array", Y.Array).then(function(array) {
array.insert(0, [1, 2, 3]);
return y.get("Array");
}).then(function(array){
expect(array.toArray()).toEqual([1, 2, 3]);
done();
});
});
it("Basic get&set of Map property (converge via update)", function(done){ it("Basic get&set of Map property (converge via update)", function(done){
var u = this.users[0]; var u = this.users[0];
u.connector.flushAll(); u.connector.flushAll();
@ -172,6 +182,28 @@ describe("Yjs", function(){
done(); done();
}, 50); }, 50);
}); });
it("Basic insert 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;
y.connector.flushAll();
l1.insert(0, [0]);
return this.users[1].root.get("Array");
}).then((array)=>{
l2 = array;
l2.insert(0, [1]);
return this.users[2].root.get("Array");
}).then((array)=>{
l3 = array;
l3.insert(0, [2]);
y.connector.flushAll();
expect(l1.toArray()).toEqual(l2.toArray());
expect(l2.toArray()).toEqual(l3.toArray());
compareAllUsers(this.users);
done();
});
});
}); });
describe("Map random tests", function(){ describe("Map random tests", function(){
var randomMapTransactions = [ var randomMapTransactions = [
@ -182,10 +214,10 @@ describe("Yjs", function(){
map.delete("somekey"); map.delete("somekey");
} }
]; ];
function compareMapValues(users){ function compareMapValues(maps){
var firstMap; var firstMap;
for (var u of users) { for (var map of maps) {
var val = u.root.get(); var val = map.get();
if (firstMap == null) { if (firstMap == null) {
firstMap = val; firstMap = val;
} else { } else {
@ -193,79 +225,80 @@ describe("Yjs", function(){
} }
} }
} }
it(`succeed after ${numberOfYMapTests} actions with flush before transactions`, function(done){ beforeEach(function(done){
this.users[0].root.set("Map", Y.Map);
this.users[0].connector.flushAll(); this.users[0].connector.flushAll();
applyRandomTransactions(this.users, randomMapTransactions, numberOfYMapTests);
setTimeout(()=>{
compareAllUsers(this.users);
compareMapValues(this.users);
done();
}, 500);
});
it(`succeed after ${numberOfYMapTests} actions without flush before transactions`, function(done){
applyRandomTransactions(this.users, randomMapTransactions, numberOfYMapTests);
setTimeout(()=>{
compareAllUsers(this.users);
compareMapValues(this.users);
done();
}, 500);
});
});
/* var then = Promise.resolve();
var maps = [];
var numberOfYListTests = 100; for (var u of this.users) {
describe("List random tests", function(){ then = then.then(function(){ //eslint-disable-line
var randomListTests = [function* insert (root) { return u.root.get("Map");
var list = yield* root.get("list"); }).then(function(map){//eslint-disable-line
yield* list.insert(Math.floor(Math.random() * 10), [getRandomNumber()]); maps.push(map);
}, function* delete_(root) {
var list = yield* root.get("list");
yield* list.delete(Math.floor(Math.random() * 10));
}];
beforeEach(function(){
this.users[0].transact(function*(root){
var list = yield* Y.List();
yield* root.set("list", list);
}); });
this.users[0].connector.flushAll();
});
it(`succeeds after ${numberOfYListTests} actions`, function(){
applyRandomTransactions(this.users, randomListTests, numberOfYListTests);
compareAllUsers(this.users);
var userList;
this.users[0].transact(function*(root){
var list = yield* root.get("list");
if (userList == null) {
userList = yield* list.get();
} else {
expect(userList).toEqual(yield* list.get());
expect(userList.length > 0).toBeTruthy();
} }
this.maps = maps;
then.then(function(){
done();
}); });
}); });
it(`succeed after ${numberOfYMapTests} actions`, function(done){
applyRandomTransactions(this.users, this.maps, randomMapTransactions, numberOfYMapTests);
setTimeout(()=>{
compareAllUsers(this.users);
compareMapValues(this.maps);
done();
}, 500);
});
}); });
describe("Map debug tests", function(){ var numberOfYArrayTests = 10;
beforeEach(function(){ describe("Array random tests", function(){
this.u1 = this.users[0]; var randomMapTransactions = [
this.u2 = this.users[1]; function insert (array) {
this.u3 = this.users[2]; array.insert(getRandomNumber(array.toArray().length), [getRandomNumber()]);
}
];
function compareArrayValues(arrays){
var firstArray;
for (var l of arrays) {
var val = l.toArray();
if (firstArray == null) {
firstArray = val;
} else {
expect(val).toEqual(firstArray);
}
}
}
beforeEach(function(done){
this.users[0].root.set("Array", Y.Array);
this.users[0].connector.flushAll();
var then = Promise.resolve();
var arrays = [];
for (var u of this.users) {
then = then.then(function(){ //eslint-disable-line
return u.root.get("Array");
}).then(function(array){//eslint-disable-line
arrays.push(array);
}); });
it("concurrent insertions #1", function(){ }
this.u1.transact(function*(root){ this.arrays = arrays;
var op = { then.then(function(){
content: 1, done();
left: null,
right: null,
parent: root._model,
parentSub: "a"
};
Struct.Insert.create.call(this, op);
}); });
});
it("arrays.length equals users.length", function(){
expect(this.arrays.length).toEqual(this.users.length);
});
it(`succeed after ${numberOfYArrayTests} actions`, function(done){
applyRandomTransactions(this.users, this.arrays, randomMapTransactions, numberOfYArrayTests);
setTimeout(()=>{
compareAllUsers(this.users); compareAllUsers(this.users);
compareArrayValues(this.arrays);
done();
}, 500);
}); });
}); });
*/
}); });