@mnightingale/react-native-image-cache-hoc
Version:
React Native Higher Order Component that adds advanced caching functionality to the react native Image component.
786 lines (665 loc) • 31.2 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var _extends = require('@babel/runtime/helpers/extends');
var React = require('react');
var reactNative = require('react-native');
var pathLib = require('path');
var RNFS = require('react-native-fs');
var sha1 = require('crypto-js/sha1');
var URL = require('url-parse');
var rxjs = require('rxjs');
var operators = require('rxjs/operators');
var uuid = require('react-native-uuid');
var traverse = require('traverse');
var validator = require('validator');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends);
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var pathLib__default = /*#__PURE__*/_interopDefaultLegacy(pathLib);
var RNFS__default = /*#__PURE__*/_interopDefaultLegacy(RNFS);
var sha1__default = /*#__PURE__*/_interopDefaultLegacy(sha1);
var URL__default = /*#__PURE__*/_interopDefaultLegacy(URL);
var uuid__default = /*#__PURE__*/_interopDefaultLegacy(uuid);
var traverse__default = /*#__PURE__*/_interopDefaultLegacy(traverse);
var validator__default = /*#__PURE__*/_interopDefaultLegacy(validator);
/**
*
* This library acts as an interface to abstract the OS file system and is platform independent.
*
* File paths passed into this library should be relative, as the base file path is different
* across platforms, and therefore is automatically set by this library.
*
*/
/**
* Resolves if 'unlink' resolves or if the file doesn't exist.
*
* @param {string} filename
*/
const RNFSUnlinkIfExists = filename => RNFS__default['default'].exists(filename).then(exists => {
if (exists) {
return RNFS__default['default'].unlink(filename);
}
return Promise.resolve();
});
class FileSystem {
/**
* All FileSystem instances will reference the cacheLock singleton "dictionary" to provide cache file locking in order to prevent concurrency race condition bugs.
*
* cacheLock structure:
*
* cacheLock = {
* 'filename.jpg': {
* 'componentIdOne': true,
* 'componentIdTwo': true
* }
* }
*
* In the above example cache/filename.jpg cannot be deleted during cache pruning,
* until both components release their locks on the dependant image file.
*
* One example (of many) of how a race condition could occur:
*
* 1st <CacheableImage> mounts and downloads image file into cache, calls render() (or not) etc.
* 2nd <CacheableImage> mounts concurrently and during download runs pruneCache(), deleting 1st <CacheableImage>'s image file from cache.
* 1st <CacheableImage> calls render() again at some point in the future, at this time it's image file no longer exists so the render fails.
*/
static lockCacheFile(fileName, componentId) {
// If file is already locked, add additional component lock, else create initial file lock.
if (FileSystem.cacheLock[fileName]) {
FileSystem.cacheLock[fileName][componentId] = true;
} else {
const componentDict = {};
componentDict[componentId] = true;
FileSystem.cacheLock[fileName] = componentDict;
}
}
static unlockCacheFile(fileName, componentId) {
// Delete component lock on cache file
if (FileSystem.cacheLock[fileName]) {
delete FileSystem.cacheLock[fileName][componentId]; // If no further component locks remain on cache file, delete filename property from cacheLock dictionary.
if (Object.keys(FileSystem.cacheLock[fileName]).length === 0) {
delete FileSystem.cacheLock[fileName];
if (FileSystem.cacheObservables[fileName]) {
delete FileSystem.cacheObservables[fileName];
}
}
}
}
constructor(cachePruneTriggerLimit, fileDirName) {
this.baseFilePath = void 0;
this.cachePruneTriggerLimit = void 0;
this.cachePruneTriggerLimit = cachePruneTriggerLimit || 1024 * 1024 * 15; // Maximum size of image file cache in bytes before pruning occurs. Defaults to 15 MB.
fileDirName = fileDirName || 'react-native-image-cache-hoc'; // Namespace local file writing to this folder.
this.baseFilePath = this._setBaseFilePath(fileDirName);
}
/**
*
* Sets the base file directory depending on platform. Prefix is used to avoid local file collisions.
*
* Apple:
* Apple requires non-user generated files to be stored in cache dir, they can be flagged to persist. https://developer.apple.com/icloud/documentation/data-storage/index.html
* it appears reactNativeFetchBlob will flag these files to persist behind the scenes, so cache dir is safe on apple. See: https://www.npmjs.com/package/rn-fetch-blob#cache-file-management
*
* Android:
* Android appears to purge cache dir files so we should use document dir to play it safe (even with reactNativeFetchBlob abstraction) : https://developer.android.com/guide/topics/data/data-storage.html
*
* @returns {String} baseFilePath - base path that files are written to in local fs.
* @private
*/
_setBaseFilePath(fileDirName) {
let baseFilePath = reactNative.Platform.OS === 'ios' ? RNFS__default['default'].CachesDirectoryPath : RNFS__default['default'].DocumentDirectoryPath;
baseFilePath += '/' + fileDirName + '/';
return baseFilePath;
}
/**
*
* Pseudo-chroot paths to the baseFilePath set in _setBaseFilePath().
*
* This method should be called on any passed in path to prevent writing to invalid directories.
* IE passing in a path of '../../../../../home/should/not/write/here/file.png' to break out of
* the base file path directory.
*
* @param path {String} - local fs path.
* @param absolute {String} - whether or not the passed in path is absolute or relative. Defaults to relative since base path differs across platforms.
* @returns {boolean} - Whether or not the file path is valid.
* @throws error on bad filepath.
* @private
*/
_validatePath(path, absolute = false) {
const relative = pathLib__default['default'].relative(this.baseFilePath, absolute ? path : this.baseFilePath + path);
if (path !== '' && (!relative || relative.startsWith('..') || pathLib__default['default'].isAbsolute(relative))) {
// resolve turns any path into an absolute path (ie: /folder1/folder2/../example.js resolves to /folder1/example.js)
const resolvedPath = absolute ? pathLib__default['default'].resolve(path) : pathLib__default['default'].resolve(this.baseFilePath + path);
throw new Error(resolvedPath + ' is not a valid file path.');
} else {
return true;
}
}
/**
*
* Wrapper for https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#existspathstringpromise
*
* @param path - local relative file path.
* @returns {Promise} - boolean promise for if file exists at path or not.
*/
exists(path) {
this._validatePath(path);
return RNFS__default['default'].exists(pathLib__default['default'].resolve(this.baseFilePath + path));
}
/**
*
* Creates a SHA1 hash filename from a url and normalizes extension.
*
* @param url {String} - An absolute url.
* @throws error on invalid (non jpg, png, gif, bmp) url file type. NOTE file extension or content-type header does not guarantee file mime type. We are trusting that it is set correctly on the server side.
* @returns fileName {string} - A SHA1 filename that is unique to the resource located at passed in URL and includes an appropriate extension.
*/
getFileNameFromUrl(url) {
const urlParts = new URL__default['default'](url);
const urlExt = urlParts.pathname.split('.').pop(); // react-native enforces Image src to default to a file extension of png
const extension = urlExt === urlParts.pathname ? 'bin' : urlExt;
return sha1__default['default'](url).toString() + '.' + extension;
}
/**
*
* Convenience method used to get the associated local file path of a web image that has been written to disk.
* If the local file does not exist yet, the remote file is downloaded to local disk then the local filepath is returned.
*
* @param url {String} - url of file to download.
* @returns {Promise<string|null>} promise that resolves to the local file path of downloaded url file.
*/
async getLocalFilePathFromUrl(url) {
const fileName = this.getFileNameFromUrl(url);
const requestId = uuid__default['default'].v4();
try {
FileSystem.lockCacheFile(fileName, requestId);
const {
path
} = await this.observable(url, requestId, 'immutable', fileName).pipe(operators.take(1)).toPromise();
return path;
} finally {
FileSystem.unlockCacheFile(fileName, requestId);
}
}
/**
*
* Manually move or copy a local file to the cache.
* Can be used to pre-warm caches.
* If calling this method repeatedly to cache a long list of files,
* be sure to use a queue and limit concurrency so your app performance does not suffer.
*
* @param local {String} - path to the local file.
* @param url {String} - url of file to download.
* @param move {Boolean} - whether the file should be copied or moved.
* @param mtime {Date} - creation timestamp
* @param ctime {Date} - modification timestamp (iOS only)
* @returns {Promise} promise that resolves to an object that contains cached file info.
*/
async cacheLocalFile(local, url, move = false, mtime, ctime) {
const fileName = this.getFileNameFromUrl(url);
const path = this.baseFilePath + fileName;
this._validatePath(path, true);
if (!(await RNFS__default['default'].exists(local))) {
return {
url,
path: null
};
} // Logic here prunes cache directory on "cache" writes to ensure cache doesn't get too large.
await this.pruneCache(); // Move or copy the file to the cache
try {
const cacheDirExists = await this.exists('');
if (!cacheDirExists) {
await RNFS__default['default'].mkdir(this.baseFilePath);
}
await RNFSUnlinkIfExists(path);
await (move ? RNFS__default['default'].moveFile(local, path) : RNFS__default['default'].copyFile(local, path)); // Update the modified and created times of the file otherwise the if-modified-since request will probably always
await RNFS__default['default'].touch(path, mtime !== null && mtime !== void 0 ? mtime : new Date(), ctime !== null && ctime !== void 0 ? ctime : new Date());
} catch (error) {
await RNFSUnlinkIfExists(path);
return {
url,
path: null
};
} // Publish to subscribers that the image for this url has been updated
if (FileSystem.cacheObservables[fileName]) {
FileSystem.cacheObservables[fileName].next({
path: 'file://' + path,
fileName,
md5: await RNFS__default['default'].hash(path, 'md5').catch(() => undefined)
});
}
return {
url,
path
};
}
/**
*
* Used to download files to local filesystem.
*
* @param url {String} - url of file to download.
* @param fileName {String} - defaults to a sha1 hash of the url param with extension of same filetype.
* @returns {Observable<CacheFileInfo>} observable that resolves to an object that contains the local path of the downloaded file and the filename.
*/
fetchFile(url, fileName = null, headers) {
fileName = fileName || this.getFileNameFromUrl(url);
const path = this.baseFilePath + fileName;
this._validatePath(path, true);
return rxjs.from(this.exists('')).pipe(operators.delayWhen(cacheDirExists => // Logic here prunes cache directory on "cache" writes to ensure cache doesn't get too large.
rxjs.from(cacheDirExists ? this.pruneCache() : RNFS__default['default'].mkdir(this.baseFilePath))), operators.mergeMap(() => rxjs.from(RNFS__default['default'].exists(path))), operators.mergeMap(alreadyExists => // Hit network and download file to local disk.
rxjs.defer(async () => {
let beginResult;
const downloadResult = await RNFS__default['default'].downloadFile({
fromUrl: url,
toFile: path,
headers,
begin: res => {
beginResult = res;
}
}).promise;
return Promise.resolve({
downloadResult,
beginResult
});
}).pipe( // If "304 Not Modified" response touch local file
operators.tap(({
downloadResult
}) => {
if (alreadyExists && downloadResult.statusCode === 304) {
// Unfortunately react-native-fs only shares headers for status >=200 && status < 300
RNFS__default['default'].touch(path, new Date(), new Date());
}
}), // Only need to emit if the local file has changed
operators.filter(({
downloadResult
}) => downloadResult.statusCode === 200), operators.mergeMap(({
downloadResult,
beginResult
}) => rxjs.defer(async () => {
if (beginResult !== undefined && 'ETag' in beginResult.headers) {
return {
md5: beginResult.headers['ETag']
};
}
return {
md5: await RNFS__default['default'].hash(path, 'md5').catch(() => undefined)
};
}).pipe(operators.map(extra => ({
downloadResult,
beginResult,
extra
})))), operators.map(({
extra
}) => {
return {
path: 'file://' + path,
fileName: pathLib__default['default'].basename(path),
md5: extra.md5
};
}))), operators.catchError(() => rxjs.from(RNFSUnlinkIfExists(path)).pipe(operators.mapTo({
path: null,
fileName: pathLib__default['default'].basename(path)
}))));
}
/**
* Used to remove files from cache directory if the cache grows too large.
* This function will delete files from the cache until the total cache size
* is less than FileSystem.cachePruneTriggerLimit setting.
*
* @returns {Promise}
*/
async pruneCache() {
// If cache directory does not exist yet there's no need for pruning.
if (!(await this.exists(''))) {
return;
} // Get directory contents
const dirContents = await RNFS__default['default'].readDir(this.baseFilePath); // Sort dirContents in order of oldest to newest file.
dirContents.sort((a, b) => {
var _a$mtime$getTime, _a$mtime, _b$mtime$getTime, _b$mtime;
return ((_a$mtime$getTime = (_a$mtime = a.mtime) === null || _a$mtime === void 0 ? void 0 : _a$mtime.getTime()) !== null && _a$mtime$getTime !== void 0 ? _a$mtime$getTime : 0) - ((_b$mtime$getTime = (_b$mtime = b.mtime) === null || _b$mtime === void 0 ? void 0 : _b$mtime.getTime()) !== null && _b$mtime$getTime !== void 0 ? _b$mtime$getTime : 0);
});
const currentCacheSize = dirContents.reduce((cacheSize, blobStatObject) => {
return cacheSize + parseInt(blobStatObject.size);
}, 0); // Prune cache if current cache size is too big.
if (currentCacheSize > this.cachePruneTriggerLimit) {
let overflowSize = currentCacheSize - this.cachePruneTriggerLimit;
const unlinkPromises = []; // Keep deleting cached files so long as the current cache size is larger than the size required to trigger cache pruning, or until
// all cache files have been evaluated.
while (overflowSize > 0 && dirContents.length) {
const contentFile = dirContents.shift(); // Only prune unlocked files from cache
if (contentFile && !FileSystem.cacheLock[contentFile.name] && this._validatePath(contentFile.name)) {
overflowSize -= parseInt(contentFile.size);
unlinkPromises.push(RNFSUnlinkIfExists(this.baseFilePath + contentFile.name));
}
}
await Promise.all(unlinkPromises);
}
}
/**
* Used to delete local files and directories
*
* @param path - local relative file path.
* @returns {Promise} - boolean promise for if deletion was successful.
*/
async unlink(path) {
this._validatePath(path);
try {
await RNFSUnlinkIfExists(pathLib__default['default'].resolve(this.baseFilePath + path));
const obs$ = FileSystem.cacheObservables[path];
if (obs$) {
obs$.next({
path: null,
fileName: path
});
}
return true;
} catch (error) {
return false;
}
}
/**
* Gets a observable which emits when a url is resolved to a local file path
* A cache lock is required @see {lockCacheFile}
*
* @param url {String} - url of file to download.
* @param componentId {String} - Unique id of the requestor.
* @param cacheStrategy {CacheStrategy} - The cache strategy to use, defaults to 'immutable'.
* @param fileName {String} - defaults to a sha1 hash of the url param with extension of same filetype.
* @param headers {HeaderFn} - Headers to include with requests
* @returns {Observable<CacheFileInfo>} observable that resolves to an object that contains the local path of the downloaded file and the filename.
*/
observable(url, componentId, cacheStrategy = 'immutable', fileName = null, headers = {}) {
if (!url) {
return rxjs.of({
path: null,
fileName: ''
});
} // Check for invalid cache strategies
if (cacheStrategy !== 'immutable' && cacheStrategy !== 'mutable') {
throw new Error(`Invalid CacheStrategy ${cacheStrategy} is unhandled`);
}
fileName = fileName || this.getFileNameFromUrl(url);
if (!FileSystem.cacheLock[fileName] || !FileSystem.cacheLock[fileName][componentId]) {
throw new Error('A lock must be aquired before requesting an observable');
}
if (!FileSystem.cacheObservables[fileName]) {
this._validatePath(fileName);
const subject$ = new rxjs.ReplaySubject();
const obs$ = rxjs.from(RNFS__default['default'].hash(this.baseFilePath + fileName, 'md5')).pipe(operators.catchError(() => rxjs.of(null)), operators.switchMap(md5 => {
if (md5 !== null) {
switch (cacheStrategy) {
case 'immutable':
{
return rxjs.of({
path: 'file://' + this.baseFilePath + fileName,
fileName,
md5
});
}
case 'mutable':
{
return rxjs.from([rxjs.of({
path: 'file://' + this.baseFilePath + fileName,
fileName,
md5
}), rxjs.defer(async () => headers instanceof Function ? await headers() : headers).pipe(operators.mergeMap(headers => this.fetchFile(url, fileName, {
'if-none-match': md5,
...headers
})))]).pipe(operators.concatAll());
}
}
} // Download
return rxjs.defer(async () => headers instanceof Function ? await headers() : headers).pipe(operators.mergeMap(headers => this.fetchFile(url, fileName, headers)));
}), operators.publishReplay(1), operators.refCount()); // Subscribe
obs$.subscribe(v => subject$.next(v));
return FileSystem.cacheObservables[fileName] = subject$;
}
return FileSystem.cacheObservables[fileName];
}
}
/**
* Export FileSystem factory for convenience.
*
* @returns {FileSystem}
*/
FileSystem.cacheLock = {};
FileSystem.cacheObservables = {};
function FileSystemFactory(cachePruneTriggerLimit, fileDirName) {
return new FileSystem(cachePruneTriggerLimit || null, fileDirName || null);
}
const imageCacheHoc = (Wrapped, options = {}) => {
var _temp;
// Validate options
if (options.validProtocols && !Array.isArray(options.validProtocols)) {
throw new Error('validProtocols option must be an array of protocol strings.');
}
if (options.fileHostWhitelist && !Array.isArray(options.fileHostWhitelist)) {
throw new Error('fileHostWhitelist option must be an array of host strings.');
}
if (options.cachePruneTriggerLimit && !Number.isInteger(options.cachePruneTriggerLimit)) {
throw new Error('cachePruneTriggerLimit option must be an integer.');
}
if (options.fileDirName && typeof options.fileDirName !== 'string') {
throw new Error('fileDirName option must be string');
}
if (options.defaultPlaceholder && typeof options.defaultPlaceholder !== 'object') {
throw new Error('defaultPlaceholder option must be a ReactNode');
}
if (options.headers && !(typeof options.headers === 'function' || typeof options.headers === 'object')) {
throw new Error('headers option must be a function or object');
}
return _temp = class extends React__default['default'].PureComponent {
/**
*
* Manually cache a file.
* Can be used to pre-warm caches.
* If calling this method repeatedly to cache a long list of files,
* be sure to use a queue and limit concurrency so your app performance does not suffer.
*
* @param url {String} - url of file to download.
* @returns {Promise} promise that resolves to an object that contains cached file info.
*/
static async cacheFile(url) {
const localFilePath = await this.fileSystem().getLocalFilePathFromUrl(url);
return {
url: url,
localFilePath
};
}
/**
*
* Manually move or copy a local file to the cache.
* Can be used to pre-warm caches.
* If calling this method repeatedly to cache a long list of files,
* be sure to use a queue and limit concurrency so your app performance does not suffer.
*
* @param local {String} - path to the local file.
* @param url {String} - url of file to download.
* @param move {Boolean} - whether the file should be copied or moved.
* @param mtime {Date} - creation timestamp
* @param ctime {Date} - modification timestamp (iOS only)
* @returns {Promise} promise that resolves to an object that contains cached file info.
*/
static async cacheLocalFile(local, url, move = false, mtime, ctime) {
return this.fileSystem().cacheLocalFile(local, url, move, mtime, ctime);
}
/**
*
* Delete all locally stored image files created by react-native-image-cache-hoc.
* Calling this method will cause a performance hit on your app until the local files are rebuilt.
*
* @returns {Promise} promise that resolves to an object that contains the flush results.
*/
static async flush() {
return this.fileSystem().unlink('');
}
/**
* Export FileSystem for convenience.
*
* @returns {FileSystem}
*/
static fileSystem() {
return FileSystemFactory(options.cachePruneTriggerLimit || null, options.fileDirName || null);
}
constructor(props) {
var _options$headers;
super(props); // Set initial state
this.componentId = void 0;
this.unmounted$ = void 0;
this.options = void 0;
this.fileSystem = void 0;
this.subscription = void 0;
this.invalidUrl = void 0;
this.state = {
source: undefined
}; // Assign component unique ID for cache locking.
this.componentId = uuid__default['default'].v4(); // Track component mount status to avoid calling setState() on unmounted component.
this.unmounted$ = new rxjs.BehaviorSubject(true); // Set default options
this.options = {
validProtocols: options.validProtocols || ['https'],
fileHostWhitelist: options.fileHostWhitelist || [],
cachePruneTriggerLimit: options.cachePruneTriggerLimit || 1024 * 1024 * 15,
// Maximum size of image file cache in bytes before pruning occurs. Defaults to 15 MB.
fileDirName: options.fileDirName || null,
// Namespace local file writing to this directory. Defaults to 'react-native-image-cache-hoc'.
defaultPlaceholder: options.defaultPlaceholder || null,
// Default placeholder component to render while remote image file is downloading. Can be overridden with placeholder prop. Defaults to <Image> component with style prop passed through.
headers: (_options$headers = options.headers) !== null && _options$headers !== void 0 ? _options$headers : {}
}; // Init file system lib
this.fileSystem = FileSystemFactory(this.options.cachePruneTriggerLimit, this.options.fileDirName); // Validate input
this.invalidUrl = !this._validateImageComponent();
}
_validateImageComponent() {
// Define validator options
const validatorUrlOptions = {
protocols: this.options.validProtocols,
// eslint-disable-next-line @typescript-eslint/camelcase
require_protocol: true
};
if (this.options.fileHostWhitelist.length) {
// eslint-disable-next-line @typescript-eslint/camelcase
validatorUrlOptions.host_whitelist = this.options.fileHostWhitelist;
} // Validate source prop to be a valid web accessible url.
if (!traverse__default['default'](this.props).get(['source', 'uri']) || !validator__default['default'].isURL(traverse__default['default'](this.props).get(['source', 'uri']), validatorUrlOptions)) {
console.warn('Invalid source prop. <CacheableImage> props.source.uri should be a web accessible url with a valid protocol and host. NOTE: Default valid protocol is https, default valid hosts are *.');
return false;
} else {
return true;
}
} // Async calls to local FS or network should occur here.
// See: https://reactjs.org/docs/react-component.html#componentdidmount
componentDidMount() {
// Track component mount status to avoid calling setState() on unmounted component.
this.unmounted$.next(false); // Set url from source prop
const url = traverse__default['default'](this.props).get(['source', 'uri']);
const cacheStrategy = traverse__default['default'](this.props).get(['source', 'cache']) || 'immutable';
const isFile = url && new URL__default['default'](url).protocol === 'file:';
if (isFile || !this.invalidUrl) {
if (isFile) {
this.onSourceLoaded({
path: url,
fileName: this.fileSystem.getFileNameFromUrl(url)
});
} else {
// Add a cache lock to file with this name (prevents concurrent <CacheableImage> components from pruning a file with this name from cache).
const fileName = this.fileSystem.getFileNameFromUrl(url);
FileSystem.lockCacheFile(fileName, this.componentId); // Init the image cache logic
this.subscription = this.fileSystem.observable(url, this.componentId, cacheStrategy, null, this.options.headers).pipe(operators.takeUntil(this.unmounted$.pipe(operators.skip(1)))).subscribe(info => this.onSourceLoaded(info));
}
}
}
/**
*
* Enables caching logic to work if component source prop is updated (that is, the image url changes without mounting a new component).
* See: https://github.com/billmalarky/react-native-image-cache-hoc/pull/15
*
* @param prevProps {Object} - Previous props.
*/
componentDidUpdate(prevProps) {
var _this$subscription;
// Set urls from source prop data
const url = traverse__default['default'](prevProps).get(['source', 'uri']);
const nextUrl = traverse__default['default'](this.props).get(['source', 'uri']);
const isFile = nextUrl && new URL__default['default'](nextUrl).protocol === 'file:'; // Do nothing if url has not changed.
if (url === nextUrl) return; // Remove component cache lock on old image file, and add cache lock to new image file.
const fileName = this.fileSystem.getFileNameFromUrl(url);
const cacheStrategy = traverse__default['default'](this.props).get(['source', 'cache']) || 'immutable';
FileSystem.unlockCacheFile(fileName, this.componentId);
(_this$subscription = this.subscription) === null || _this$subscription === void 0 ? void 0 : _this$subscription.unsubscribe();
this.invalidUrl = !this._validateImageComponent(); // Init the image cache logic
if (isFile || !this.invalidUrl) {
if (isFile) {
this.onSourceLoaded({
path: nextUrl,
fileName: this.fileSystem.getFileNameFromUrl(nextUrl)
});
} else {
// Add a cache lock to file with this name (prevents concurrent <CacheableImage> components from pruning a file with this name from cache).
const nextFileName = this.fileSystem.getFileNameFromUrl(nextUrl);
FileSystem.lockCacheFile(nextFileName, this.componentId);
this.subscription = this.fileSystem.observable(nextUrl, this.componentId, cacheStrategy, null, this.options.headers).pipe(operators.takeUntil(this.unmounted$.pipe(operators.skip(1)))).subscribe(info => this.onSourceLoaded(info));
}
} else {
this.setState({
source: undefined
});
}
}
componentWillUnmount() {
// Track component mount status to avoid calling setState() on unmounted component.
this.unmounted$.next(true); // Remove component cache lock on associated image file on component teardown.
const fileName = this.fileSystem.getFileNameFromUrl(traverse__default['default'](this.props).get(['source', 'uri']));
FileSystem.unlockCacheFile(fileName, this.componentId);
}
onSourceLoaded({
path,
md5
}) {
this.setState({
source: path ? {
uri: path + (md5 !== undefined ? '?' + md5 : '')
} : undefined
});
this.invalidUrl = path === null;
if (path && this.props.onLoadFinished) {
reactNative.Image.getSize(path, (width, height) => {
if (!this.unmounted$.value && this.props.onLoadFinished) {
this.props.onLoadFinished({
width,
height
});
}
});
}
}
render() {
// If media loaded, render full image component, else render placeholder.
if (this.state.source) {
// Android caches images in memory, if we are rendering the image should have changed locally so appending a timestamp to the path forces it to be loaded from disk
// The internals of te Android behaviour have not been investigated but perhaps it would be beneficial to use the last modified date instead
const props = { ...this.props,
source: this.state.source
};
return /*#__PURE__*/React__default['default'].createElement(Wrapped, _extends__default['default']({
key: this.componentId
}, props));
} else {
if (this.props.placeholder) {
return this.props.placeholder;
} else if (this.options.defaultPlaceholder) {
return this.options.defaultPlaceholder;
} else {
// Extract props proprietary to this HOC before passing props through.
const {
source,
...filteredProps
} = this.props;
return /*#__PURE__*/React__default['default'].createElement(Wrapped, filteredProps);
}
}
}
}, _temp;
};
exports.FileSystem = FileSystem;
exports.FileSystemFactory = FileSystemFactory;
exports.imageCacheHoc = imageCacheHoc;