-
Notifications
You must be signed in to change notification settings - Fork 24.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Blob support for XMLHttpRequest #11573
Changes from 72 commits
642d451
be07fa9
e8ea388
553f20e
aaba705
22ebafb
781e9e6
109a465
2746628
a60605d
d4b62b9
600217a
b1614c1
8ad6369
968a7b8
60f8ebb
f56c66b
7c0d8b2
d62da30
39feed9
1a6060a
8880eb9
9930649
8b6fd3f
d0efa54
c2fa7ff
5c2dd2c
5fc2acb
e644c21
9798801
8564ec8
71b69ed
1479086
d9d3295
ccf580e
6159d5b
0dbe167
cc9a425
ed2b250
f928990
35a37b6
adc5318
e70fb7e
37a0269
1aa1f84
8e98f95
45d014a
21ff09b
cd6ede7
b536989
75b4f1e
d3a36ae
03165ca
3a28ad1
a977635
ebe159e
1de53b1
ec88828
fb3496a
c243471
368b86e
20cdd7e
84953bf
ddcbc64
4c7a928
da15540
28d1500
00408ab
236ed3f
35e0c1c
e2481c4
5d215ce
5494fe9
ad568cf
2f15886
d56239b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,19 +8,12 @@ | |
* | ||
* @providesModule Blob | ||
* @flow | ||
* @format | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const invariant = require('fbjs/lib/invariant'); | ||
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error | ||
* found when Flow v0.54 was deployed. To see the error delete this comment and | ||
* run Flow. */ | ||
const uuid = require('uuid'); | ||
|
||
const { BlobModule } = require('NativeModules'); | ||
|
||
import type { BlobProps } from 'BlobTypes'; | ||
import type {BlobData, BlobOptions} from 'BlobTypes'; | ||
|
||
/** | ||
* Opaque JS representation of some binary data in native. | ||
|
@@ -60,61 +53,39 @@ import type { BlobProps } from 'BlobTypes'; | |
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob | ||
*/ | ||
class Blob { | ||
/** | ||
* Size of the data contained in the Blob object, in bytes. | ||
*/ | ||
size: number; | ||
/* | ||
* String indicating the MIME type of the data contained in the Blob. | ||
* If the type is unknown, this string is empty. | ||
*/ | ||
type: string; | ||
|
||
/* | ||
* Unique id to identify the blob on native side (non-standard) | ||
*/ | ||
blobId: string; | ||
/* | ||
* Offset to indicate part of blob, used when sliced (non-standard) | ||
*/ | ||
offset: number; | ||
|
||
/** | ||
* Construct blob instance from blob data from native. | ||
* Used internally by modules like XHR, WebSocket, etc. | ||
*/ | ||
static create(props: BlobProps): Blob { | ||
return Object.assign(Object.create(Blob.prototype), props); | ||
} | ||
_data: ?BlobData; | ||
|
||
/** | ||
* Constructor for JS consumers. | ||
* Currently we only support creating Blobs from other Blobs. | ||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob | ||
*/ | ||
constructor(parts: Array<Blob>, options: any) { | ||
const blobId = uuid(); | ||
let size = 0; | ||
parts.forEach((part) => { | ||
invariant(part instanceof Blob, 'Can currently only create a Blob from other Blobs'); | ||
size += part.size; | ||
}); | ||
BlobModule.createFromParts(parts, blobId); | ||
return Blob.create({ | ||
blobId, | ||
offset: 0, | ||
size, | ||
}); | ||
constructor(parts: Array<Blob | string> = [], options?: BlobOptions) { | ||
const BlobManager = require('BlobManager'); | ||
this.data = BlobManager.createFromParts(parts, options).data; | ||
} | ||
|
||
/* | ||
* This method is used to create a new Blob object containing | ||
* the data in the specified range of bytes of the source Blob. | ||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice | ||
*/ | ||
set data(data: ?BlobData) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any reason for making Also, these methods cause the docblock for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO doesn't make sense to expose both |
||
this._data = data; | ||
} | ||
|
||
get data(): BlobData { | ||
if (!this._data) { | ||
throw new Error('Blob has been closed and is no longer available'); | ||
} | ||
|
||
return this._data; | ||
} | ||
|
||
slice(start?: number, end?: number): Blob { | ||
let offset = this.offset; | ||
let size = this.size; | ||
const BlobManager = require('BlobManager'); | ||
let {offset, size} = this.data; | ||
|
||
if (typeof start === 'number') { | ||
if (start > size) { | ||
start = size; | ||
|
@@ -129,8 +100,8 @@ class Blob { | |
size = end - start; | ||
} | ||
} | ||
return Blob.create({ | ||
blobId: this.blobId, | ||
return BlobManager.createFromOptions({ | ||
blobId: this.data.blobId, | ||
offset, | ||
size, | ||
}); | ||
|
@@ -149,7 +120,24 @@ class Blob { | |
* `new Blob([blob, ...])` actually copies the data in memory. | ||
*/ | ||
close() { | ||
BlobModule.release(this.blobId); | ||
const BlobManager = require('BlobManager'); | ||
BlobManager.release(this.data.blobId); | ||
this.data = null; | ||
} | ||
|
||
/** | ||
* Size of the data contained in the Blob object, in bytes. | ||
*/ | ||
get size(): number { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we remove the ability to mutate a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are read-only properties according to the |
||
return this.data.size; | ||
} | ||
|
||
/* | ||
* String indicating the MIME type of the data contained in the Blob. | ||
* If the type is unknown, this string is empty. | ||
*/ | ||
get type(): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto. |
||
return this.data.type || ''; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/** | ||
* Copyright (c) 2013-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
* | ||
* @providesModule BlobManager | ||
* @flow | ||
* @format | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const uuid = require('uuid'); | ||
const Blob = require('Blob'); | ||
const BlobRegistry = require('BlobRegistry'); | ||
const {BlobModule} = require('NativeModules'); | ||
|
||
import type {BlobData, BlobOptions} from 'BlobTypes'; | ||
|
||
/** | ||
* Module to manage blobs. Wrapper around the native blob module. | ||
*/ | ||
class BlobManager { | ||
/** | ||
* If the native blob module is available. | ||
*/ | ||
static isAvailable = !!BlobModule; | ||
|
||
/** | ||
* Create blob from existing array of blobs. | ||
*/ | ||
static createFromParts( | ||
parts: Array<Blob | string>, | ||
options?: BlobOptions, | ||
): Blob { | ||
const blobId = uuid.v4(); | ||
const items = parts.map(part => { | ||
if ( | ||
part instanceof ArrayBuffer || | ||
(global.ArrayBufferView && part instanceof global.ArrayBufferView) | ||
) { | ||
throw new Error( | ||
"Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported", | ||
); | ||
} | ||
if (part instanceof Blob) { | ||
return { | ||
data: part.data, | ||
type: 'blob', | ||
}; | ||
} else { | ||
return { | ||
data: String(part), | ||
type: 'string', | ||
}; | ||
} | ||
}); | ||
const size = items.reduce((acc, curr) => { | ||
if (curr.type === 'string') { | ||
return acc + global.unescape(encodeURI(curr.data)).length; | ||
} else { | ||
return acc + curr.data.size; | ||
} | ||
}, 0); | ||
|
||
BlobModule.createFromParts(items, blobId); | ||
|
||
return BlobManager.createFromOptions({ | ||
blobId, | ||
offset: 0, | ||
size, | ||
type: options ? options.type : '', | ||
lastModified: options ? options.lastModified : Date.now(), | ||
}); | ||
} | ||
|
||
/** | ||
* Create blob instance from blob data from native. | ||
* Used internally by modules like XHR, WebSocket, etc. | ||
*/ | ||
static createFromOptions(options: BlobData): Blob { | ||
BlobRegistry.register(options.blobId); | ||
return Object.assign(Object.create(Blob.prototype), {data: options}); | ||
} | ||
|
||
/** | ||
* Deallocate resources for a blob. | ||
*/ | ||
static release(blobId: string): void { | ||
BlobRegistry.unregister(blobId); | ||
if (BlobRegistry.has(blobId)) { | ||
return; | ||
} | ||
BlobModule.release(blobId); | ||
} | ||
|
||
/** | ||
* Inject the blob content handler in the networking module to support blob | ||
* requests and responses. | ||
*/ | ||
static addNetworkingHandler(): void { | ||
BlobModule.addNetworkingHandler(); | ||
} | ||
|
||
/** | ||
* Indicate the websocket should return a blob for incoming binary | ||
* messages. | ||
*/ | ||
static addWebSocketHandler(socketId: number): void { | ||
BlobModule.addWebSocketHandler(socketId); | ||
} | ||
|
||
/** | ||
* Indicate the websocket should no longer return a blob for incoming | ||
* binary messages. | ||
*/ | ||
static removeWebSocketHandler(socketId: number): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you consider making There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had a look at this and I think the current api makes sense because of how it is used in Websocket.js. It has the same type of API as WebSocketModule which has similar methods. |
||
BlobModule.removeWebSocketHandler(socketId); | ||
} | ||
|
||
/** | ||
* Send a blob message to a websocket. | ||
*/ | ||
static sendOverSocket(blob: Blob, socketId: number): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto. Did you consider making |
||
BlobModule.sendOverSocket(blob.data, socketId); | ||
} | ||
} | ||
|
||
module.exports = BlobManager; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/** | ||
* Copyright (c) 2013-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
* | ||
* @providesModule BlobRegistry | ||
* @flow | ||
* @format | ||
*/ | ||
|
||
const registry: {[key: string]: number} = {}; | ||
|
||
const register = (id: string) => { | ||
if (registry[id]) { | ||
registry[id]++; | ||
} else { | ||
registry[id] = 1; | ||
} | ||
}; | ||
|
||
const unregister = (id: string) => { | ||
if (registry[id]) { | ||
registry[id]--; | ||
if (registry[id] <= 0) { | ||
delete registry[id]; | ||
} | ||
} | ||
}; | ||
|
||
const has = (id: string) => { | ||
return registry[id] && registry[id] > 0; | ||
}; | ||
|
||
module.exports = { | ||
register, | ||
unregister, | ||
has, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,18 +8,21 @@ | |
* | ||
* @providesModule BlobTypes | ||
* @flow | ||
* @format | ||
*/ | ||
|
||
'use strict'; | ||
|
||
export type BlobProps = { | ||
export type BlobData = { | ||
blobId: string, | ||
offset: number, | ||
size: number, | ||
name?: string, | ||
type?: string, | ||
lastModified?: number, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Kureev |
||
}; | ||
|
||
export type FileProps = BlobProps & { | ||
name: string, | ||
export type BlobOptions = { | ||
type: string, | ||
lastModified: number, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/** | ||
* Copyright (c) 2013-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
* | ||
* @providesModule File | ||
* @flow | ||
* @format | ||
*/ | ||
'use strict'; | ||
|
||
const Blob = require('Blob'); | ||
|
||
const invariant = require('fbjs/lib/invariant'); | ||
|
||
import type {BlobOptions} from 'BlobTypes'; | ||
|
||
/** | ||
* The File interface provides information about files. | ||
*/ | ||
class File extends Blob { | ||
/** | ||
* Constructor for JS consumers. | ||
*/ | ||
constructor( | ||
parts: Array<Blob | string>, | ||
name: string, | ||
options?: BlobOptions, | ||
) { | ||
invariant( | ||
parts != null && name != null, | ||
'Failed to construct `File`: Must pass both `parts` and `name` arguments.', | ||
); | ||
|
||
super(parts, options); | ||
this.data.name = name; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mutating |
||
} | ||
|
||
/** | ||
* Name of the file. | ||
*/ | ||
get name(): string { | ||
invariant(this.data.name != null, 'Files must have a name set.'); | ||
return this.data.name; | ||
} | ||
|
||
/* | ||
* Last modified time of the file. | ||
*/ | ||
get lastModified(): number { | ||
return this.data.lastModified || 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Kureev native side needs to pass this info when it's possible, but we can probably set it to current time from native if we cannot get the time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on what I see as an outcome of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto, this should just be an invariant that |
||
} | ||
} | ||
|
||
module.exports = File; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To follow conventions of the repository (especially since initializing
BlobManager
does not have side effects), can you hoist these inlined require statements into the module scope?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not possible due to circular dependency between the
Blob
andBlobManager
modules