W3CWebSocket.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. /************************************************************************
  2. * Copyright 2010-2015 Brian McKelvey.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. ***********************************************************************/
  16. var WebSocketClient = require('./WebSocketClient');
  17. var toBuffer = require('typedarray-to-buffer');
  18. var yaeti = require('yaeti');
  19. const CONNECTING = 0;
  20. const OPEN = 1;
  21. const CLOSING = 2;
  22. const CLOSED = 3;
  23. module.exports = W3CWebSocket;
  24. function W3CWebSocket(url, protocols, origin, headers, requestOptions, clientConfig) {
  25. // Make this an EventTarget.
  26. yaeti.EventTarget.call(this);
  27. // Sanitize clientConfig.
  28. clientConfig = clientConfig || {};
  29. clientConfig.assembleFragments = true; // Required in the W3C API.
  30. var self = this;
  31. this._url = url;
  32. this._readyState = CONNECTING;
  33. this._protocol = undefined;
  34. this._extensions = '';
  35. this._bufferedAmount = 0; // Hack, always 0.
  36. this._binaryType = 'arraybuffer'; // TODO: Should be 'blob' by default, but Node has no Blob.
  37. // The WebSocketConnection instance.
  38. this._connection = undefined;
  39. // WebSocketClient instance.
  40. this._client = new WebSocketClient(clientConfig);
  41. this._client.on('connect', function(connection) {
  42. onConnect.call(self, connection);
  43. });
  44. this._client.on('connectFailed', function() {
  45. onConnectFailed.call(self);
  46. });
  47. this._client.connect(url, protocols, origin, headers, requestOptions);
  48. }
  49. // Expose W3C read only attributes.
  50. Object.defineProperties(W3CWebSocket.prototype, {
  51. url: { get: function() { return this._url; } },
  52. readyState: { get: function() { return this._readyState; } },
  53. protocol: { get: function() { return this._protocol; } },
  54. extensions: { get: function() { return this._extensions; } },
  55. bufferedAmount: { get: function() { return this._bufferedAmount; } }
  56. });
  57. // Expose W3C write/read attributes.
  58. Object.defineProperties(W3CWebSocket.prototype, {
  59. binaryType: {
  60. get: function() {
  61. return this._binaryType;
  62. },
  63. set: function(type) {
  64. // TODO: Just 'arraybuffer' supported.
  65. if (type !== 'arraybuffer') {
  66. throw new SyntaxError('just "arraybuffer" type allowed for "binaryType" attribute');
  67. }
  68. this._binaryType = type;
  69. }
  70. }
  71. });
  72. // Expose W3C readyState constants into the WebSocket instance as W3C states.
  73. [['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) {
  74. Object.defineProperty(W3CWebSocket.prototype, property[0], {
  75. get: function() { return property[1]; }
  76. });
  77. });
  78. // Also expose W3C readyState constants into the WebSocket class (not defined by the W3C,
  79. // but there are so many libs relying on them).
  80. [['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) {
  81. Object.defineProperty(W3CWebSocket, property[0], {
  82. get: function() { return property[1]; }
  83. });
  84. });
  85. W3CWebSocket.prototype.send = function(data) {
  86. if (this._readyState !== OPEN) {
  87. throw new Error('cannot call send() while not connected');
  88. }
  89. // Text.
  90. if (typeof data === 'string' || data instanceof String) {
  91. this._connection.sendUTF(data);
  92. }
  93. // Binary.
  94. else {
  95. // Node Buffer.
  96. if (data instanceof Buffer) {
  97. this._connection.sendBytes(data);
  98. }
  99. // If ArrayBuffer or ArrayBufferView convert it to Node Buffer.
  100. else if (data.byteLength || data.byteLength === 0) {
  101. data = toBuffer(data);
  102. this._connection.sendBytes(data);
  103. }
  104. else {
  105. throw new Error('unknown binary data:', data);
  106. }
  107. }
  108. };
  109. W3CWebSocket.prototype.close = function(code, reason) {
  110. switch(this._readyState) {
  111. case CONNECTING:
  112. // NOTE: We don't have the WebSocketConnection instance yet so no
  113. // way to close the TCP connection.
  114. // Artificially invoke the onConnectFailed event.
  115. onConnectFailed.call(this);
  116. // And close if it connects after a while.
  117. this._client.on('connect', function(connection) {
  118. if (code) {
  119. connection.close(code, reason);
  120. } else {
  121. connection.close();
  122. }
  123. });
  124. break;
  125. case OPEN:
  126. this._readyState = CLOSING;
  127. if (code) {
  128. this._connection.close(code, reason);
  129. } else {
  130. this._connection.close();
  131. }
  132. break;
  133. case CLOSING:
  134. case CLOSED:
  135. break;
  136. }
  137. };
  138. /**
  139. * Private API.
  140. */
  141. function createCloseEvent(code, reason) {
  142. var event = new yaeti.Event('close');
  143. event.code = code;
  144. event.reason = reason;
  145. event.wasClean = (typeof code === 'undefined' || code === 1000);
  146. return event;
  147. }
  148. function createMessageEvent(data) {
  149. var event = new yaeti.Event('message');
  150. event.data = data;
  151. return event;
  152. }
  153. function onConnect(connection) {
  154. var self = this;
  155. this._readyState = OPEN;
  156. this._connection = connection;
  157. this._protocol = connection.protocol;
  158. this._extensions = connection.extensions;
  159. this._connection.on('close', function(code, reason) {
  160. onClose.call(self, code, reason);
  161. });
  162. this._connection.on('message', function(msg) {
  163. onMessage.call(self, msg);
  164. });
  165. this.dispatchEvent(new yaeti.Event('open'));
  166. }
  167. function onConnectFailed() {
  168. destroy.call(this);
  169. this._readyState = CLOSED;
  170. try {
  171. this.dispatchEvent(new yaeti.Event('error'));
  172. } finally {
  173. this.dispatchEvent(createCloseEvent(1006, 'connection failed'));
  174. }
  175. }
  176. function onClose(code, reason) {
  177. destroy.call(this);
  178. this._readyState = CLOSED;
  179. this.dispatchEvent(createCloseEvent(code, reason || ''));
  180. }
  181. function onMessage(message) {
  182. if (message.utf8Data) {
  183. this.dispatchEvent(createMessageEvent(message.utf8Data));
  184. }
  185. else if (message.binaryData) {
  186. // Must convert from Node Buffer to ArrayBuffer.
  187. // TODO: or to a Blob (which does not exist in Node!).
  188. if (this.binaryType === 'arraybuffer') {
  189. var buffer = message.binaryData;
  190. var arraybuffer = new ArrayBuffer(buffer.length);
  191. var view = new Uint8Array(arraybuffer);
  192. for (var i=0, len=buffer.length; i<len; ++i) {
  193. view[i] = buffer[i];
  194. }
  195. this.dispatchEvent(createMessageEvent(arraybuffer));
  196. }
  197. }
  198. }
  199. function destroy() {
  200. this._client.removeAllListeners();
  201. if (this._connection) {
  202. this._connection.removeAllListeners();
  203. }
  204. }