diff --git a/doc/bloodhound.md b/doc/bloodhound.md index dd2cd499..dc7a8144 100644 --- a/doc/bloodhound.md +++ b/doc/bloodhound.md @@ -48,7 +48,7 @@ var engine = new Bloodhound({ }); ``` -#### Bloodhound#initialize() +#### Bloodhound#initialize(reinitialize) Kicks off the initialization of the suggestion engine. This includes processing the data provided through `local` and fetching/processing the data provided @@ -63,6 +63,59 @@ promise .fail(function() { console.log('err!'); }); ``` +After the initial call of `initialize`, how subsequent invocations of the method +behave depends on the `reinitialize` argument. If `reinitialize` is falsy, the +method will not execute the initialization logic and will just return the same +jQuery promise returned by the initial invocation. If `reinitialize` is truthy, +the method will behave as if it were being called for the first time. + +```javascript +var promise1 = engine.initialize(); +var promise2 = engine.initialize(); +var promise3 = engine.initialize(true); + +promise1 === promise2; +promise3 !== promise1 && promise3 !== promise2; +``` + +#### Bloodhound#add(datums) + +Takes one argument, `datums`, which is expected to be an array of +[datums](#datums). The passed in datums will get added to the search index that +powers the suggestion engine. + +```javascript +engine.add([{ val: 'one' }, { val: 'two' }]); +``` + +#### Bloodhound#clear() + +Removes all suggestions from the search index. + +```javascript +engine.clear(); +``` + +#### Bloodhound#clearPrefetchCache() + +If you're using `prefetch`, data gets cached in local storage in an effort to +cut down on unnecessary network requests. `clearPrefetchCache` offers a way to +programmatically clear said cache. + +```javascript +engine.clearPrefetchCache(); +``` + +#### Bloodhound#clearRemoteCache() + +If you're using `remote`, Bloodhound will cache the 10 most recent responses +in an effort to provide a better user experience. `clearRemoteCache` offers a +way to programmatically clear said cache. + +```javascript +engine.clearRemoteCache(); +``` + [jQuery promise]: http://api.jquery.com/Types/#Promise diff --git a/src/bloodhound/bloodhound.js b/src/bloodhound/bloodhound.js index d1236785..10b9f775 100644 --- a/src/bloodhound/bloodhound.js +++ b/src/bloodhound/bloodhound.js @@ -83,10 +83,9 @@ return deferred; function handlePrefetchResponse(resp) { - var filtered; - - filtered = o.filter ? o.filter(resp) : resp; - that.add(filtered); + // clear to mirror the behavior of bootstrapping + that.clear(); + that.add(o.filter ? o.filter(resp) : resp); that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl); } @@ -105,13 +104,7 @@ return this.transport.get(url, this.remote.ajax, handleRemoteResponse); function handleRemoteResponse(err, resp) { - var filtered; - - // failed request is equivalent to an empty suggestion set - if (err) { return cb([]); } - - filtered = that.remote.filter ? that.remote.filter(resp) : resp; - cb(filtered); + err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp); } }, @@ -140,28 +133,29 @@ return stored.data && !isExpired ? stored.data : null; }, - // ### public - - // the contents of this function are broken out of the constructor - // to help improve the testability of bloodhounds - initialize: function initialize() { - var that = this, deferred; + _initialize: function initialize() { + var that = this, local = this.local, deferred; deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve(); - // local can be a function that returns an array of datums - this.local = _.isFunction(this.local) ? this.local() : this.local; - // make sure local is added to the index after prefetch - this.local && deferred.done(addLocalToIndex); + local && deferred.done(addLocalToIndex); this.transport = this.remote ? new Transport(this.remote) : null; - this.initialize = function initialize() { return deferred.promise(); }; - return deferred.promise(); + return (this.initPromise = deferred.promise()); - function addLocalToIndex() { that.add(that.local); } + function addLocalToIndex() { + // local can be a function that returns an array of datums + that.add(_.isFunction(local) ? local() : local); + } + }, + + // ### public + + initialize: function initialize(force) { + return !this.initPromise || force ? this._initialize() : this.initPromise; }, add: function add(data) { @@ -208,6 +202,18 @@ } }, + clear: function clear() { + this.index.reset(); + }, + + clearPrefetchCache: function clearPrefetchCache() { + this.storage && this.storage.clear(); + }, + + clearRemoteCache: function clearRemoteCache() { + this.transport && Transport.resetCache(); + }, + ttAdapter: function ttAdapter() { return _.bind(this.get, this); } }); diff --git a/src/bloodhound/search_index.js b/src/bloodhound/search_index.js index 86e33c15..3f342c04 100644 --- a/src/bloodhound/search_index.js +++ b/src/bloodhound/search_index.js @@ -19,8 +19,7 @@ var SearchIndex = (function() { this.datumTokenizer = o.datumTokenizer; this.queryTokenizer = o.queryTokenizer; - this.datums = []; - this.trie = newNode(); + this.reset(); } // instance methods @@ -96,6 +95,11 @@ var SearchIndex = (function() { _.map(unique(matches), function(id) { return that.datums[id]; }) : []; }, + reset: function reset() { + this.datums = []; + this.trie = newNode(); + }, + serialize: function serialize() { return { datums: this.datums, trie: this.trie }; } diff --git a/test/bloodhound_spec.js b/test/bloodhound_spec.js index 88753a36..f4cbc1eb 100644 --- a/test/bloodhound_spec.js +++ b/test/bloodhound_spec.js @@ -10,6 +10,108 @@ describe('Bloodhound', function() { clearAjaxRequests(); }); + describe('#initialize', function() { + beforeEach(function() { + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + local: fixtures.data.simple + }); + + spyOn(this.bloodhound, '_initialize').andCallThrough(); + }); + + it('should not support reinitialization by default', function() { + var p1, p2; + + p1 = this.bloodhound.initialize(); + p2 = this.bloodhound.initialize(); + + expect(p1).toBe(p2); + expect(this.bloodhound._initialize.callCount).toBe(1); + }); + + it('should reinitialize if reintialize flag is true', function() { + var p1, p2; + + p1 = this.bloodhound.initialize(); + p2 = this.bloodhound.initialize(true); + + expect(p1).not.toBe(p2); + expect(this.bloodhound._initialize.callCount).toBe(2); + }); + }); + + describe('#add', function() { + it('should add datums to search index', function() { + var spy = jasmine.createSpy(); + + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + local: [] + }); + this.bloodhound.initialize(); + this.bloodhound.add(fixtures.data.simple); + + this.bloodhound.get('big', spy); + + expect(spy).toHaveBeenCalledWith([ + { value: 'big' }, + { value: 'bigger' }, + { value: 'biggest' } + ]); + }); + }); + + describe('#clear', function() { + it('should remove all datums to search index', function() { + var spy = jasmine.createSpy(); + + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + local: fixtures.data.simple + }); + this.bloodhound.initialize(); + this.bloodhound.clear(); + + this.bloodhound.get('big', spy); + + expect(spy).toHaveBeenCalledWith([]); + }); + }); + + describe('#clearPrefetchCache', function() { + it('should clear persistent storage', function() { + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + prefetch: '/test' + }); + this.bloodhound.initialize(); + this.bloodhound.clearPrefetchCache(); + + expect(this.bloodhound.storage.clear).toHaveBeenCalled(); + }); + }); + + describe('#clearRemoteCache', function() { + it('should clear remote request cache', function() { + spyOn(Transport, 'resetCache'); + + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + remote: '/test' + }); + this.bloodhound.initialize(); + + this.bloodhound.clearRemoteCache(); + expect(Transport.resetCache).toHaveBeenCalled(); + }); + }); + describe('local', function() { describe('when local is an array', function() { beforeEach(function() { @@ -140,6 +242,20 @@ describe('Bloodhound', function() { ]); }); + it('should clear preexisting data', function() { + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + prefetch: '/test' + }); + + spyOn(this.bloodhound, 'clear'); + this.bloodhound.initialize(); + + mostRecentAjaxRequest().response(fixtures.ajaxResps.ok); + expect(this.bloodhound.clear).toHaveBeenCalled(); + }); + it('should filter data if filter is provided', function() { var filterSpy, spy; diff --git a/test/search_index_spec.js b/test/search_index_spec.js index 65655f90..b41e4af2 100644 --- a/test/search_index_spec.js +++ b/test/search_index_spec.js @@ -45,6 +45,13 @@ describe('SearchIndex', function() { expect(this.searchIndex.get('wtf')).toEqual([]); }); + it('#reset should empty the search index', function() { + this.searchIndex.reset(); + expect(this.searchIndex.datums).toEqual([]); + expect(this.searchIndex.trie.ids).toEqual([]); + expect(this.searchIndex.trie.children).toEqual({}); + }); + // helper functions // ----------------