diff --git a/README.md b/README.md index 5fb30f4..8f60362 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,38 @@ Finally we can invoke search queries by invoking following API. PackageSearch.search("the text to search"); ``` +### Pagination + +You can provide options to limit your search query and implement pagination into your search: `skip` and `limit`. SearchSource will also sanitize your limiting options to only fetch data from you backend that is not already in history (that is, if you enabled local history caching by configuring a value for keepInHistory). + +```js +PackageSearch.search("the text to search", { + skip: 0, + limit: 12 +}); +``` + +For example, when you do the following subsequent searches with the same text string to seacrh for +```js +[ + {skip: 0, limit: 24}, + {skip: 0, limit: 36}, + {skip: 12, limit: 36}, + {skip: 24, limit: 24}, +]; +``` + +SearchSource will send the following requests on your backend (when keepInhistory is enabled): +```js +[ + {skip: 0, limit: 24}, + {skip: 24, limit: 12}, + {skip: 36, limit: 12} +]; +``` + +To see an elaborate example on how to implement a paged search application, go to (http://infinitesearch.meteor.com). + ### Status You can get the status of the search source by invoking following API. It's reactive too. diff --git a/lib/client.js b/lib/client.js index 6c11495..004c1fa 100644 --- a/lib/client.js +++ b/lib/client.js @@ -13,21 +13,29 @@ SearchSource = function SearchSource(source, fields, options) { this._currentQueryDep = new Tracker.Dependency(); this._currentVersion = 0; this._loadedVersion = 0; -} +}; SearchSource.prototype._loadData = function(query, options) { var self = this; var version = 0; - if(this._canUseHistory(query)) { + + if(this._canUseHistory(query, options)) { this._updateStore(this.history[query].data); this.metaData.set(this.history[query].metadata); self._storeDep.changed(); - } else { - this.status.set({loading: true}); - version = ++this._currentVersion; - this._fetch(this.source, query, options, handleData); + return; } + if(options){ + if(this.options.keepHistory) { + options = this._changeOptionsForWhatsInCache(query, options); + } + } + + this.status.set({loading: true}); + version = ++this._currentVersion; + this._fetch(this.source, query, options, handleData); + function handleData(err, payload) { if(err) { self.status.set({error: err}); @@ -43,11 +51,25 @@ SearchSource.prototype._loadData = function(query, options) { } if(self.options.keepHistory) { - self.history[query] = {data: data, loaded: new Date(), metadata: metadata}; + var range = { + start: options.skip || 0 + }; + range.end = range.start + (options.limit || data.length); + self._mergeInHistory(query, { + data: data, + range: range, + loaded: new Date(), + metadata: metadata + }); } if(version > self._loadedVersion) { - self._updateStore(data); + if(self.options.keepHistory){ + //new data was already merge in history + self._updateStore(self.history[query].data); + } else { + self._updateStore(data); + } self._loadedVersion = version; } @@ -60,19 +82,83 @@ SearchSource.prototype._loadData = function(query, options) { } }; -SearchSource.prototype._canUseHistory = function(query) { - var historyItem = this.history[query]; - if(this.options.keepHistory && historyItem) { - var diff = Date.now() - historyItem.loaded.getTime(); - return diff < this.options.keepHistory; +SearchSource.prototype._canUseHistory = function(query, options) { + if(this.options.keepHistory) { + var historyItem = this.history[query]; + if(historyItem){ + //are limiting options within current range? + if(options.skip || options.limit){ + if(!options.skip){ + options.skip = 0; + } + if(!options.limit){ + //TODO: default limit + options.limit = 99999; + } + if( options.skip < historyItem.range.start || + (options.skip + options.limit) > historyItem.range.end){ + return false; + } + } + var diff = Date.now() - historyItem.loaded.getTime(); + return diff < this.options.keepHistory; + } } return false; }; +SearchSource.prototype._mergeInHistory = function(query, newData) { + var current = this.history[query]; + if(!current){ + this.history[query] = newData; + return; + } + if(current.range.start > newData.range.start){ + current.range.start = newData.range.start; + } + if(current.range.end < newData.range.end){ + current.range.end = newData.range.end; + } + + //check what new data needs to be pushed to history data + var currentIds = _.reduce(current.data, function(memo, d){ + memo[d._id] = true; + return memo; + }, {}); + + _.each(newData.data, function(newDoc){ + if(!currentIds[newDoc._id]){ + current.data.push(newDoc); + } + }); + this.history[query] = current; +}; + +SearchSource.prototype._changeOptionsForWhatsInCache = function(query, options) { + var historyItem = this.history[query]; + if(!historyItem){ + return options; + } + + if(options.skip || options.limit){ + if(!options.skip){ + options.skip = 0; + } + if( options.skip < historyItem.range.end ){ + if(options.limit){ + options.limit -= (historyItem.range.end - options.skip); + } + options.skip = historyItem.range.end; + } + } + + return options; +}; + SearchSource.prototype._updateStore = function(data) { var self = this; - var storeIds = _.pluck(this.store.find().fetch(), "_id"); + var storeIds = _.pluck(this.store.find({}, {fields: {_id: true}}).fetch(), "_id"); var currentIds = []; data.forEach(function(item) { currentIds.push(item._id); @@ -193,7 +279,7 @@ SearchSource.prototype.getMetadata = function() { SearchSource.prototype.getCurrentQuery = function() { this._currentQueryDep.depend(); return this.currentQuery; -} +}; SearchSource.prototype.getStatus = function() { return this.status.get();