Implemented some operations. OperationStore executes now ops, not the Engine
This commit is contained in:
parent
ae790b6947
commit
dcec0fe967
@ -8,7 +8,10 @@
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"globals": {
|
||||
"OperationBuffer": true,
|
||||
"OperationStore": true,
|
||||
"AbstractOperationStore": true,
|
||||
"AbstractTransaction": true,
|
||||
"Transaction": true,
|
||||
"IndexedDB": true,
|
||||
"IDBRequest": true
|
||||
}
|
||||
|
@ -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"]);
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
})();
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
class OperationBuffer { //eslint-disable-line no-unused-vars
|
||||
constructor () {
|
||||
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
/* @flow */
|
||||
/*eslint-env browser,jasmine,console */
|
117
src/OperationStore.js
Normal file
117
src/OperationStore.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
src/OperationStore.spec.js
Normal file
40
src/OperationStore.spec.js
Normal 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]);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user