diff --git a/packages/react-native/Libraries/Blob/FileReader.js b/packages/react-native/Libraries/Blob/FileReader.js index 3745c00923a8..df2df9e2e75a 100644 --- a/packages/react-native/Libraries/Blob/FileReader.js +++ b/packages/react-native/Libraries/Blob/FileReader.js @@ -44,6 +44,7 @@ class FileReader extends EventTarget { _error: ?Error; _result: ?ReaderResult; _aborted: boolean = false; + _readId: number = 0; constructor() { super(); @@ -56,6 +57,15 @@ class FileReader extends EventTarget { this._result = null; } + _startRead(): number { + this._aborted = false; + this._error = null; + this._result = null; + const readId = ++this._readId; + this._setReadyState(LOADING); + return readId; + } + _setReadyState(newState: ReadyState) { this._readyState = newState; this.dispatchEvent(new Event('readystatechange')); @@ -67,24 +77,24 @@ class FileReader extends EventTarget { } else { this.dispatchEvent(new Event('load')); } - this.dispatchEvent(new Event('loadend')); + if (this._readyState !== LOADING) { + this.dispatchEvent(new Event('loadend')); + } } } readAsArrayBuffer(blob: ?Blob): void { - this._aborted = false; - if (blob == null) { throw new TypeError( "Failed to execute 'readAsArrayBuffer' on 'FileReader': parameter 1 is not of type 'Blob'", ); } - this._setReadyState(LOADING); + const readId = this._startRead(); NativeFileReaderModule.readAsDataURL(blob.data).then( (text: string) => { - if (this._aborted) { + if (readId !== this._readId) { return; } @@ -95,7 +105,7 @@ class FileReader extends EventTarget { this._setReadyState(DONE); }, error => { - if (this._aborted) { + if (readId !== this._readId) { return; } this._error = error; @@ -105,26 +115,24 @@ class FileReader extends EventTarget { } readAsDataURL(blob: ?Blob): void { - this._aborted = false; - if (blob == null) { throw new TypeError( "Failed to execute 'readAsDataURL' on 'FileReader': parameter 1 is not of type 'Blob'", ); } - this._setReadyState(LOADING); + const readId = this._startRead(); NativeFileReaderModule.readAsDataURL(blob.data).then( (text: string) => { - if (this._aborted) { + if (readId !== this._readId) { return; } this._result = text; this._setReadyState(DONE); }, error => { - if (this._aborted) { + if (readId !== this._readId) { return; } this._error = error; @@ -134,26 +142,24 @@ class FileReader extends EventTarget { } readAsText(blob: ?Blob, encoding: string = 'UTF-8'): void { - this._aborted = false; - if (blob == null) { throw new TypeError( "Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'", ); } - this._setReadyState(LOADING); + const readId = this._startRead(); NativeFileReaderModule.readAsText(blob.data, encoding).then( (text: string) => { - if (this._aborted) { + if (readId !== this._readId) { return; } this._result = text; this._setReadyState(DONE); }, error => { - if (this._aborted) { + if (readId !== this._readId) { return; } this._error = error; @@ -163,14 +169,12 @@ class FileReader extends EventTarget { } abort() { - this._aborted = true; - // only call onreadystatechange if there is something to abort, as per spec - if (this._readyState !== EMPTY && this._readyState !== DONE) { - this._reset(); + this._result = null; + if (this._readyState === LOADING) { + this._aborted = true; + this._readId++; this._setReadyState(DONE); } - // Reset again after, in case modified in handler - this._reset(); } get readyState(): ReadyState { diff --git a/packages/react-native/Libraries/Blob/__tests__/FileReader-test.js b/packages/react-native/Libraries/Blob/__tests__/FileReader-test.js index 36d55849c487..da61fa1a6d55 100644 --- a/packages/react-native/Libraries/Blob/__tests__/FileReader-test.js +++ b/packages/react-native/Libraries/Blob/__tests__/FileReader-test.js @@ -14,6 +14,7 @@ import type Event from '../../../src/private/webapis/dom/events/Event'; const Blob = require('../Blob').default; const FileReader = require('../FileReader').default; +const NativeFileReaderModule = require('../NativeFileReaderModule').default; jest.mock('../../BatchedBridge/NativeModules', () => ({ __esModule: true, @@ -69,6 +70,63 @@ describe('FileReader', function () { reader.abort(); expect(aborted).toBe(true); expect(loadended).toBe(true); + expect(reader.readyState).toBe(FileReader.DONE); + expect(reader.result).toBe(null); + }); + + it('should preserve a read started by an abort handler', async () => { + const reader = new FileReader(); + let loadendCount = 0; + const replacementRead = new Promise(resolve => { + reader.onloadend = () => { + loadendCount++; + resolve(); + }; + }); + reader.onabort = () => { + reader.readAsText(new Blob()); + }; + + reader.readAsText(new Blob()); + reader.abort(); + + expect(reader.readyState).toBe(FileReader.LOADING); + expect(loadendCount).toBe(0); + + await replacementRead; + expect(reader.readyState).toBe(FileReader.DONE); + expect(reader.result).toBe(''); + expect(loadendCount).toBe(1); + }); + + it('should clear stale result and error when starting a read', async () => { + const reader = new FileReader(); + const readAsText = jest.spyOn(NativeFileReaderModule, 'readAsText'); + + const successfulRead = new Promise(resolve => { + reader.onloadend = () => resolve(); + }); + reader.readAsText(new Blob()); + await successfulRead; + expect(reader.result).toBe(''); + + const error = new Error('read failed'); + readAsText.mockRejectedValueOnce(error); + const failedRead = new Promise(resolve => { + reader.onloadend = () => resolve(); + }); + reader.readAsText(new Blob()); + expect(reader.result).toBe(null); + expect(reader.error).toBe(null); + await failedRead; + expect(reader.error).toBe(error); + + readAsText.mockReturnValueOnce(new Promise(() => {})); + reader.readAsText(new Blob()); + expect(reader.result).toBe(null); + expect(reader.error).toBe(null); + + readAsText.mockRestore(); }); it('should read blob as ArrayBuffer', async () => {