WebSocket.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @format
  8. * @flow
  9. */
  10. 'use strict';
  11. const Blob = require('../Blob/Blob');
  12. const BlobManager = require('../Blob/BlobManager');
  13. const EventTarget = require('event-target-shim');
  14. const NativeEventEmitter = require('../EventEmitter/NativeEventEmitter');
  15. const WebSocketEvent = require('./WebSocketEvent');
  16. const base64 = require('base64-js');
  17. const binaryToBase64 = require('../Utilities/binaryToBase64');
  18. const invariant = require('invariant');
  19. import type EventSubscription from '../vendor/emitter/EventSubscription';
  20. import NativeWebSocketModule from './NativeWebSocketModule';
  21. type ArrayBufferView =
  22. | Int8Array
  23. | Uint8Array
  24. | Uint8ClampedArray
  25. | Int16Array
  26. | Uint16Array
  27. | Int32Array
  28. | Uint32Array
  29. | Float32Array
  30. | Float64Array
  31. | DataView;
  32. type BinaryType = 'blob' | 'arraybuffer';
  33. const CONNECTING = 0;
  34. const OPEN = 1;
  35. const CLOSING = 2;
  36. const CLOSED = 3;
  37. const CLOSE_NORMAL = 1000;
  38. const WEBSOCKET_EVENTS = ['close', 'error', 'message', 'open'];
  39. let nextWebSocketId = 0;
  40. /**
  41. * Browser-compatible WebSockets implementation.
  42. *
  43. * See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
  44. * See https://github.com/websockets/ws
  45. */
  46. class WebSocket extends (EventTarget(...WEBSOCKET_EVENTS): any) {
  47. static CONNECTING: number = CONNECTING;
  48. static OPEN: number = OPEN;
  49. static CLOSING: number = CLOSING;
  50. static CLOSED: number = CLOSED;
  51. CONNECTING: number = CONNECTING;
  52. OPEN: number = OPEN;
  53. CLOSING: number = CLOSING;
  54. CLOSED: number = CLOSED;
  55. _socketId: number;
  56. _eventEmitter: NativeEventEmitter;
  57. _subscriptions: Array<EventSubscription>;
  58. _binaryType: ?BinaryType;
  59. onclose: ?Function;
  60. onerror: ?Function;
  61. onmessage: ?Function;
  62. onopen: ?Function;
  63. bufferedAmount: number;
  64. extension: ?string;
  65. protocol: ?string;
  66. readyState: number = CONNECTING;
  67. url: ?string;
  68. constructor(
  69. url: string,
  70. protocols: ?string | ?Array<string>,
  71. options: ?{headers?: {origin?: string, ...}, ...},
  72. ) {
  73. super();
  74. if (typeof protocols === 'string') {
  75. protocols = [protocols];
  76. }
  77. const {headers = {}, ...unrecognized} = options || {};
  78. // Preserve deprecated backwards compatibility for the 'origin' option
  79. /* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
  80. * error found when Flow v0.68 was deployed. To see the error delete this
  81. * comment and run Flow. */
  82. if (unrecognized && typeof unrecognized.origin === 'string') {
  83. console.warn(
  84. 'Specifying `origin` as a WebSocket connection option is deprecated. Include it under `headers` instead.',
  85. );
  86. /* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This
  87. * comment suppresses an error found when Flow v0.54 was deployed. To see
  88. * the error delete this comment and run Flow. */
  89. headers.origin = unrecognized.origin;
  90. /* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This
  91. * comment suppresses an error found when Flow v0.54 was deployed. To see
  92. * the error delete this comment and run Flow. */
  93. delete unrecognized.origin;
  94. }
  95. // Warn about and discard anything else
  96. if (Object.keys(unrecognized).length > 0) {
  97. console.warn(
  98. 'Unrecognized WebSocket connection option(s) `' +
  99. Object.keys(unrecognized).join('`, `') +
  100. '`. ' +
  101. 'Did you mean to put these under `headers`?',
  102. );
  103. }
  104. if (!Array.isArray(protocols)) {
  105. protocols = null;
  106. }
  107. this._eventEmitter = new NativeEventEmitter(NativeWebSocketModule);
  108. this._socketId = nextWebSocketId++;
  109. this._registerEvents();
  110. NativeWebSocketModule.connect(url, protocols, {headers}, this._socketId);
  111. }
  112. get binaryType(): ?BinaryType {
  113. return this._binaryType;
  114. }
  115. set binaryType(binaryType: BinaryType): void {
  116. if (binaryType !== 'blob' && binaryType !== 'arraybuffer') {
  117. throw new Error("binaryType must be either 'blob' or 'arraybuffer'");
  118. }
  119. if (this._binaryType === 'blob' || binaryType === 'blob') {
  120. invariant(
  121. BlobManager.isAvailable,
  122. 'Native module BlobModule is required for blob support',
  123. );
  124. if (binaryType === 'blob') {
  125. BlobManager.addWebSocketHandler(this._socketId);
  126. } else {
  127. BlobManager.removeWebSocketHandler(this._socketId);
  128. }
  129. }
  130. this._binaryType = binaryType;
  131. }
  132. close(code?: number, reason?: string): void {
  133. if (this.readyState === this.CLOSING || this.readyState === this.CLOSED) {
  134. return;
  135. }
  136. this.readyState = this.CLOSING;
  137. this._close(code, reason);
  138. }
  139. send(data: string | ArrayBuffer | ArrayBufferView | Blob): void {
  140. if (this.readyState === this.CONNECTING) {
  141. throw new Error('INVALID_STATE_ERR');
  142. }
  143. if (data instanceof Blob) {
  144. invariant(
  145. BlobManager.isAvailable,
  146. 'Native module BlobModule is required for blob support',
  147. );
  148. BlobManager.sendOverSocket(data, this._socketId);
  149. return;
  150. }
  151. if (typeof data === 'string') {
  152. NativeWebSocketModule.send(data, this._socketId);
  153. return;
  154. }
  155. if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
  156. NativeWebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
  157. return;
  158. }
  159. throw new Error('Unsupported data type');
  160. }
  161. ping(): void {
  162. if (this.readyState === this.CONNECTING) {
  163. throw new Error('INVALID_STATE_ERR');
  164. }
  165. NativeWebSocketModule.ping(this._socketId);
  166. }
  167. _close(code?: number, reason?: string): void {
  168. // See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
  169. const statusCode = typeof code === 'number' ? code : CLOSE_NORMAL;
  170. const closeReason = typeof reason === 'string' ? reason : '';
  171. NativeWebSocketModule.close(statusCode, closeReason, this._socketId);
  172. if (BlobManager.isAvailable && this._binaryType === 'blob') {
  173. BlobManager.removeWebSocketHandler(this._socketId);
  174. }
  175. }
  176. _unregisterEvents(): void {
  177. this._subscriptions.forEach(e => e.remove());
  178. this._subscriptions = [];
  179. }
  180. _registerEvents(): void {
  181. this._subscriptions = [
  182. this._eventEmitter.addListener('websocketMessage', ev => {
  183. if (ev.id !== this._socketId) {
  184. return;
  185. }
  186. let data = ev.data;
  187. switch (ev.type) {
  188. case 'binary':
  189. data = base64.toByteArray(ev.data).buffer;
  190. break;
  191. case 'blob':
  192. data = BlobManager.createFromOptions(ev.data);
  193. break;
  194. }
  195. this.dispatchEvent(new WebSocketEvent('message', {data}));
  196. }),
  197. this._eventEmitter.addListener('websocketOpen', ev => {
  198. if (ev.id !== this._socketId) {
  199. return;
  200. }
  201. this.readyState = this.OPEN;
  202. this.protocol = ev.protocol;
  203. this.dispatchEvent(new WebSocketEvent('open'));
  204. }),
  205. this._eventEmitter.addListener('websocketClosed', ev => {
  206. if (ev.id !== this._socketId) {
  207. return;
  208. }
  209. this.readyState = this.CLOSED;
  210. this.dispatchEvent(
  211. new WebSocketEvent('close', {
  212. code: ev.code,
  213. reason: ev.reason,
  214. }),
  215. );
  216. this._unregisterEvents();
  217. this.close();
  218. }),
  219. this._eventEmitter.addListener('websocketFailed', ev => {
  220. if (ev.id !== this._socketId) {
  221. return;
  222. }
  223. this.readyState = this.CLOSED;
  224. this.dispatchEvent(
  225. new WebSocketEvent('error', {
  226. message: ev.message,
  227. }),
  228. );
  229. this.dispatchEvent(
  230. new WebSocketEvent('close', {
  231. message: ev.message,
  232. }),
  233. );
  234. this._unregisterEvents();
  235. this.close();
  236. }),
  237. ];
  238. }
  239. }
  240. module.exports = WebSocket;