Implemented some operations. OperationStore executes now ops, not the Engine

This commit is contained in:
Kevin Jahns 2015-06-21 02:24:41 +02:00
parent ae790b6947
commit dcec0fe967
12 changed files with 268 additions and 72 deletions

View File

@ -8,7 +8,10 @@
},
"parser": "babel-eslint",
"globals": {
"OperationBuffer": true,
"OperationStore": true,
"AbstractOperationStore": true,
"AbstractTransaction": true,
"Transaction": true,
"IndexedDB": true,
"IDBRequest": true
}

View File

@ -128,7 +128,6 @@ gulp.task("develop", ["build_jasmine_browser", "test", "build"], function(){
.pipe(watch("build/jasmine_browser.js"))
.pipe(jasmineBrowser.specRunner())
.pipe(jasmineBrowser.server({port: options.testport}));
});
gulp.task("default", ["build", "test"]);

View File

@ -44,11 +44,10 @@
"gulp-jasmine-browser": "^0.1.3",
"gulp-sourcemaps": "^1.5.2",
"gulp-uglify": "^1.2.0",
"gulp-util": "^3.0.5",
"gulp-watch": "^4.2.4",
"gulp-webpack": "^1.5.0",
"minimist": "^1.1.1",
"pre-commit": "^1.0.10",
"promise-polyfill": "^2.0.2",
"regenerator": "^0.8.30"
}
}

View File

@ -20,54 +20,49 @@ declare var indexedDB : Object;
declare var setTimeout : Function;
class AbstractTransaction { //eslint-disable-line no-unused-vars
constructor () {
}
*addOperation (op) {
var state = yield* this.getState(op.uid[0]);
if (state == null){
state = {
user: op.uid[0],
clock: 0
};
}
if (op.uid[1] === state.clock){
state.clock++;
yield* this.setState(state);
return true;
} else {
return false;
}
}
}
var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
class Transaction extends AbstractTransaction{
class Transaction extends AbstractTransaction { //eslint-disable-line
transaction: IDBTransaction;
sv: IDBObjectStore;
ob: IDBObjectStore;
constructor (transaction) {
super();
this.transaction = transaction;
this.sv = transaction.objectStore("StateVector");
this.ob = transaction.objectStore("OperationBuffer");
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) {
yield* (function*(){})();
yield this.ob.put(op);
return op;
yield this.os.put(op);
this.buffer[JSON.stringify(op.uid)] = op;
return op;
}
*getOperation (uid) {
return yield this.ob.get(uid);
*getOperation (id) {
var op = this.buffer[JSON.stringify(id)];
if (op == null) {
op = yield this.os.get(id);
this.buffer[JSON.stringify(id)] = op;
}
return op;
}
*removeOperation (id) {
return yield this.os.delete(id);
}
*setState (state : State) : State {
return yield this.sv.put(state);
}
*getState (user : string) : State {
return (yield this.sv.get(user)) || {
user: user,
clock: 0
};
var state;
if ((state = yield this.sv.get(user)) != null){
return state;
} else {
return {
user: user,
clock: 0
};
}
}
*getStateVector () : StateVector {
var stateVector = [];
@ -100,7 +95,7 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
var startPos = startSS[user] || 0;
var endPos = endState.clock;
var range = IDBKeyRange.bound([user, startPos], [user, endPos]);
var cursorResult = this.ob.openCursor(range);
var cursorResult = this.os.openCursor(range);
var cursor;
while ((cursor = yield cursorResult) != null) {
ops.push(cursor.value);
@ -110,16 +105,17 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
return ops;
}
}
class DB {
class OperationStore extends AbstractOperationStore { //eslint-disable-line no-undef
namespace: string;
ready: Promise;
whenReadyListeners: Array<Function>;
constructor (namespace : string) {
super();
this.whenReadyListeners = [];
this.namespace = namespace;
this.ready = false;
var req = indexedDB.open(namespace); //eslint-disable-line no-undef
var req = indexedDB.open(namespace, 2); //eslint-disable-line no-undef
req.onerror = function(){
throw new Error("Couldn't open the IndexedDB database!");
};
@ -133,7 +129,7 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
};
req.onupgradeneeded = function(event){
var db = event.target.result;
db.createObjectStore("OperationBuffer", {keyPath: "uid"});
db.createObjectStore("OperationStore", {keyPath: "id"});
db.createObjectStore("StateVector", {keyPath: "user"});
};
}
@ -146,7 +142,7 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
}
requestTransaction (makeGen : Function) {
this.whenReady(()=>{
var transaction = new Transaction(this.db.transaction(["OperationBuffer", "StateVector"], "readwrite"));
var transaction = new Transaction(this);
var gen = makeGen.apply(transaction);
function handle(res : any){
@ -170,8 +166,9 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
});
}
*removeDatabase () {
return yield indexedDB.deleteDatabase(this.namespace);
this.db.close();
yield indexedDB.deleteDatabase(this.namespace);
}
}
return DB;
return OperationStore;
})();

View File

@ -5,10 +5,10 @@ if(typeof window !== "undefined"){
describe("IndexedDB", function() {
var ob = new IndexedDB("Test");
it("can create transactions", function(done) {
it("can add and get operation", function(done) {
ob.requestTransaction(function*(){
var op = yield* this.setOperation({
"uid": ["1", 0],
"id": ["1", 0],
"stuff": true
});
expect(yield* this.getOperation(["1", 0]))
@ -17,6 +17,21 @@ if(typeof window !== "undefined"){
});
});
it("can remove operation", function(done) {
ob.requestTransaction(function*(){
var op = yield* this.setOperation({
"id": ["1", 0],
"stuff": true
});
expect(yield* this.getOperation(["1", 0]))
.toEqual(op);
yield* this.removeOperation(["1", 0]);
expect(yield* this.getOperation(["1", 0]))
.toBeUndefined();
done();
});
});
it("getOperation(op) returns undefined if op does not exist", function(done){
ob.requestTransaction(function*(){
var op = yield* this.getOperation("plzDon'tBeThere");
@ -73,11 +88,11 @@ if(typeof window !== "undefined"){
var s1 = {user: "1", clock: 55};
yield* this.setState(s1);
var op1 = yield* this.setOperation({
"uid": ["1", 0],
"id": ["1", 0],
"stuff": true
});
var op2 = yield* this.setOperation({
"uid": ["1", 3],
"id": ["1", 3],
"stuff": true
});
var ops = yield* this.getOperations();
@ -87,9 +102,11 @@ if(typeof window !== "undefined"){
done();
});
});
afterAll(function(){
afterAll(function(done){
ob.requestTransaction(function*(){
yield* ob.removeDatabase();
ob = null;
done();
});
});
});

View File

@ -1,7 +0,0 @@
/* @flow */
class OperationBuffer { //eslint-disable-line no-unused-vars
constructor () {
}
}

View File

@ -1,2 +0,0 @@
/* @flow */
/*eslint-env browser,jasmine,console */

117
src/OperationStore.js Normal file
View File

@ -0,0 +1,117 @@
/* @flow */
class AbstractTransaction { //eslint-disable-line no-unused-vars
constructor (store : OperationStore) {
this.store = store;
}
// Throws if operation is not expected.
*addOperation (op) {
var state = this.getState(op.id[0]);
if (op.id[1] === state.clock){
state.clock++;
yield* this.setState(state);
this.store.operationAdded(op);
return true;
} else {
return false;
}
}
}
type Listener = {
f : GeneratorFunction, // is called when all operations are available
missing : number // number of operations that are missing
}
type GeneratorFunction = Function;
type Id = [string, number];
class AbstractOperationStore { //eslint-disable-line no-unused-vars
constructor () {
// E.g. this.listenersById[id] : Array<Listener>
this.listenersById = {};
// Execute the next time a transaction is requested
this.listenersByIdExecuteNow = [];
// A transaction is requested
this.listenersByIdRequestPending = false;
/* To make things more clear, the following naming conventions:
* ls : we put this.listenersById on ls
* l : Array<Listener>
* id : Id (can't use as property name)
* sid : String (converted from id via JSON.stringify
so we can use it as a property name)
Always remember to first overwrite over
a property before you iterate over it!
*/
}
// f is called as soon as every operation requested is available.
// Note that Transaction can (and should) buffer requests.
whenOperationsExist (ids : Array<Id>, f : GeneratorFunction, args : Array<any>) {
if (ids.length > 0) {
let listener : Listener = {
f: f,
args: args || [],
missing: ids.length
};
for (let id of ids) {
let sid = JSON.stringify(id);
let l = this.listenersById[sid];
if (l == null){
l = [];
this.listenersById[sid] = l;
}
l.push(listener);
}
} else {
this.listenersByIdExecuteNow.push({
f: f,
args: args || []
});
}
if (this.listenersByIdRequestPending){
return;
}
this.listenersByIdRequestPending = true;
var store = this;
this.requestTransaction(function*(){
var exe = store.listenersByIdExecuteNow;
store.listenersByIdExecuteNow = [];
for (let listener of exe) {
yield* listener.f.apply(this, listener.args);
}
var ls = store.listenersById;
store.listenersById = {};
for (var sid in ls){
var l = ls[sid];
var id = JSON.parse(sid);
if ((yield* this.getOperation(id)) == null){
store.listenersById[sid] = l;
} else {
for (let listener of l) {
if (--listener.missing === 0){
yield* listener.f.apply(this, listener.args);
}
}
}
}
store.listenersByIdRequestPending = false;
});
}
// called by a transaction when an operation is added
operationAdded (op) {
var l = this.listenersById[op.id];
for (var listener of l){
if (--listener.missing === 0){
this.whenOperationsExist([], listener.f, listener.args);
}
}
}
}

View File

@ -0,0 +1,40 @@
/* @flow */
/*eslint-env browser,jasmine,console */
describe("OperationStore", function() {
class OperationStore extends AbstractOperationStore {
requestTransaction (makeGen) {
var gen = makeGen.apply();
function handle(res : any){
if (res.done){
return;
} else {
handle(gen.next(res.value));
}
}
handle(gen.next());
}
}
var os = new OperationStore();
it("calls when operation added", function(done) {
var id = ["u1", 1];
os.whenOperationsExist([id], function*(){
done();
});
os.operationAdded({id: id});
});
it("calls when no requirements", function(done) {
os.whenOperationsExist([], function*(){
done();
});
});
it("calls when no requirements with arguments", function(done) {
os.whenOperationsExist([], function*(arg){
expect(arg).toBeTrue();
done();
}, [true]);
});
});

View File

@ -1,11 +1,44 @@
/* @flow */
class Operation { //eslint-disable-line no-unused-vars
i : number;
constructor (op) {
this.i = op.i;
// Op is anything that we could get from the OperationStore.
type Op = Object;
var Struct = {
Operation: { //eslint-disable-line no-unused-vars
create: function*(op : Op, user : string) : Struct.Operation {
var state = yield* this.getState(user);
op.id = [user, state.clock];
return yield* this.addOperation(op);
}
},
Insert: {
create: function*( op : Op,
user : string,
left : Struct.Insert,
right : Struct.Insert) : Struct.Insert {
op.left = left ? left.id : null;
op.origin = op.left;
op.right = right ? right.id : null;
op.type = "Insert";
yield* Struct.Operation.create(op, user);
if (left != null) {
left.right = op.id;
yield* this.setOperation(left);
}
if (right != null) {
right.left = op.id;
yield* this.setOperation(right);
}
return op;
},
requiredOps: function(op, ids){
ids.push(op.left);
ids.push(op.right);
return ids;
},
execute: function*(op){
return op;
}
}
yay () {
return this.i;
}
}
};

2
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long