Skip to content
This repository was archived by the owner on Apr 15, 2025. It is now read-only.

Commit e3e8ede

Browse files
committed
perf: replace default header for mutable requests (if-modified-since) with if-none-match #2
1 parent b7f2397 commit e3e8ede

File tree

6 files changed

+119
-104
lines changed

6 files changed

+119
-104
lines changed

src/FileSystem.tsx

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
delayWhen,
2626
concatAll,
2727
take,
28+
tap,
2829
} from 'rxjs/operators'
2930
import uuid from 'react-native-uuid'
3031
import { CacheStrategy } from '.'
@@ -33,6 +34,7 @@ import { HeaderFn } from './types'
3334
export interface CacheFileInfo {
3435
path: string | null
3536
fileName: string
37+
md5?: string
3638
}
3739

3840
/**
@@ -270,6 +272,7 @@ export class FileSystem {
270272
FileSystem.cacheObservables[fileName].next({
271273
path: 'file://' + path,
272274
fileName,
275+
md5: await RNFS.hash(path, 'md5').catch(() => undefined),
273276
})
274277
}
275278

@@ -301,26 +304,54 @@ export class FileSystem {
301304
// Logic here prunes cache directory on "cache" writes to ensure cache doesn't get too large.
302305
from(cacheDirExists ? this.pruneCache() : RNFS.mkdir(this.baseFilePath)),
303306
),
304-
mergeMap(() => from(RNFS.stat(path)).pipe(catchError(() => of(null)))),
305-
// Hit network and download file to local disk.
306-
mergeMap((stat) =>
307-
from(
308-
RNFS.downloadFile({
307+
mergeMap(() => from(RNFS.exists(path))),
308+
mergeMap((alreadyExists) =>
309+
// Hit network and download file to local disk.
310+
defer(async () => {
311+
let beginResult: RNFS.DownloadBeginCallbackResult | undefined
312+
313+
const downloadResult = await RNFS.downloadFile({
309314
fromUrl: url,
310315
toFile: path,
311316
headers,
312-
}).promise,
313-
).pipe(
314-
// Only need to emit or throw errors if the file has changed or this is the first download
315-
filter((downloadResult) => stat === null || downloadResult.statusCode === 200),
316-
map((downloadResult) => {
317-
if (stat === null && downloadResult.statusCode !== 200) {
318-
throw new Error('Request failed ' + downloadResult.statusCode)
317+
begin: (res: RNFS.DownloadBeginCallbackResult) => {
318+
beginResult = res
319+
},
320+
}).promise
321+
return Promise.resolve({ downloadResult, beginResult })
322+
}).pipe(
323+
// If "304 Not Modified" response touch local file
324+
tap(({ downloadResult }) => {
325+
if (alreadyExists && downloadResult.statusCode === 304) {
326+
// Unfortunately react-native-fs only shares headers for status >=200 && status < 300
327+
RNFS.touch(path, new Date(), new Date())
319328
}
320-
329+
}),
330+
// Only need to emit if the local file has changed
331+
filter(({ downloadResult }) => downloadResult.statusCode === 200),
332+
mergeMap(({ downloadResult, beginResult }) =>
333+
defer(async () => {
334+
if (beginResult !== undefined && 'ETag' in beginResult.headers) {
335+
return {
336+
md5: beginResult.headers['ETag'],
337+
}
338+
}
339+
return {
340+
md5: await RNFS.hash(path, 'md5').catch(() => undefined),
341+
}
342+
}).pipe(
343+
map((extra) => ({
344+
downloadResult,
345+
beginResult,
346+
extra,
347+
})),
348+
),
349+
),
350+
map(({ extra }) => {
321351
return {
322352
path: 'file://' + path,
323353
fileName: pathLib.basename(path),
354+
md5: extra.md5,
324355
}
325356
}),
326357
),
@@ -454,27 +485,29 @@ export class FileSystem {
454485

455486
const subject$ = new ReplaySubject<CacheFileInfo>()
456487

457-
const obs$ = from(RNFS.stat(this.baseFilePath + fileName)).pipe(
488+
const obs$ = from(RNFS.hash(this.baseFilePath + fileName, 'md5')).pipe(
458489
catchError(() => of(null)),
459-
switchMap((stat) => {
460-
if (stat !== null) {
490+
switchMap((md5) => {
491+
if (md5 !== null) {
461492
switch (cacheStrategy) {
462493
case 'immutable': {
463494
return of({
464495
path: 'file://' + this.baseFilePath + fileName,
465496
fileName,
497+
md5,
466498
} as CacheFileInfo)
467499
}
468500
case 'mutable': {
469501
return from([
470502
of({
471503
path: 'file://' + this.baseFilePath + fileName,
472504
fileName,
505+
md5,
473506
} as CacheFileInfo),
474507
defer(async () => (headers instanceof Function ? await headers() : headers)).pipe(
475508
mergeMap((headers) =>
476509
this.fetchFile(url, fileName, {
477-
'if-modified-since': new Date(stat.mtime).toUTCString(),
510+
'if-none-match': md5,
478511
...headers,
479512
}),
480513
),

src/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import FileSystemFactory, { CacheFileInfo, FileSystem } from './FileSystem'
1919
import traverse from 'traverse'
2020
import validator from 'validator'
2121
import uuid from 'react-native-uuid'
22-
import { Image, ImageStyle, Platform, StyleProp } from 'react-native'
22+
import { Image, ImageStyle, StyleProp } from 'react-native'
2323
import { BehaviorSubject, Subscription } from 'rxjs'
2424
import { skip, takeUntil } from 'rxjs/operators'
2525
import URL from 'url-parse'
@@ -310,11 +310,11 @@ const imageCacheHoc = <P extends object>(
310310
FileSystem.unlockCacheFile(fileName, this.componentId)
311311
}
312312

313-
onSourceLoaded({ path }: CacheFileInfo) {
313+
onSourceLoaded({ path, md5 }: CacheFileInfo) {
314314
this.setState({
315315
source: path
316316
? {
317-
uri: path + (Platform.OS === 'android' ? '?' + Date.now() : ''),
317+
uri: path + (md5 !== undefined ? '?' + md5 : ''),
318318
}
319319
: undefined,
320320
})

tests/CacheableImage.test.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ describe('CacheableImage', function () {
160160
path:
161161
'file:///base/file/path/react-native-image-cache-hoc/90c1be491d18ff2a7280039e9b65749461a65403.png',
162162
fileName: '90c1be491d18ff2a7280039e9b65749461a65403.png',
163+
md5: '#md5#',
163164
})
164165
done()
165166
},
@@ -301,15 +302,15 @@ describe('CacheableImage', function () {
301302
setImmediate(() => {
302303
expect(wrapper.prop('source')).toStrictEqual({
303304
uri:
304-
'file:///base/file/path/react-native-image-cache-hoc/d3b74e9fa8248a5805e2dcf17a8577acd28c089b.png',
305+
'file:///base/file/path/react-native-image-cache-hoc/d3b74e9fa8248a5805e2dcf17a8577acd28c089b.png?#md5#',
305306
})
306307

307308
wrapper.setProps({ source: { uri: 'https://example.com/B.jpg' } })
308309

309310
setImmediate(() => {
310311
expect(wrapper.prop('source')).toStrictEqual({
311312
uri:
312-
'file:///base/file/path/react-native-image-cache-hoc/a940ee9ea388fcea7628d9a64dfac6a698aa0228.jpg',
313+
'file:///base/file/path/react-native-image-cache-hoc/a940ee9ea388fcea7628d9a64dfac6a698aa0228.jpg?#md5#',
313314
})
314315

315316
done()
@@ -325,7 +326,7 @@ describe('CacheableImage', function () {
325326
setImmediate(() => {
326327
expect(wrapper.prop('source')).toStrictEqual({
327328
uri:
328-
'file:///base/file/path/react-native-image-cache-hoc/d3b74e9fa8248a5805e2dcf17a8577acd28c089b.png',
329+
'file:///base/file/path/react-native-image-cache-hoc/d3b74e9fa8248a5805e2dcf17a8577acd28c089b.png?#md5#',
329330
})
330331

331332
wrapper.setState({

0 commit comments

Comments
 (0)