UNPKG

@georstat/react-native-image-cache

Version:

React Native image file system caching for iOS and Android

265 lines (262 loc) 8.87 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.CacheEntry = void 0; var _sha = _interopRequireDefault(require("crypto-js/sha1")); var _reactNativeFileAccess = require("react-native-file-access"); var _defaultConfiguration = _interopRequireDefault(require("./defaultConfiguration")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // @ts-ignore async function retry(fn) { let retriesLeft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : CacheManager.config.maxRetries || 0; let interval = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : CacheManager.config.retryDelay; try { /* for some reason FileSystem.fetch won't throw error if image is not found * we need to catch the errors locally */ const request = await fn(); switch (request.status) { case 404: throw new Error(request.status); case 401: throw new Error(request.status); case 408: throw new Error(request.status); case 500: throw new Error(request.status); case 503: throw new Error(request.status); default: return request; } } catch (error) { /* abort early if the image is not found or * the access is not authorized */ if (error.message === '404' || error.message === '401' || error.message === '500' || error.message === '503' || error.message === '408') { throw new Error(error); } /* FileSystem.fetch throws error if device is offline/temp internet loss with message "Host unreachable" or "Unable to resolve host" * so keep trying */ if (retriesLeft === 0) { throw new Error(`Maximum retries exceeded: ${error.message}`); } await new Promise(resolve => setTimeout(resolve, interval)); return retry(fn, retriesLeft - 1, interval); } } class CacheEntry { pathResolved = false; noCache = false; constructor(source, options, noCache, maxAge) { this.noCache = noCache; this.options = options; this.source = source; this.maxAge = maxAge; } async getPath() { const { source, maxAge, noCache } = this; const { exists, path } = await getCacheEntry(source, maxAge); if (exists && !noCache) { return path; } if (!this.downloadPromise) { this.pathResolved = false; this.downloadPromise = this.download(path); } if (this.downloadPromise && this.pathResolved) { this.pathResolved = false; this.downloadPromise = this.download(path); } return this.downloadPromise; } async download(path) { const { source, options, noCache } = this; /* if noCache is true then return the source uri without caching it */ if (noCache) { return source; } if (source != null) { try { const result = await retry(() => _reactNativeFileAccess.FileSystem.fetch(source, { path, ...options })); /* If the image download failed, we don't cache anything */ if (result && result.status !== 200) { this.downloadPromise = undefined; return undefined; } } catch (e) { if (__DEV__) { console.log(`FileSystem.fetch has some trouble, error: ${e instanceof Error ? e.message : 'unknown'}`); } this.downloadPromise = undefined; return undefined; } if (CacheManager.config.cacheLimit) { await CacheManager.pruneCache(); } this.pathResolved = true; return path; } return source; } } exports.CacheEntry = CacheEntry; class CacheManager { static defaultConfig = _defaultConfiguration.default; get config() { return CacheManager.defaultConfig; } set config(newConfig) { CacheManager.defaultConfig = newConfig; } static entries = {}; static get(source, options, noCache, maxAge) { var _CacheManager$entries, _options$headers; if (!CacheManager.entries[source] || ((_CacheManager$entries = CacheManager.entries[source].options) === null || _CacheManager$entries === void 0 || (_CacheManager$entries = _CacheManager$entries.headers) === null || _CacheManager$entries === void 0 ? void 0 : _CacheManager$entries.Authorization) !== (options === null || options === void 0 || (_options$headers = options.headers) === null || _options$headers === void 0 ? void 0 : _options$headers.Authorization)) { CacheManager.entries[source] = new CacheEntry(source, options, noCache, maxAge); return CacheManager.entries[source]; } return CacheManager.entries[source]; } static async clearCache() { if (await _reactNativeFileAccess.FileSystem.exists(CacheManager.config.baseDir)) { const files = await _reactNativeFileAccess.FileSystem.ls(CacheManager.config.baseDir); for (const file of files) { try { await _reactNativeFileAccess.FileSystem.unlink(`${CacheManager.config.baseDir}${file}`); } catch (e) { if (__DEV__) { console.log(`error while clearing images cache, error: ${e}`); } } } } } static async removeCacheEntry(entry) { try { const file = await getCacheEntry(entry); const { path } = file; await _reactNativeFileAccess.FileSystem.unlink(path); } catch (e) { throw new Error('error while clearing image from cache'); } } static async getCacheSize() { const result = await _reactNativeFileAccess.FileSystem.stat(CacheManager.config.baseDir); if (!result) { throw new Error(`${CacheManager.config.baseDir} not found`); } return result.size; } static async isImageCached(entry) { try { const file = await getCacheEntry(entry); const { exists } = file; return exists; } catch (e) { throw new Error('Error while checking if image already exists on cache'); } } static prefetch(source, options) { if (typeof source === 'string') { CacheManager.get(source, options).getPath(); } else { source.forEach(image => { CacheManager.get(image, options).getPath(); }); } } static async prefetchBlob(source, options) { const path = await CacheManager.get(source, options).getPath(); if (path) { const blob = await _reactNativeFileAccess.FileSystem.readFile(path, 'base64'); return blob; } return undefined; } static async pruneCache() { /* If cache directory does not exist yet there's no need for pruning. */ if (!(await CacheManager.getCacheSize())) { return; } const files = await _reactNativeFileAccess.FileSystem.statDir(CacheManager.config.baseDir); files.sort((a, b) => { return a.lastModified - b.lastModified; }); const currentCacheSize = files.reduce((cacheSize, file) => { return cacheSize + file.size; }, 0); if (currentCacheSize > CacheManager.config.cacheLimit) { let overflowSize = currentCacheSize - CacheManager.config.cacheLimit; while (overflowSize > 0 && files.length) { const file = files.shift(); if (file) { if (await _reactNativeFileAccess.FileSystem.exists(file.path)) { overflowSize = overflowSize - file.size; await _reactNativeFileAccess.FileSystem.unlink(file.path).catch(e => { if (__DEV__) { console.log(e); } }); } } } } } } exports.default = CacheManager; const getCacheEntry = async (cacheKey, maxAge) => { let newCacheKey = cacheKey; if (CacheManager.config.getCustomCacheKey) { newCacheKey = CacheManager.config.getCustomCacheKey(cacheKey); } const filename = cacheKey.substring(cacheKey.lastIndexOf('/'), cacheKey.indexOf('?') === -1 ? cacheKey.length : cacheKey.indexOf('?')); const ext = filename.indexOf('.') === -1 ? '.jpg' : filename.substring(filename.lastIndexOf('.')); const sha = (0, _sha.default)(newCacheKey); const path = `${CacheManager.config.baseDir}${sha}${ext}`; // TODO: maybe we don't have to do this every time try { await _reactNativeFileAccess.FileSystem.mkdir(CacheManager.config.baseDir); } catch (e) { /* do nothing */ } const exists = await _reactNativeFileAccess.FileSystem.exists(path); if (maxAge && exists) { const { lastModified } = await _reactNativeFileAccess.FileSystem.stat(path); const ageInHours = Math.floor(Date.now() - lastModified) / 1000 / 3600; if (maxAge < ageInHours) { await _reactNativeFileAccess.FileSystem.unlink(path); return { exists: false, path }; } } return { exists, path }; }; //# sourceMappingURL=CacheManager.js.map