Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/react-native/Libraries/Blob/FileReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class FileReader extends EventTarget {
_error: ?Error;
_result: ?ReaderResult;
_aborted: boolean = false;
// Keep the Blob strongly referenced until the native read settles. If the
// caller drops its own reference the Blob can be GC'd mid-read. Its
// BlobCollector finalizer then frees the native buffer and the read fails
// with "The specified blob is invalid".
_blob: ?Blob;

constructor() {
super();
Expand All @@ -54,6 +59,14 @@ class FileReader extends EventTarget {
this._readyState = EMPTY;
this._error = null;
this._result = null;
this._blob = null;
}

_releaseBlob(blob: Blob): void {
// A stale read settling after abort() must not drop a newer read's blob.
if (this._blob === blob) {
this._blob = null;
}
}

_setReadyState(newState: ReadyState) {
Expand All @@ -80,8 +93,11 @@ class FileReader extends EventTarget {
);
}

this._blob = blob;

NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
this._releaseBlob(blob);
if (this._aborted) {
return;
}
Expand All @@ -93,6 +109,7 @@ class FileReader extends EventTarget {
this._setReadyState(DONE);
},
error => {
this._releaseBlob(blob);
if (this._aborted) {
return;
}
Expand All @@ -111,15 +128,19 @@ class FileReader extends EventTarget {
);
}

this._blob = blob;

NativeFileReaderModule.readAsDataURL(blob.data).then(
(text: string) => {
this._releaseBlob(blob);
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
this._releaseBlob(blob);
if (this._aborted) {
return;
}
Expand All @@ -138,15 +159,19 @@ class FileReader extends EventTarget {
);
}

this._blob = blob;

NativeFileReaderModule.readAsText(blob.data, encoding).then(
(text: string) => {
this._releaseBlob(blob);
if (this._aborted) {
return;
}
this._result = text;
this._setReadyState(DONE);
},
error => {
this._releaseBlob(blob);
if (this._aborted) {
return;
}
Expand Down
105 changes: 105 additions & 0 deletions packages/react-native/Libraries/Blob/__tests__/FileReader-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import type Event from '../../../src/private/webapis/dom/events/Event';

const FileReaderModuleMock = require('../__mocks__/FileReaderModule').default;
const Blob = require('../Blob').default;
const FileReader = require('../FileReader').default;

Expand Down Expand Up @@ -59,4 +60,108 @@ describe('FileReader', function () {
// $FlowFixMe[cannot-resolve-name]
expect(new TextDecoder().decode(ab)).toBe('42');
});

it('should retain the blob until the read resolves', async () => {
let resolveRead: string => void = () => {};
const spy = jest
.spyOn(FileReaderModuleMock, 'readAsText')
.mockImplementation(
() =>
new Promise(resolve => {
resolveRead = resolve;
}),
);

const reader = new FileReader();
const blob = new Blob();
const loadend = new Promise<Event>(resolve => {
reader.onloadend = resolve;
});
reader.readAsText(blob);
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(blob);

resolveRead('');
await loadend;
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(null);

spy.mockRestore();
});

it('should release the blob when the read rejects', async () => {
let rejectRead: Error => void = () => {};
const spy = jest
.spyOn(FileReaderModuleMock, 'readAsText')
.mockImplementation(
() =>
new Promise((resolve, reject) => {
rejectRead = reject;
}),
);

const reader = new FileReader();
const blob = new Blob();
const loadend = new Promise<Event>(resolve => {
reader.onloadend = resolve;
});
reader.readAsText(blob);
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(blob);

rejectRead(new Error('nope'));
await loadend;
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(null);

spy.mockRestore();
});

it('should release the blob when a pending read is aborted', () => {
const spy = jest
.spyOn(FileReaderModuleMock, 'readAsText')
.mockImplementation(() => new Promise(() => {}));

const reader = new FileReader();
const blob = new Blob();
reader.readAsText(blob);
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(blob);

reader.abort();
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(null);

spy.mockRestore();
});

it('should keep retaining the new blob when a stale read settles after abort', async () => {
const resolvers: Array<(string) => void> = [];
const spy = jest
.spyOn(FileReaderModuleMock, 'readAsText')
.mockImplementation(
() =>
new Promise(resolve => {
resolvers.push(resolve);
}),
);

const reader = new FileReader();
const staleBlob = new Blob();
reader.readAsText(staleBlob);
reader.abort();

const newBlob = new Blob();
reader.readAsText(newBlob);
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(newBlob);

// Settle the first (aborted) read; it must not drop the new blob.
resolvers[0]('');
await Promise.resolve();
// $FlowFixMe[prop-missing] - accessing private state for the test
expect(reader._blob).toBe(newBlob);

spy.mockRestore();
});
});
Loading