Permalink
Browse files

Add priority and transaction stores

  • Loading branch information...
kriszyp committed Feb 21, 2014
1 parent 44927a0 commit 54457741d06529ad3444f450963e32a4778b8170
Showing with 442 additions and 1 deletion.
  1. +140 −0 store/priority.js
  2. +3 −1 store/tests/all.js
  3. +75 −0 store/tests/priority.js
  4. +95 −0 store/tests/transaction.js
  5. +129 −0 store/transaction.js
View
@@ -0,0 +1,140 @@
+define(['dojo/_base/lang', 'dojo/Deferred', 'dojo/when'], function(lang, Deferred, when){
+// summary:
+// This is a store wrapper that provides prioritized operations. One can include a "priority" property in the options
+// property that is a number representing the priority, for get, put, add, remove, or query calls. The associated
+// action will execute before any other actions in the queue that have a lower priority. The priority queue will also
+// throttle the execution of actions to limit the number of concurrent actions to the priority number. If you submit
+// an action with a priority of 3, it will not be executed until there are at most 2 other concurrent actions.
+
+ var queue = []; // the main queue
+ var running = 0; // how many actions are running
+ function processQueue(){
+ // here we pull actions of the queue and run them
+ // each priority level has it's own sub-queue, we go
+ // from highest to lowest, stopping if we hit the minimum
+ // priority for the currently running operations
+ for(var priority = queue.length - 1; priority >= running; priority--){
+ // get the sub-queue for this priority
+ var queueForPriorityLevel = queue[priority];
+ var action = queueForPriorityLevel && queueForPriorityLevel[queueForPriorityLevel.length - 1];
+ if(action){
+ queueForPriorityLevel.pop();
+ // track the number currently running
+ running++;
+ try{
+ action.executor(function(){
+ running--;
+ // once finished, process the next one in the queue
+ processQueue();
+ });
+ }catch(e){
+ action.def.reject(e);
+ processQueue();
+ }
+ }
+ }
+ }
+ function deferredResults(){
+ // because a query result set is not merely a promise that can be piped through, we have
+ // to recreate the full result set API to defer the execution of query, within each of the
+ // methods we wait for the promise for the query result to finish. We have to wrap this
+ // in an object wrapper so that the returned promise doesn't further defer access to the
+ // query result set
+ var deferred = new Deferred();
+ return {
+ promise: {
+ total: {
+ // allow for lazy access to the total
+ then: function(callback, errback){
+ return deferred.then(function(wrapper){
+ return wrapper.results.total;
+ }).then(callback, errback);
+ }
+ },
+ forEach: function(callback, thisObj){
+ // wait for the action to be executed
+ return deferred.then(function(wrapper){
+ // now run the forEach
+ return wrapper.results.forEach(callback, thisObj);
+ });
+ },
+ map: function(callback, thisObj){
+ return deferred.then(function(wrapper){
+ return wrapper.results.map(callback, thisObj);
+ });
+ },
+ filter: function(callback, thisObj){
+ return deferred.then(function(wrapper){
+ return wrapper.results.filter(callback, thisObj);
+ });
+ },
+ then: function(callback, errback){
+ return deferred.then(function(wrapper){
+ return when(wrapper.results, callback, errback);
+ });
+ }
+ },
+ resolve: deferred.resolve,
+ reject: deferred.reject
+ };
+ }
+ return function(store, config){
+ config = config || {};
+ var priorityStore = lang.delegate(store);
+ // setup the handling for each of these methods
+ ['add', 'put', 'query', 'remove', 'get'].forEach(function(method){
+ var original = store[method];
+ if(original){
+ priorityStore[method] = function(first, options){
+ options = options || {};
+ var def, executor;
+ if(options.immediate){
+ // if immediate is set, skip the queue
+ return original.call(store, first, options);
+ }
+ // just in case a method calls another method with the same args,
+ // default to doing the second call immediately.
+ options.immediate = true;
+ if(method === 'query'){
+ // use the special query result deferral
+ executor = function(callback){
+ // execute and resolve the wrapper
+ var queryResults = original.call(store, first, options);
+ def.resolve({results: queryResults});
+ // wait until the query results are done before performing the next action
+ when(queryResults, callback, callback);
+ };
+ // the special query deferred
+ def = deferredResults();
+ }else{
+ executor = function(callback){
+ // execute the main action (for get, put, add, remove)
+ when(original.call(store, first, options), function(value){
+ def.resolve(value);
+ callback(); // done
+ },
+ function(error){
+ def.reject(error);
+ callback();
+ });
+
+ };
+ // can use a standard deferred
+ def = new Deferred();
+ }
+ // add to the queue
+ var priority = options.priority > -1 ?
+ options.priority :
+ config.priority > -1 ?
+ config.priority : 4;
+ (queue[priority] || (queue[priority] = [])).push(
+ {executor: executor, def: def});
+ // get the next one off the queue
+ processQueue();
+ return def.promise;
+ };
+ }
+ });
+ return priorityStore;
+ };
+});
View
@@ -1,3 +1,5 @@
define([
- './LocalStorage'
+ './LocalStorage',
+ './transaction',
+ './priority'
], function(){});
View
@@ -0,0 +1,75 @@
+define([
+ 'intern!object',
+ 'intern/chai!assert',
+ '../priority',
+ 'dojo/Deferred',
+ 'dojo/promise/all',
+ 'dojo/_base/declare',
+ 'dojo/store/Memory',
+ 'dojo/store/util/QueryResults'
+], function (registerSuite, assert, priority, Deferred, all, declare, Memory, QueryResults) {
+
+ var started = 0;
+ function anAsyncMethod(query){
+ return function(){
+ started++;
+ var results = this.inherited(arguments);
+ var deferred = new Deferred();
+ setTimeout(function(){
+ deferred.resolve(results);
+ }, 10);
+ return query ? new QueryResults(deferred) : deferred;
+ }
+ }
+ var AsyncMemory = declare(Memory, {
+ get: anAsyncMethod(),
+ put: anAsyncMethod(),
+ add: anAsyncMethod(),
+ query: anAsyncMethod(true)
+ });
+ var data = [
+ {id: 1, name: 'one', prime: false, mappedTo: 'E', words: ['banana']},
+ {id: 2, name: 'two', even: true, prime: true, mappedTo: 'D', words: ['banana', 'orange']},
+ {id: 3, name: 'three', prime: true, mappedTo: 'C', words: ['apple', 'orange']},
+ {id: 4, name: 'four', even: true, prime: false, mappedTo: null},
+ {id: 5, name: 'five', prime: true, mappedTo: 'A'}
+ ];
+ priorityStore = priority(new AsyncMemory({
+ data: data
+ }));
+
+ registerSuite({
+ name: "priority",
+ order: function(){
+ var results = [];
+ var operations = [];
+ var order = [];
+ var initialData = data.slice(0);
+ operations.push(priorityStore.query({}, {priority: 10}).forEach(function(object){
+ // clear the data
+ results.push(object);
+ }).then(function(){
+ order.push('query');
+ assert.deepEqual(results, initialData);
+ }));
+ assert.strictEqual(started, 1);
+ operations.push(priorityStore.get(1, {priority: 0}).then(function(object){
+ order.push('get');
+ assert.deepEqual(object, data[0]);
+ }));
+ assert.strictEqual(started, 1);
+ operations.push(priorityStore.put({id: 6, name: 'six'}, {priority: 3}).then(function(object){
+ order.push('put');
+ }));
+ assert.strictEqual(started, 2);
+ operations.push(priorityStore.add({id: 7, name: 'seven'}, {priority: 1}).then(function(object){
+ order.push('add');
+ }));
+ assert.strictEqual(started, 2);
+ return all(operations).then(function(){
+ assert.strictEqual(started, 5);
+ assert.deepEqual(order, ['query', 'put', 'add', 'get']);
+ })
+ }
+ });
+});
View
@@ -0,0 +1,95 @@
+define([
+ 'intern!object',
+ 'intern/chai!assert',
+ '../transaction',
+ 'dojo/Deferred',
+ 'dojo/promise/all',
+ 'dojo/_base/declare',
+ 'dojo/store/Memory',
+ 'dojo/store/util/QueryResults'
+], function (registerSuite, assert, transaction, Deferred, all, declare, Memory, QueryResults) {
+
+ var started = 0;
+ function anAsyncMethod(query){
+ return function(){
+ started++;
+ var results = this.inherited(arguments);
+ var deferred = new Deferred();
+ setTimeout(function(){
+ deferred.resolve(results);
+ }, 10);
+ return query ? new QueryResults(deferred) : deferred;
+ }
+ }
+ var AsyncMemory = declare(Memory, {
+ get: anAsyncMethod(),
+ put: anAsyncMethod(),
+ add: anAsyncMethod(),
+ query: anAsyncMethod(true)
+ });
+ var data = [
+ {id: 1, name: 'one', prime: false, mappedTo: 'E', words: ['banana']},
+ {id: 2, name: 'two', even: true, prime: true, mappedTo: 'D', words: ['banana', 'orange']},
+ {id: 3, name: 'three', prime: true, mappedTo: 'C', words: ['apple', 'orange']},
+ {id: 4, name: 'four', even: true, prime: false, mappedTo: null},
+ {id: 5, name: 'five', prime: true, mappedTo: 'A'}
+ ];
+ var masterStore = new AsyncMemory({
+ data: data
+ });
+ var cachingStore = new AsyncMemory();
+ var logStore = new AsyncMemory();
+
+ var transactionStore = transaction(masterStore, cachingStore, {
+ transactionLogStore: logStore
+ });
+ registerSuite({
+ name: "transaction",
+ transaction: function(){
+ var results = [];
+ var operations = [];
+ var order = [];
+ var initialData = data.slice(0);
+
+ // initially in auto-commit mode
+ operations.push(transactionStore.add(
+ {id: 6, name: 'six'}
+ ));
+ assert.strictEqual(masterStore.data.length, 6);
+ operations.push(transactionStore.remove(6));
+ assert.strictEqual(masterStore.data.length, 5);
+
+ var transaction = transactionStore.transaction();
+ operations.push(transactionStore.add(
+ {id: 6, name: 'six'}
+ ));
+ operations.push(transactionStore.put(
+ {id: 7, name: 'seven'}
+ ));
+ operations.push(transactionStore.remove(3));
+ // make sure the master store hasn't been updated yet
+ assert.strictEqual(masterStore.data.length, 5);
+ // make sure it is in the caching store
+ assert.strictEqual(cachingStore.data.length, 2);
+ operations.push(transactionStore.get(6).then(function(six){
+ assert.deepEqual(six, {id: 6, name: 'six'});
+ }));
+ return all(operations).then(function(){
+ assert.strictEqual(logStore.data.length, 3);
+ return transaction.commit().then(function(){
+ operations = [];
+ operations.push(transactionStore.get(6).then(function(six){
+ assert.deepEqual(six, {id: 6, name: 'six'});
+ }));
+ operations.push(transactionStore.get(7).then(function(seven){
+ assert.deepEqual(seven, {id: 7, name: 'seven'});
+ }));
+ operations.push(transactionStore.get(3).then(function(gone){
+ assert.strictEqual(gone, undefined);
+ }));
+ return all(operations);
+ });
+ });
+ }
+ });
+});
Oops, something went wrong.

0 comments on commit 5445774

Please sign in to comment.