import {defer, isXml, parse} from "./utils/core"; import httpRequest from "./utils/request"; import mime from "./utils/mime"; import Path from "./utils/path"; import EventEmitter from "event-emitter"; import localforage from "localforage"; /** * Handles saving and requesting files from local storage * @class * @param {string} name This should be the name of the application for modals * @param {function} [requester] * @param {function} [resolver] */ class Store { constructor(name, requester, resolver) { this.urlCache = {}; this.storage = undefined; this.name = name; this.requester = requester || httpRequest; this.resolver = resolver; this.online = true; this.checkRequirements(); this.addListeners(); } /** * Checks to see if localForage exists in global namspace, * Requires localForage if it isn't there * @private */ checkRequirements(){ try { let store; if (typeof localforage === "undefined") { store = localforage; } this.storage = store.createInstance({ name: this.name }); } catch (e) { throw new Error("localForage lib not loaded"); } } /** * Add online and offline event listeners * @private */ addListeners() { this._status = this.status.bind(this); window.addEventListener('online', this._status); window.addEventListener('offline', this._status); } /** * Remove online and offline event listeners * @private */ removeListeners() { window.removeEventListener('online', this._status); window.removeEventListener('offline', this._status); this._status = undefined; } /** * Update the online / offline status * @private */ status(event) { let online = navigator.onLine; this.online = online; if (online) { this.emit("online", this); } else { this.emit("offline", this); } } /** * Add all of a book resources to the store * @param {Resources} resources book resources * @param {boolean} [force] force resaving resources * @return {Promise} store objects */ add(resources, force) { let mapped = resources.resources.map((item) => { let { href } = item; let url = this.resolver(href); let encodedUrl = window.encodeURIComponent(url); return this.storage.getItem(encodedUrl).then((item) => { if (!item || force) { return this.requester(url, "binary") .then((data) => { return this.storage.setItem(encodedUrl, data); }); } else { return item; } }); }); return Promise.all(mapped); } /** * Put binary data from a url to storage * @param {string} url a url to request from storage * @param {boolean} [withCredentials] * @param {object} [headers] * @return {Promise} */ put(url, withCredentials, headers) { let encodedUrl = window.encodeURIComponent(url); return this.storage.getItem(encodedUrl).then((result) => { if (!result) { return this.requester(url, "binary", withCredentials, headers).then((data) => { return this.storage.setItem(encodedUrl, data); }); } return result; }); } /** * Request a url * @param {string} url a url to request from storage * @param {string} [type] specify the type of the returned result * @param {boolean} [withCredentials] * @param {object} [headers] * @return {Promise} */ request(url, type, withCredentials, headers){ if (this.online) { // From network return this.requester(url, type, withCredentials, headers).then((data) => { // save to store if not present this.put(url); return data; }) } else { // From store return this.retrieve(url, type); } } /** * Request a url from storage * @param {string} url a url to request from storage * @param {string} [type] specify the type of the returned result * @return {Promise} */ retrieve(url, type) { var deferred = new defer(); var response; var path = new Path(url); // If type isn't set, determine it from the file extension if(!type) { type = path.extension; } if(type == "blob"){ response = this.getBlob(url); } else { response = this.getText(url); } return response.then((r) => { var deferred = new defer(); var result; if (r) { result = this.handleResponse(r, type); deferred.resolve(result); } else { deferred.reject({ message : "File not found in storage: " + url, stack : new Error().stack }); } return deferred.promise; }); } /** * Handle the response from request * @private * @param {any} response * @param {string} [type] * @return {any} the parsed result */ handleResponse(response, type){ var r; if(type == "json") { r = JSON.parse(response); } else if(isXml(type)) { r = parse(response, "text/xml"); } else if(type == "xhtml") { r = parse(response, "application/xhtml+xml"); } else if(type == "html" || type == "htm") { r = parse(response, "text/html"); } else { r = response; } return r; } /** * Get a Blob from Storage by Url * @param {string} url * @param {string} [mimeType] * @return {Blob} */ getBlob(url, mimeType){ let encodedUrl = window.encodeURIComponent(url); return this.storage.getItem(encodedUrl).then(function(uint8array) { if(!uint8array) return; mimeType = mimeType || mime.lookup(url); return new Blob([uint8array], {type : mimeType}); }); } /** * Get Text from Storage by Url * @param {string} url * @param {string} [mimeType] * @return {string} */ getText(url, mimeType){ let encodedUrl = window.encodeURIComponent(url); mimeType = mimeType || mime.lookup(url); return this.storage.getItem(encodedUrl).then(function(uint8array) { var deferred = new defer(); var reader = new FileReader(); var blob; if(!uint8array) return; blob = new Blob([uint8array], {type : mimeType}); reader.addEventListener("loadend", () => { deferred.resolve(reader.result); }); reader.readAsText(blob, mimeType); return deferred.promise; }); } /** * Get a base64 encoded result from Storage by Url * @param {string} url * @param {string} [mimeType] * @return {string} base64 encoded */ getBase64(url, mimeType){ let encodedUrl = window.encodeURIComponent(url); mimeType = mimeType || mime.lookup(url); return this.storage.getItem(encodedUrl).then((uint8array) => { var deferred = new defer(); var reader = new FileReader(); var blob; if(!uint8array) return; blob = new Blob([uint8array], {type : mimeType}); reader.addEventListener("loadend", () => { deferred.resolve(reader.result); }); reader.readAsDataURL(blob, mimeType); return deferred.promise; }); } /** * Create a Url from a stored item * @param {string} url * @param {object} [options.base64] use base64 encoding or blob url * @return {Promise} url promise with Url string */ createUrl(url, options){ var deferred = new defer(); var _URL = window.URL || window.webkitURL || window.mozURL; var tempUrl; var response; var useBase64 = options && options.base64; if(url in this.urlCache) { deferred.resolve(this.urlCache[url]); return deferred.promise; } if (useBase64) { response = this.getBase64(url); if (response) { response.then(function(tempUrl) { this.urlCache[url] = tempUrl; deferred.resolve(tempUrl); }.bind(this)); } } else { response = this.getBlob(url); if (response) { response.then(function(blob) { tempUrl = _URL.createObjectURL(blob); this.urlCache[url] = tempUrl; deferred.resolve(tempUrl); }.bind(this)); } } if (!response) { deferred.reject({ message : "File not found in storage: " + url, stack : new Error().stack }); } return deferred.promise; } /** * Revoke Temp Url for a achive item * @param {string} url url of the item in the store */ revokeUrl(url){ var _URL = window.URL || window.webkitURL || window.mozURL; var fromCache = this.urlCache[url]; if(fromCache) _URL.revokeObjectURL(fromCache); } destroy() { var _URL = window.URL || window.webkitURL || window.mozURL; for (let fromCache in this.urlCache) { _URL.revokeObjectURL(fromCache); } this.urlCache = {}; this.removeListeners(); } } EventEmitter(Store.prototype); export default Store;