src/PaginatedResourceWrapper.js
/*
* BSD 3-Clause License
*
* Copyright (c) 2018, MapCreator
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import PaginatedResourceListing from './PaginatedResourceListing';
import ResourceCache from './ResourceCache';
import { hashObject } from './utils/hash';
/**
* Used for wrapping {@link PaginatedResourceListing} to make it spa friendly
* @todo Allow for manual cache updates, ex: a resource has been modified, deleted, created
* @deprecated
*/
export default class PaginatedResourceWrapper {
/**
*
* @param {PaginatedResourceListing} listing - Listing result
* @param {Maps4News} api - Instance of the api
* @param {Boolean} shareCache - Share cache across instances
*/
constructor (listing, api = listing.api, shareCache = api.defaults.shareCache) {
// Fields
this._api = api;
this._shareCache = shareCache;
this._currentPage = 1;
this._context = [];
/**
* Available data assembled from the cache
* @type {Array<ResourceBase>} - Available data
*/
this.data = [];
// Internal
this._localCache = new ResourceCache(this.api.defaults.cacheSeconds, this.api.defaults.dereferenceCache);
this._inflight = [];
this._last = listing;
this._waiting = false;
this.on('invalidate', () => this.rebuild());
this._promiseCallback(listing);
}
get _promiseCallback () {
return result => {
const query = this.query();
this._last = result;
this._query = query;
this.cache.push(result);
const inflightId = this.inflight.findIndex(x => x === result.page);
if (inflightId >= 0) {
this._inflight.splice(inflightId, 1);
}
this._waiting = this.inflight.length > 0;
this.rebuild();
};
}
/**
* Manually fetch a page. This will change the current page.
* @param {Number|Array<Number>} pageId - Page(s) to fetch
*/
get (pageId) {
if (pageId instanceof Array) {
pageId.map(this.get);
} else {
this._waiting = true;
this._inflight.push(pageId);
void (async () => {
this._promiseCallback(await this._last.getPage(pageId));
})();
}
}
/**
* Grab the next page
*/
next () {
this.get(++this.currentPage);
}
/**
* Grab the previous page
*/
previous () {
this.get(--this.currentPage);
}
/**
* Manually rebuild the data
*/
rebuild () {
this.data = this.cache
.resolve(this.route, this._last.cacheToken)
.filter(value => typeof value !== 'undefined');
this.cache.emitter.emit('post-rebuild', { resourceUrl: this._last.route });
}
/**
* Updates the cached pages.
* @param {Boolean} flush - Clear the cached route data
* @example
* function onRefresh() {
* if(wrapper.waiting) {
* return; // not done yet
* }
*
* wrapper.off('post-rebuild', onRefresh);
*
* // Do stuff here
* }
*
* wrapper.on('post-rebuild', onRefresh);
* wrapper.refresh();
*/
refresh (flush = false) {
if (flush) {
this.cache.clear(this.route);
}
this.cache
.collectPages(this.route, this._last.cacheToken)
.map(page => this.get(page.page));
}
/**
* Returns the page number that is currently being used as a reference point
* @returns {Number} - The current page
* @see {@link PaginatedResourceWrapper#next}
* @see {@link PaginatedResourceWrapper#previous}
*/
get currentPage () {
return this._currentPage;
}
/**
* Set the current page number
* @param {Number} value - page number
*/
set currentPage (value) {
this._currentPage = Math.max(1, value);
}
/**
* Get the route of the resource
* @returns {String} - route
*/
get route () {
return this._last.route;
}
/**
* Override the resource route
* @param {String} value - route
*/
set route (value) {
this._route = value;
}
/**
* Row count
* @returns {Number} - Row count
*/
get rows () {
return this._last.rows;
}
/**
* Get the number of pages available
* @returns {Number} - Page count
*/
get pageCount () {
return this._last.pageCount;
}
/**
* Set the request params and submit
* @param {?Object<String, *>} value - Query
* @throws {TypeError}
* @default {}
* @see {@link ResourceProxy#search}
* @returns {Object<String, String|Array<String>>} - query
*/
query (value = null) {
if (!value || value === this.query()) {
return this._last.query;
}
this._context[this._last.cacheToken] = this._last;
const token = hashObject(value);
if (this._context[token]) {
this._last = this._context[token];
} else {
const parameters = this._last.parameters.copy();
parameters.page = 1;
parameters.apply(value);
this._last = new PaginatedResourceListing(this.api, this._last.route, this._last.Target, parameters);
this.get(parameters.page);
this.currentPage = 1;
}
this.rebuild();
return this.query();
}
/**
* Get api instance
* @returns {Maps4News} - Api instance
*/
get api () {
return this._api;
}
/**
* Get the active cache instance
* @returns {ResourceCache} - Cache instance
*/
get cache () {
return this.shareCache ? this.api.cache : this._localCache;
}
/**
* Get if the shared cache should be used
* @returns {Boolean} - Should the shared cache be used
*/
get shareCache () {
return this._shareCache;
}
/**
* Sets if the shared cache should be used
* @param {Boolean} value - Should the shared cache be used
*/
set shareCache (value) {
this._shareCache = Boolean(value);
}
/**
* If there is a next page
* @returns {boolean} - If there is a next page
*/
get hasNext () {
return this.inflight.length === 0 ? this._last.hasNext : this.currentPage < this.pageCount;
}
/**
* If there is a previous page
* @returns {boolean} - If there is a previous page
*/
get hasPrevious () {
return this._last.hasPrevious;
}
/**
* List of page numbers that are still mid-flight
* @returns {Array} - Page numbers that are still mid-flight
*/
get inflight () {
return this._inflight;
}
/**
* Returns if there are still requests mid-flight
* @returns {boolean} - Returns if the wrapper is waiting for requests to finish
*/
get waiting () {
return this._waiting;
}
/**
* Register an event handler for the given type.
*
* @param {string} type - Type of event to listen for, or `"*"` for all events.
* @param {function(eventType: string, event: any): void|function(event: any): void} handler - Function to call in response to the given event.
*/
on (type, handler) {
this.cache.emitter.on(type, (t, e) => {
if (type === '*' && e.resourceUrl === this.route) {
handler(t, e);
} else if (type !== '*' && t.resourceUrl === this.route) {
handler(t);
}
});
}
/**
* Function to call in response to the given event
*
* @param {string} type - Type of event to unregister `handler` from, or `"*"`
* @param {function(event: any): void} handler - Handler function to remove.
*/
off (type, handler) {
this.cache.emitter.off(type, handler);
}
}