Debouncing Models Observable as they may be triggered too frequently and cause an InfiniteDigest Exception

Change-Id: Idaa49acc9307c93fb46b5378fa7aa1c7b201dfc6
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index d275a9f..17e15ac 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -22,6 +22,7 @@
 import {xosKeyBindingPanel} from './key-binding/key-binding-panel';
 import {xosPagination} from './pagination/pagination';
 import {PaginationFilter} from './pagination/pagination.filter';
+import {XosDebouncer} from './services/helpers/debounce.helper';
 
 export const xosCore = 'xosCore';
 
@@ -40,6 +41,7 @@
   .service('XosSidePanel', XosSidePanel)
   .service('XosKeyboardShortcut', XosKeyboardShortcut)
   .service('XosComponentInjector', XosComponentInjector)
+  .service('XosDebouncer', XosDebouncer)
   .directive('xosLinkWrapper', xosLinkWrapper)
   .component('xosHeader', xosHeader)
   .component('xosFooter', xosFooter)
diff --git a/src/app/core/services/helpers/debounce.helper.spec.ts b/src/app/core/services/helpers/debounce.helper.spec.ts
new file mode 100644
index 0000000..1c47dbd
--- /dev/null
+++ b/src/app/core/services/helpers/debounce.helper.spec.ts
@@ -0,0 +1,37 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+import {XosDebouncer, IXosDebouncer} from './debounce.helper';
+
+let service: IXosDebouncer;
+
+describe('The XosDebouncer service', () => {
+
+  beforeEach(() => {
+    angular
+      .module('test', ['toastr'])
+      .service('XosDebouncer', XosDebouncer);
+    angular.mock.module('test');
+  });
+
+  beforeEach(angular.mock.inject((
+    XosDebouncer: IXosDebouncer,
+  ) => {
+    service = XosDebouncer;
+  }));
+
+  it('should call a function only after it has not been called for 500ms', (done) => {
+    const spy = jasmine.createSpy('fn');
+    const efficientSpy = service.debounce(spy, 500, false);
+    /* tslint:disable */
+    efficientSpy();
+    efficientSpy();
+    /* tslint:enable */
+    expect(spy).not.toHaveBeenCalled();
+    setTimeout(() => {
+      expect(spy).toHaveBeenCalled();
+      done();
+    }, 600);
+  });
+});
+
diff --git a/src/app/core/services/helpers/debounce.helper.ts b/src/app/core/services/helpers/debounce.helper.ts
new file mode 100644
index 0000000..3d4011f
--- /dev/null
+++ b/src/app/core/services/helpers/debounce.helper.ts
@@ -0,0 +1,37 @@
+export interface IXosDebouncer {
+  debounce(func: any, wait: number, immediate?: boolean): any;
+}
+
+export class XosDebouncer implements IXosDebouncer {
+  static $inject = ['$log'];
+
+  constructor (
+    private $log: ng.ILogService
+  ) {
+
+  }
+
+  // wait for 'wait' ms without actions to call the function
+  // if 'immediate' call it immediatly then wait for 'wait'
+  // NOTE that we cannot use $timeout service to debounce functions as it trigger infiniteDigest Exception
+  public debounce(func: any, wait: number, immediate?: boolean) {
+    let timeout;
+    const self = this;
+    return function() {
+      const context = self;
+      const args = arguments;
+      const later = function() {
+        timeout = null;
+        if (!immediate) {
+          func.apply(context, args);
+        }
+      };
+      const callNow = immediate && !timeout;
+      clearTimeout(timeout);
+      timeout = setTimeout(later, wait);
+      if (callNow) {
+        func.apply(context, args);
+      }
+    };
+  }
+}
diff --git a/src/app/datasources/helpers/search.service.ts b/src/app/datasources/helpers/search.service.ts
index 8cee4d1..fd038a2 100644
--- a/src/app/datasources/helpers/search.service.ts
+++ b/src/app/datasources/helpers/search.service.ts
@@ -41,12 +41,11 @@
         return list;
       }, []);
       this.states = _.uniqBy(this.states, 'state');
-      this.$log.debug(`[XosSearchService] Views Loaded: `, this.states);
     });
   }
 
   public search(query: string): IXosSearchResult[] {
-    this.$log.info(`[XosSearchService] Searching for: ${query}`);
+    this.$log.debug(`[XosSearchService] Searching for: ${query}`);
     const routes: IXosSearchResult[] = _.filter(this.states, s => {
       return s.label.toLowerCase().indexOf(query) > -1;
     }).map(r => {
diff --git a/src/app/datasources/stores/model.store.spec.ts b/src/app/datasources/stores/model.store.spec.ts
index 533a53d..b114de4 100644
--- a/src/app/datasources/stores/model.store.spec.ts
+++ b/src/app/datasources/stores/model.store.spec.ts
@@ -8,6 +8,7 @@
 import {ModelRest} from '../rest/model.rest';
 import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
 import {AuthService} from '../rest/auth.rest';
+import {XosDebouncer} from '../../core/services/helpers/debounce.helper';
 
 let service: IXosModelStoreService;
 let httpBackend: ng.IHttpBackendService;
@@ -49,7 +50,8 @@
       .service('XosModelStore', XosModelStore)
       .service('ConfigHelpers', ConfigHelpers) // TODO mock
       .service('AuthService', AuthService)
-      .constant('AppConfig', MockAppCfg);
+      .constant('AppConfig', MockAppCfg)
+      .service('XosDebouncer', XosDebouncer);
 
     angular.mock.module('ModelStore');
   });
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index 79b7231..ab2f3d2 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -4,6 +4,7 @@
 import {IWSEvent, IWSEventService} from '../websocket/global';
 import {IXosResourceService} from '../rest/model.rest';
 import {IStoreHelpersService} from '../helpers/store.helpers';
+import {IXosDebouncer} from '../../core/services/helpers/debounce.helper';
 
 export interface  IXosModelStoreService {
   query(model: string, apiUrl?: string): Observable<any>;
@@ -11,15 +12,18 @@
 }
 
 export class XosModelStore implements IXosModelStoreService {
-  static $inject = ['$log', 'WebSocket', 'StoreHelpers', 'ModelRest'];
+  static $inject = ['$log', 'WebSocket', 'StoreHelpers', 'ModelRest', 'XosDebouncer'];
   private _collections: any; // NOTE contains a map of {model: BehaviourSubject}
+  private efficientNext: any; // NOTE debounce next
   constructor(
     private $log: ng.ILogService,
     private webSocket: IWSEventService,
     private storeHelpers: IStoreHelpersService,
     private ModelRest: IXosResourceService,
+    private XosDebouncer: IXosDebouncer
   ) {
     this._collections = {};
+    this.efficientNext = this.XosDebouncer.debounce(this.next, 500, false);
   }
 
   public query(modelName: string, apiUrl: string): Observable<any> {
@@ -31,7 +35,7 @@
     }
     // else manually trigger the next with the last know value to trigger the subscribe method of who's requestiong this data
     else {
-      this._collections[modelName].next(this._collections[modelName].value);
+      this.efficientNext(this._collections[modelName]);
     }
 
     this.webSocket.list()
@@ -80,6 +84,10 @@
     // TODO implement a get method
   }
 
+  private next(subject: BehaviorSubject<any>): void {
+    subject.next(subject.value);
+  }
+
   private loadInitialData(model: string, apiUrl?: string) {
     // TODO provide always the apiUrl togheter with the query() params
     if (!angular.isDefined(apiUrl)) {