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",
|
"parser": "babel-eslint",
|
||||||
"globals": {
|
"globals": {
|
||||||
"OperationBuffer": true,
|
"OperationStore": true,
|
||||||
|
"AbstractOperationStore": true,
|
||||||
|
"AbstractTransaction": true,
|
||||||
|
"Transaction": true,
|
||||||
"IndexedDB": true,
|
"IndexedDB": true,
|
||||||
"IDBRequest": true
|
"IDBRequest": true
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,6 @@ gulp.task("develop", ["build_jasmine_browser", "test", "build"], function(){
|
|||||||
.pipe(watch("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"]);
|
||||||
|
@ -44,11 +44,10 @@
|
|||||||
"gulp-jasmine-browser": "^0.1.3",
|
"gulp-jasmine-browser": "^0.1.3",
|
||||||
"gulp-sourcemaps": "^1.5.2",
|
"gulp-sourcemaps": "^1.5.2",
|
||||||
"gulp-uglify": "^1.2.0",
|
"gulp-uglify": "^1.2.0",
|
||||||
|
"gulp-util": "^3.0.5",
|
||||||
"gulp-watch": "^4.2.4",
|
"gulp-watch": "^4.2.4",
|
||||||
"gulp-webpack": "^1.5.0",
|
|
||||||
"minimist": "^1.1.1",
|
"minimist": "^1.1.1",
|
||||||
"pre-commit": "^1.0.10",
|
"pre-commit": "^1.0.10",
|
||||||
"promise-polyfill": "^2.0.2",
|
|
||||||
"regenerator": "^0.8.30"
|
"regenerator": "^0.8.30"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,55 +20,50 @@ declare var indexedDB : Object;
|
|||||||
|
|
||||||
declare var setTimeout : Function;
|
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
|
var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
|
||||||
class Transaction extends AbstractTransaction{
|
class Transaction extends AbstractTransaction { //eslint-disable-line
|
||||||
transaction: IDBTransaction;
|
transaction: IDBTransaction;
|
||||||
sv: IDBObjectStore;
|
sv: IDBObjectStore;
|
||||||
ob: IDBObjectStore;
|
os: IDBObjectStore;
|
||||||
constructor (transaction) {
|
store: OperationStore;
|
||||||
super();
|
|
||||||
this.transaction = transaction;
|
constructor (store : OperationStore) {
|
||||||
this.sv = transaction.objectStore("StateVector");
|
super(store);
|
||||||
this.ob = transaction.objectStore("OperationBuffer");
|
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* (function*(){})();
|
yield this.os.put(op);
|
||||||
yield this.ob.put(op);
|
this.buffer[JSON.stringify(op.uid)] = op;
|
||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
*getOperation (uid) {
|
*getOperation (id) {
|
||||||
return yield this.ob.get(uid);
|
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 {
|
*setState (state : State) : State {
|
||||||
return yield this.sv.put(state);
|
return yield this.sv.put(state);
|
||||||
}
|
}
|
||||||
*getState (user : string) : State {
|
*getState (user : string) : State {
|
||||||
return (yield this.sv.get(user)) || {
|
var state;
|
||||||
|
if ((state = yield this.sv.get(user)) != null){
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
user: user,
|
user: user,
|
||||||
clock: 0
|
clock: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
*getStateVector () : StateVector {
|
*getStateVector () : StateVector {
|
||||||
var stateVector = [];
|
var stateVector = [];
|
||||||
var cursorResult = this.sv.openCursor();
|
var cursorResult = this.sv.openCursor();
|
||||||
@ -100,7 +95,7 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
var startPos = startSS[user] || 0;
|
var startPos = startSS[user] || 0;
|
||||||
var endPos = endState.clock;
|
var endPos = endState.clock;
|
||||||
var range = IDBKeyRange.bound([user, startPos], [user, endPos]);
|
var range = IDBKeyRange.bound([user, startPos], [user, endPos]);
|
||||||
var cursorResult = this.ob.openCursor(range);
|
var cursorResult = this.os.openCursor(range);
|
||||||
var cursor;
|
var cursor;
|
||||||
while ((cursor = yield cursorResult) != null) {
|
while ((cursor = yield cursorResult) != null) {
|
||||||
ops.push(cursor.value);
|
ops.push(cursor.value);
|
||||||
@ -110,16 +105,17 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
return ops;
|
return ops;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DB {
|
class OperationStore extends AbstractOperationStore { //eslint-disable-line no-undef
|
||||||
namespace: string;
|
namespace: string;
|
||||||
ready: Promise;
|
ready: Promise;
|
||||||
whenReadyListeners: Array<Function>;
|
whenReadyListeners: Array<Function>;
|
||||||
constructor (namespace : string) {
|
constructor (namespace : string) {
|
||||||
|
super();
|
||||||
this.whenReadyListeners = [];
|
this.whenReadyListeners = [];
|
||||||
this.namespace = namespace;
|
this.namespace = namespace;
|
||||||
this.ready = false;
|
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(){
|
req.onerror = function(){
|
||||||
throw new Error("Couldn't open the IndexedDB database!");
|
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){
|
req.onupgradeneeded = function(event){
|
||||||
var db = event.target.result;
|
var db = event.target.result;
|
||||||
db.createObjectStore("OperationBuffer", {keyPath: "uid"});
|
db.createObjectStore("OperationStore", {keyPath: "id"});
|
||||||
db.createObjectStore("StateVector", {keyPath: "user"});
|
db.createObjectStore("StateVector", {keyPath: "user"});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -146,7 +142,7 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
}
|
}
|
||||||
requestTransaction (makeGen : Function) {
|
requestTransaction (makeGen : Function) {
|
||||||
this.whenReady(()=>{
|
this.whenReady(()=>{
|
||||||
var transaction = new Transaction(this.db.transaction(["OperationBuffer", "StateVector"], "readwrite"));
|
var transaction = new Transaction(this);
|
||||||
var gen = makeGen.apply(transaction);
|
var gen = makeGen.apply(transaction);
|
||||||
|
|
||||||
function handle(res : any){
|
function handle(res : any){
|
||||||
@ -170,8 +166,9 @@ var IndexedDB = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
*removeDatabase () {
|
*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() {
|
describe("IndexedDB", function() {
|
||||||
var ob = new IndexedDB("Test");
|
var ob = new IndexedDB("Test");
|
||||||
|
|
||||||
it("can create transactions", 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({
|
||||||
"uid": ["1", 0],
|
"id": ["1", 0],
|
||||||
"stuff": true
|
"stuff": true
|
||||||
});
|
});
|
||||||
expect(yield* this.getOperation(["1", 0]))
|
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){
|
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");
|
||||||
@ -73,11 +88,11 @@ if(typeof window !== "undefined"){
|
|||||||
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({
|
||||||
"uid": ["1", 0],
|
"id": ["1", 0],
|
||||||
"stuff": true
|
"stuff": true
|
||||||
});
|
});
|
||||||
var op2 = yield* this.setOperation({
|
var op2 = yield* this.setOperation({
|
||||||
"uid": ["1", 3],
|
"id": ["1", 3],
|
||||||
"stuff": true
|
"stuff": true
|
||||||
});
|
});
|
||||||
var ops = yield* this.getOperations();
|
var ops = yield* this.getOperations();
|
||||||
@ -87,9 +102,11 @@ if(typeof window !== "undefined"){
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
afterAll(function(){
|
afterAll(function(done){
|
||||||
ob.requestTransaction(function*(){
|
ob.requestTransaction(function*(){
|
||||||
yield* ob.removeDatabase();
|
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 */
|
/* @flow */
|
||||||
|
|
||||||
class Operation { //eslint-disable-line no-unused-vars
|
// Op is anything that we could get from the OperationStore.
|
||||||
i : number;
|
type Op = Object;
|
||||||
constructor (op) {
|
|
||||||
this.i = op.i;
|
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);
|
||||||
}
|
}
|
||||||
yay () {
|
},
|
||||||
return this.i;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user