Repository: websockets/ws
Pull request #930 (2016-12-09 10:16:28)

Refactor `WebSocket` to use class syntax
- Refactor `WebSocket` to use the ES6 class syntax.
- Move `cleanupWebsocketResources` and `establishConnection` to `WebSocket.prototype`.
- Rename `establishConnection` to `setSocket` and `cleanupWebsocketResources` to `finalize`.
- Keep `initAsServerClient` and `initAsClient` as completely private API.

lpinca: I think the new methods names (`setSocket` and `finalize`) are better than the original ones but I'm not very happy with them. If you have better ideas please share!
lpinca: @ericmdantas thank you for reviewing, this is greatly appreciated! Let me know if you have more suggestions. 
ericmdantas: @lpinca, no problem. I'm a bit busy at the moment, but I'll take a closer look when possible. But, from what I've seen, it's good to go.

Patch:
diff --git a/.npmignore b/.npmignore
index e0e4dee6..7c56d8c0 100644
--- a/.npmignore
+++ b/.npmignore
@@ -3,4 +3,5 @@ examples/
 bench/
 test/
 doc/
+appveyor.yml
 .*
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..a145cd1d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 654c1160..53a1a252 100644
--- a/README.md
+++ b/README.md
@@ -203,12 +203,6 @@ Note that the usage together with Express 3.0 is quite different from Express
 
 Otherwise, see the test cases.
 
-### Running the tests
-
-```
-make test
-```
-
 ## API Docs
 
 See [`/doc/ws.md`](https://github.com/websockets/ws/blob/master/doc/ws.md) for Node.js-like docs for the ws classes.
@@ -219,27 +213,6 @@ We're using the GitHub [`releases`](https://github.com/websockets/ws/releases) f
 
 ## License
 
-(The MIT License)
-
-Copyright (c) 2011 Einar Otto Stangvik &lt;einaros@gmail.com&gt;
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-'Software'), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+[MIT](LICENSE)
 
 [archive]: http://web.archive.org/web/20130314230536/http://hobbycoding.posterous.com/the-fastest-websocket-module-for-nodejs
diff --git a/doc/ws.md b/doc/ws.md
index c8b6a748..f2503562 100644
--- a/doc/ws.md
+++ b/doc/ws.md
@@ -2,242 +2,407 @@
 
 ## Class: WebSocket.Server
 
-This class is a WebSocket server. It is an `EventEmitter`.
+This class represents a WebSocket server. It extends the `EventEmitter`.
+
+### new WebSocket.Server(options[, callback])
+
+- `options` {Object}
+  - `host` {String} The hostname where to bind the server.
+  - `port` {Number} The port where to bind the server.
+  - `backlog` {Number} The maximum length of the queue of pending connections.
+  - `server` {http.Server|https.Server} A pre-created Node.js HTTP server.
+  - `verifyClient` {Function} A function which can be used to validate incoming
+    connections. See description below.
+  - `handleProtocols` {Function} A function which can be used to handle the
+    WebSocket subprotocols. See description below.
+  - `path` {String} Accept only connections matching this path.
+  - `noServer` {Boolean} Enable no server mode.
+  - `clientTracking` {Boolean} Specifies whether or not to track clients.
+  - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate.
+  - `maxPayload` {Number} The maximum allowed message size in bytes.
+- `callback` {Function}
+
+Create a new server instance. One of `port`, `server` or `noServer` must be
+provided or an error is thrown.
+
+
+If `verifyClient` is not set then the handshake is automatically accepted. If
+it is is provided with a single argument then that is:
+
+- `info` {Object}
+  - `origin` {String} The value in the Origin header indicated by the client.
+  - `req` {http.IncomingMessage} The client HTTP GET request.
+  - `secure` {Boolean} `true` if `req.connection.authorized` or
+    `req.connection.encrypted` is set.
+
+The return value (Boolean) of the function determines whether or not to accept
+the handshake.
+
+if `verifyClient` is provided with two arguments then those are:
+
+- `info` {Object} Same as above.
+- `cb` {Function} A callback that must be called by the user upon inspection
+  of the `info` fields. Arguments in this callback are:
+  - `result` {Boolean} Whether or not to accept the handshake.
+  - `code` {Number} When `result` is `false` this field determines the HTTP
+    error status code to be sent to the client.
+  - `name` {String} When `result` is `false` this field determines the HTTP
+    reason phrase.
+
+
+If `handleProtocols` is not set then the handshake is automatically accepted,
+otherwise the function takes a single argument:
+
+- `protocols` {Array} The list of WebSocket subprotocols indicated by the
+  client in the `Sec-WebSocket-Protocol` header.
+
+If returned value is `false` then the handshake is rejected with the HTTP 401
+status code, otherwise the returned value sets the value of the
+`Sec-WebSocket-Protocol` header in the HTTP 101 response.
+
+`perMessageDeflate` can be used to control the behavior of
+[permessage-deflate extension][permessage-deflate].
+The extension is disabled when `false`. Defaults to `true`. If an object is
+provided then that is extension parameters:
+
+- `serverNoContextTakeover` {Boolean} Whether to use context take over or not.
+- `clientNoContextTakeover` {Boolean} The value to be requested to clients
+  whether to use context take over or not.
+- `serverMaxWindowBits` {Number} The value of windowBits.
+- `clientMaxWindowBits` {Number} The value of max windowBits to be requested
+  to clients.
+- `memLevel` {Number} The value of memLevel.
+- `threshold` {Number} Payloads smaller than this will not be compressed.
+  Defaults to 1024 bytes.
+
+If a property is empty then either an offered configuration or a default value
+is used.
+When sending a fragmented message the length of the first fragment is compared
+to the threshold. This determines if compression is used for the entire message.
+
+
+`callback` will be added as a listener for the `listening` event when the
+HTTP server is created internally and that is when the `port` option is
+provided.
 
-### new WebSocket.Server([options], [callback])
+### Event: 'connection'
 
-* `options` Object
-  * `host` String
-  * `port` Number
-  * `server` http.Server
-  * `verifyClient` Function
-  * `handleProtocols` Function
-  * `path` String
-  * `noServer` Boolean
-  * `clientTracking` Boolean
-  * `perMessageDeflate` Boolean|Object
-* `callback` Function
+- `socket` {WebSocket}
 
-Construct a new server object.
+Emitted when the handshake is complete. `socket` is an instance of `WebSocket`.
 
-Either `port` or `server` must be provided, otherwise you might enable
-`noServer` if you want to pass the requests directly. Please note that the
-`callback` is only used when you supply the a `port` number in the options.
+### Event: 'error'
 
-### options.verifyClient
+- `error` {Error}
 
-`verifyClient` can be used in two different ways. If it is provided with two arguments then those are:
-* `info` Object:
-  * `origin` String: The value in the Origin header indicated by the client.
-  * `req` http.ClientRequest: The client HTTP GET request.
-  * `secure` Boolean: `true` if `req.connection.authorized` or `req.connection.encrypted` is set.
-* `cb` Function: A callback that must be called by the user upon inspection of the `info` fields. Arguments in this callback are:
-  * `result` Boolean: Whether the user accepts or not the handshake.
-  * `code` Number: If `result` is `false` this field determines the HTTP error status code to be sent to the client.
-  * `name` String: If `result` is `false` this field determines the HTTP reason phrase.
+Emitted when an error occurs on the underlying server.
 
-If `verifyClient` is provided with a single argument then that is:
-* `info` Object: Same as above.
+### Event: 'headers'
 
-In this case the return code (Boolean) of the function determines whether the handshake is accepted or not.
+- `headers` {Array}
 
-If `verifyClient` is not set then the handshake is automatically accepted.
+Emitted before the response headers are written to the socket as part of the
+handshake. This allows you to inspect/modify the headers before they are sent.
 
-### options.handleProtocols
+### Event: 'listening'
 
-`handleProtocols` takes a single argument:
-* `protocols` Array: The list of WebSocket sub-protocols indicated by the client in the `Sec-WebSocket-Protocol` header.
+Emitted when the underlying server has been bound.
 
-If returned value is `false` then the handshake is rejected with the HTTP 401 status code, otherwise the returned value sets the value of the `Sec-WebSocket-Protocol` header in the HTTP 101 response.
+### server.clients
 
-If `handleProtocols` is not set then the handshake is automatically accepted.
+- {Set}
 
-### options.perMessageDeflate
+A set that stores all connected clients. Please note that this property is only
+added when the `clientTracking` is truthy.
 
-`perMessageDeflate` can be used to control the behavior of [permessage-deflate extension](https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19). The extension is disabled when `false`. Defaults to `true`. If an object is provided then that is extension parameters:
+### server.close([callback])
 
-* `serverNoContextTakeover` Boolean: Whether to use context take over or not.
-* `clientNoContextTakeover` Boolean: The value to be requested to clients whether to use context take over or not.
-* `serverMaxWindowBits` Number: The value of windowBits.
-* `clientMaxWindowBits` Number: The value of max windowBits to be requested to clients.
-* `memLevel` Number: The value of memLevel.
-* `threshold` Number: Payloads smaller than this will not be compressed. Default 1024 bytes.
+Close the server and terminate all clients, calls callback when done.
 
-If a property is empty then either an offered configuration or a default value is used.
-When sending a fragmented message the length of the first fragment is compared to the threshold. This determines if compression is used for the entire message.
+### server.handleUpgrade(request, socket, head, callback)
 
-### server.close([callback])
+- `request` {http.IncomingMessage} The client HTTP GET request.
+- `socket` {net.Socket} The network socket between the server and client.
+- `head` {Buffer} The first packet of the upgraded stream.
+- `callback` {Function}.
 
-Close the server and terminate all clients, calls callback when done with an error if one occurred.
+Handle a HTTP upgrade request. When the HTTP server is created internally or
+when the HTTP server is passed via the `server` option, this method is called
+automatically. When operating in "noServer" mode, this method must be called
+manually.
 
-### server.handleUpgrade(request, socket, upgradeHead, callback)
+If the upgrade is successfull, the `callback` is called with a `WebSocket`
+object as parameter.
 
-Handles a HTTP Upgrade request. `request` is an instance of `http.ServerRequest`, `socket` is an instance of `net.Socket`.
+### server.shouldHandle(request)
+
+- `request` {http.IncomingMessage} The client HTTP GET request.
+
+See if a given request should be handled by this server.
+By default this method validates the pathname of the request, matching it
+against the `path` option if provided.
+The return value, `true` or `false`, determines whether or not to accept the
+handshake.
+
+This method can be overriden when a custom handling logic is required.
+
+## Class: WebSocket
 
-When the Upgrade was successfully, the `callback` will be called with a `WebSocket` object as parameter.
+This class represents a WebSocket. It extends the `EventEmitter`.
+
+### Ready state constants
+
+|Constant   | Value | Description                                      |
+|-----------|-------|--------------------------------------------------|
+|CONNECTING | 0     | The connection is not yet open.                  |
+|OPEN       | 1     | The connection is open and ready to communicate. |
+|CLOSING    | 2     | The connection is in the process of closing.     |
+|CLOSED     | 3     | The connection is closed.                        |
+
+### new WebSocket(address[, protocols][, options])
+
+- `address` {String} The URL to which to connect.
+- `protocols` {String|Array} The list of subprotocols.
+- `options` {Object}
+  - `protocol` {String} Value of the `Sec-WebSocket-Protocol` header.
+  - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate.
+  - `localAddress` {String} Local interface to bind for network connections.
+  - `protocolVersion` {Number} Value of the `Sec-WebSocket-Version` header.
+  - `headers` {Object} An object with custom headers to send along with the
+    request.
+  - `origin` {String} Value of the `Origin` or `Sec-WebSocket-Origin` header
+    depending on the `protocolVersion`.
+  - `agent` {http.Agent|https.Agent} Use the specified Agent,
+  - `host` {String} Value of the `Host` header.
+  - `checkServerIdentity` {Function} A function to validate the server hostname.
+  - `rejectUnauthorized` {Boolean} Verify or not the server certificate.
+  - `passphrase` {String} The passphrase for the private key or pfx.
+  - `ciphers` {String} The ciphers to use or exclude
+  - `cert` {String|Array|Buffer} The certificate key.
+  - `key` {String|Array|Buffer} The private key.
+  - `pfx` {String|Buffer} The private key, certificate, and CA certs.
+  - `ca` {Array} Trusted certificates.
+
+`perMessageDeflate` parameters are the same of the server, the only difference
+is the direction of requests (e.g. `serverNoContextTakeover` is the value to be
+requested to the server).
+
+Create a new WebSocket instance.
+
+### Event: 'close'
+
+- `code` {Number}
+- `reason` {String}
+
+Emitted when the connection is closed. `code` is a numeric value indicating the
+status code explaining why the connection has been closed. `reason` is a
+human-readable string explaining why the connection has been closed.
 
 ### Event: 'error'
 
-`function (error) { }`
+- `error` {Error}
 
-If the underlying server emits an error, it will be forwarded here.
+Emitted when an error occurs. Errors from the underlying `net.Socket` are
+forwarded here.
 
-### Event: 'headers'
+### Event: 'message'
 
-`function (headers) { }`
+- `data` {String|Buffer}
+- `flags` {Object}
+  - `binary` {Boolean} Specifies if `data` is binary.
+  - `masked` {Boolean} Specifies if `data` was masked.
 
-Emitted with the object of HTTP headers that are going to be written to the `Stream` as part of the handshake.
+Emitted when a message is received from the server.
 
-### Event: 'connection'
+### Event: 'open'
 
-`function (socket) { }`
+Emitted when the connection is established.
 
-When a new WebSocket connection is established. `socket` is an object of type `WebSocket`.
+### Event: 'ping'
 
+- `data` {Buffer}
+- `flags` {Object}
+  - `binary` {Boolean} Specifies if `data` is binary.
+  - `masked` {Boolean} Specifies if `data` was masked.
 
-## Class: WebSocket
+Emitted when a ping is received from the server.
+
+### Event: 'pong'
+
+- `data` {Buffer}
+- `flags` {Object}
+  - `binary` {Boolean} Specifies if `data` is binary.
+  - `masked` {Boolean} Specifies if `data` was masked.
+
+Emitted when a pong is received from the server.
+
+### Event: 'unexpected-response'
+
+- `request` {http.ClientRequest}
+- `response` {http.IncomingMessage}
+
+Emitted when the server response is not the expected one, for example a 401
+response. This event gives the ability to read the response in order to extract
+useful information. If the server sends an invalid response and there isn't a
+listener for this event, an error is emitted.
 
-This class represents a WebSocket connection. It is an `EventEmitter`.
+### websocket.addEventListener(type, listener)
 
-### new WebSocket(address, [protocols], [options])
+- `type` {String} A string representing the event type to listen for.
+- `listener` {Function} The listener to add.
 
-* `address` String
-* `protocols` String|Array
-* `options` Object
-  * `protocol` String
-  * `agent` Agent
-  * `headers` Object
-  * `protocolVersion` Number
-    -- the following only apply if `address` is a String
-  * `host` String
-  * `origin` String
-  * `pfx` String|Buffer
-  * `key` String|Buffer
-  * `passphrase` String
-  * `cert` String|Buffer
-  * `ca` Array
-  * `ciphers` String
-  * `rejectUnauthorized` Boolean
-  * `perMessageDeflate` Boolean|Object
-  * `localAddress` String
+Register an event listener emulating the `EventTarget` interface.
 
-Instantiating with an `address` creates a new WebSocket client object. If `address` is an Array (request, socket, rest), it is instantiated as a Server client (e.g. called from the `WebSocket.Server`).
+### websocket.binaryType
 
-### options.perMessageDeflate
+- {String}
 
-Parameters of permessage-deflate extension which have the same form with the one for `WebSocket.Server` except the direction of requests. (e.g. `serverNoContextTakeover` is the value to be requested to the server)
+A string indicating the type of binary data being transmitted by the connection.
+This should be either "nodebuffer" or "arraybuffer". Defaults to "nodebuffer".
+
+### websocket.bufferedAmount
+
+- {Number}
+
+The number of bytes of data that have been queued using calls to `send()` but
+not yet transmitted to the network.
 
 ### websocket.bytesReceived
 
+- {Number}
+
 Received bytes count.
 
-### websocket.readyState
+### websocket.close([code][, reason])
 
-Possible states are `WebSocket.CONNECTING`, `WebSocket.OPEN`, `WebSocket.CLOSING`, `WebSocket.CLOSED`.
+- `code` {Number} A numeric value indicating the status code explaining why
+  the connection is being closed.
+- `reason` {String} A human-readable string explaining why the connection is
+  closing.
 
-### websocket.protocolVersion
+Initiate a closing handshake.
 
-The WebSocket protocol version used for this connection, `8`, `13`.
+### websocket.extensions
 
-### websocket.url
+- {Object}
 
-The URL of the WebSocket server (only for clients)
+An object containing the negotiated extensions.
 
-### websocket.upgradeReq
+### websocket.onclose
 
-The http request that initiated the upgrade. Useful for parsing authorty headers, cookie headers and other information to associate a specific Websocket to a specific Client. This is only available for WebSockets constructed by a Server.
+- {Function}
 
-### websocket.close([code], [data])
+An event listener to be called when connection is closed. The listener receives
+a `CloseEvent` named "close".
 
-Gracefully closes the connection, after sending a description message
+### websocket.onerror
 
-### websocket.pause()
+- {Function}
 
-Pause the client stream
+An event listener to be called when an error occurs. The listener receives
+an `Error` instance.
 
-### websocket.ping([data], [options], [dontFailWhenClosed])
+### websocket.onmessage
 
-Sends a ping. `data` is sent, `options` is an object with members `mask` and `binary`. `dontFailWhenClosed` indicates whether or not to throw if the connection isnt open.
+- {Function}
 
-### websocket.pong([data], [options], [dontFailWhenClosed])
+An event listener to be called when a message is received from the server. The
+listener receives a `MessageEvent` named "message".
 
-Sends a pong. `data` is sent, `options` is an object with members `mask` and `binary`. `dontFailWhenClosed` indicates whether or not to throw if the connection isnt open.
+### websocket.onopen
 
+- {Function}
 
-### websocket.resume()
+An event listener to be called when the connection is established. The listener
+receives an `OpenEvent` named "open".
 
-Resume the client stream
+### websocket.pause()
 
-### websocket.send(data, [options], [callback])
+Pause the socket.
 
-* `data` Any The data to send.
-* `options` Object An options object.
-  * `compress` Boolean Specifies whether `data` should be compressed or not.
-    Defaults to `true` when permessage-deflate is enabled.
-  * `binary` Boolean Specifies whether `data` should be sent as a binary or not.
-    Default is autodetected.
-  * `mask` Boolean Specifies whether `data` should be masked or not. Defaults
+### websocket.ping([data[, options[, dontFailWhenClosed]]])
+
+- `data` {Any} The data to send in the ping frame.
+- `options` {Object}
+  - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults
     to `true` when `websocket` is not a server client.
-  * `fin` Boolean Specifies whether `data` is the last fragment of a message or
-    not. Defaults to `true`.
-* `callback` Function An optional callback which is invoked when the send
-  completes.
+- `dontFailWhenClosed` {Boolean} Specifies whether or not to throw an error if
+  the connection is not open.
 
-Sends `data` through the connection.
+Send a ping.
 
-### websocket.stream([options], callback)
+### websocket.pong([data[, options[, dontFailWhenClosed]]])
 
-Streams data through calls to a user supplied function. `options` can be an object with members `mask` and `binary`.  `callback` is executed on successive ticks of which send is `function (data, final)`.
+- `data` {Any} The data to send in the ping frame.
+- `options` {Object}
+  - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults
+    to `true` when `websocket` is not a server client.
+- `dontFailWhenClosed` {Boolean} Specifies whether or not to throw an error if
+  the connection is not open.
 
-### websocket.terminate()
+Send a pong.
 
-Immediately shuts down the connection
+### websocket.protocol
 
-### websocket.onopen
-### websocket.onerror
-### websocket.onclose
-### websocket.onmessage
+- {String}
 
-Emulates the W3C Browser based WebSocket interface using function members.
+The subprotocol selected by the server.
 
-### websocket.addEventListener(method, listener)
+### websocket.protocolVersion
 
-Emulates the W3C Browser based WebSocket interface using addEventListener.
+- {Number}
 
-### Event: 'error'
+The WebSocket protocol version used for this connection, 8 or 13.
 
-`function (error) { }`
+### websocket.readyState
 
-If the client emits an error, this event is emitted (errors from the underlying `net.Socket` are forwarded here).
+- {Number}
 
-### Event: 'close'
+The current state of the connection. This is one of the ready state constants.
 
-`function (code, message) { }`
+### websocket.removeEventListener(type, listener)
 
-Is emitted when the connection is closed. `code` is defined in the WebSocket specification.
+- `type` {String} A string representing the event type to remove.
+- `listener` {Function} The listener to remove.
 
-The `close` event is also emitted when then underlying `net.Socket` closes the connection (`end` or `close`).
+Removes an event listener emulating the `EventTarget` interface.
 
-### Event: 'message'
+### websocket.resume()
 
-`function (data, flags) { }`
+Resume the socket
 
-Is emitted when data is received. `flags` is an object with member `binary`.
+### websocket.send(data, [options][, callback])
 
-### Event: 'ping'
+- `data` {Any} The data to send.
+- `options` {Object}
+  - `compress` {Boolean} Specifies whether `data` should be compressed or not.
+    Defaults to `true` when permessage-deflate is enabled.
+  - `binary` {Boolean} Specifies whether `data` should be sent as a binary or not.
+    Default is autodetected.
+  - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults
+    to `true` when `websocket` is not a server client.
+  - `fin` {Boolean} Specifies whether `data` is the last fragment of a message or
+    not. Defaults to `true`.
+- `callback` {Function} An optional callback which is invoked when `data` is
+  written out.
 
-`function (data, flags) { }`
+Sends `data` through the connection.
 
-Is emitted when a ping is received. `flags` is an object with member `binary`.
+### websocket.terminate()
 
-### Event: 'pong'
+Send a FIN packet to the other peer.
 
-`function (data, flags) { }`
+### websocket.upgradeReq
 
-Is emitted when a pong is received. `flags` is an object with member `binary`.
+- {http.IncomingMessage}
 
-### Event: 'open'
+The http GET request sent by the client. Useful for parsing authorty headers,
+cookie headers, and other information. This is only available for server clients.
 
-`function () { }`
+### websocket.url
 
-Emitted when the connection is established.
+- {String}
+
+The URL of the WebSocket server. Server clients don't have this attribute.
+
+[permessage-deflate]: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19
diff --git a/lib/Sender.js b/lib/Sender.js
index b15b20a7..0091a849 100644
--- a/lib/Sender.js
+++ b/lib/Sender.js
@@ -148,24 +148,36 @@ class Sender {
   send (data, options, cb) {
     const pmd = this.extensions[PerMessageDeflate.extensionName];
     var opcode = options.binary ? 2 : 1;
-    var compress = options.compress;
+    var rsv1 = options.compress;
+    var readOnly = true;
+
+    if (data && !Buffer.isBuffer(data)) {
+      if (data instanceof ArrayBuffer) {
+        data = Buffer.from(data);
+      } else if (ArrayBuffer.isView(data)) {
+        data = viewToBuffer(data);
+      } else {
+        data = Buffer.from(typeof data === 'number' ? data.toString() : data);
+        readOnly = false;
+      }
+    }
 
     if (this.firstFragment) {
       this.firstFragment = false;
-      if (compress && data && pmd) compress = data.length >= pmd.threshold;
-      this.compress = compress;
+      if (rsv1 && data && pmd) rsv1 = data.length >= pmd.threshold;
+      this.compress = rsv1;
     } else {
-      compress = false;
+      rsv1 = false;
       opcode = 0;
     }
 
     if (options.fin) this.firstFragment = true;
 
     if (pmd) {
-      const args = [opcode, data, options.fin, options.mask, compress, cb];
+      const args = [opcode, data, readOnly, options.fin, options.mask, rsv1, cb];
       this.enqueue([this.sendCompressed, args]);
     } else {
-      this.frameAndSend(opcode, data, true, options.fin, options.mask, false, cb);
+      this.frameAndSend(opcode, data, readOnly, options.fin, options.mask, false, cb);
     }
   }
 
@@ -174,20 +186,20 @@ class Sender {
    *
    * @param {Number} opcode The opcode
    * @param {*} data The message to send
+   * @param {Boolean} readOnly Specifies whether `data` can be modified
    * @param {Boolean} fin Specifies whether or not to set the FIN bit
    * @param {Boolean} mask Specifies whether or not to mask `data`
    * @param {Boolean} rsv1 Specifies whether or not to set the RSV1 bit
    * @param {Function} cb Callback
    * @private
    */
-  sendCompressed (opcode, data, fin, mask, rsv1, cb) {
+  sendCompressed (opcode, data, readOnly, fin, mask, rsv1, cb) {
     if (!this.compress) {
-      this.frameAndSend(opcode, data, true, fin, mask, false, cb);
+      this.frameAndSend(opcode, data, readOnly, fin, mask, false, cb);
       this.continue();
       return;
     }
 
-    if (data && !Buffer.isBuffer(data)) data = toBuffer(data);
     this.extensions[PerMessageDeflate.extensionName].compress(data, fin, (err, buf) => {
       if (err) {
         if (cb) cb(err);
@@ -344,20 +356,6 @@ function viewToBuffer (view) {
   return buf;
 }
 
-/**
- * Converts `data` into a buffer.
- *
- * @param {*} data Data to convert
- * @return {Buffer} Converted data
- * @private
- */
-function toBuffer (data) {
-  if (data instanceof ArrayBuffer) return Buffer.from(data);
-  if (ArrayBuffer.isView(data)) return viewToBuffer(data);
-
-  return Buffer.from(typeof data === 'number' ? data.toString() : data);
-}
-
 /**
  * Generates a random mask.
  *
diff --git a/lib/WebSocket.js b/lib/WebSocket.js
index 5899c3a1..794e7469 100644
--- a/lib/WebSocket.js
+++ b/lib/WebSocket.js
@@ -11,7 +11,6 @@ const crypto = require('crypto');
 const Ultron = require('ultron');
 const https = require('https');
 const http = require('http');
-const util = require('util');
 const url = require('url');
 
 const PerMessageDeflate = require('./PerMessageDeflate');
@@ -25,277 +24,391 @@ const closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection
 const protocolVersion = 13;
 
 /**
- * WebSocket implementation
+ * Class representing a WebSocket.
  *
- * @constructor
- * @param {String} address Connection address.
- * @param {String|Array} protocols WebSocket protocols.
- * @param {Object} options Additional connection options.
- * @api public
+ * @extends EventEmitter
  */
-function WebSocket (address, protocols, options) {
-  if (this instanceof WebSocket === false) {
-    return new WebSocket(address, protocols, options);
-  }
+class WebSocket extends EventEmitter {
+  /**
+   * Create a new `WebSocket`.
+   *
+   * @param {String} address The URL to which to connect
+   * @param {(String|String[])} protocols The subprotocols
+   * @param {Object} options Connection options
+   */
+  constructor (address, protocols, options) {
+    super();
+
+    if (typeof protocols === 'object' && !Array.isArray(protocols)) {
+      //
+      // Accept the `options` object as the 2nd argument.
+      //
+      options = protocols;
+      protocols = null;
+    }
 
-  EventEmitter.call(this);
+    if (typeof protocols === 'string') protocols = [protocols];
+    if (!Array.isArray(protocols)) protocols = [];
 
-  if (protocols && !Array.isArray(protocols) && typeof protocols === 'object') {
-    // accept the "options" Object as the 2nd argument
-    options = protocols;
-    protocols = null;
-  }
-
-  if (typeof protocols === 'string') {
-    protocols = [ protocols ];
-  }
+    this._finalize = this.finalize.bind(this);
 
-  if (!Array.isArray(protocols)) {
-    protocols = [];
-  }
+    this._binaryType = 'nodebuffer';
+    this._closeReceived = false;
+    this.bytesReceived = 0;
+    this.readyState = null;
+    this.extensions = {};
+    this._socket = null;
+    this._ultron = null;
 
-  this._socket = null;
-  this._ultron = null;
-  this._closeReceived = false;
-  this.bytesReceived = 0;
-  this.readyState = null;
-  this.extensions = {};
-  this._binaryType = 'nodebuffer';
-
-  if (Array.isArray(address)) {
-    initAsServerClient.apply(this, address.concat(options));
-  } else {
-    initAsClient.apply(this, [address, protocols, options]);
+    if (Array.isArray(address)) {
+      initAsServerClient.call(this, address[0], address[1], address[2], options);
+    } else {
+      initAsClient.call(this, address, protocols, options);
+    }
   }
-}
 
-/**
- * Inherits from EventEmitter.
- */
-util.inherits(WebSocket, EventEmitter);
+  get CONNECTING () { return WebSocket.CONNECTING; }
+  get CLOSING () { return WebSocket.CLOSING; }
+  get CLOSED () { return WebSocket.CLOSED; }
+  get OPEN () { return WebSocket.OPEN; }
 
-/**
- * Ready States
- */
-['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'].forEach(function each (state, index) {
-  WebSocket.prototype[state] = WebSocket[state] = index;
-});
+  /**
+   * @type {Number}
+   */
+  get bufferedAmount () {
+    var amount = 0;
 
-/**
- * Gracefully closes the connection, after sending a description message to the server
- *
- * @param {Object} data to be sent to the server
- * @api public
- */
-WebSocket.prototype.close = function close (code, data) {
-  if (this.readyState === WebSocket.CLOSED) return;
+    if (this._socket) amount = this._socket.bufferSize || 0;
+    return amount;
+  }
 
-  if (this.readyState === WebSocket.CONNECTING) {
-    this.readyState = WebSocket.CLOSED;
-    return;
+  /**
+   * This deviates from the WHATWG interface since ws doesn't support the required
+   * default "blob" type (instead we define a custom "nodebuffer" type).
+   *
+   * @type {String}
+   */
+  get binaryType () {
+    return this._binaryType;
   }
 
-  if (this.readyState === WebSocket.CLOSING) {
-    if (this._closeReceived && this._isServer) {
-      this.terminate();
+  set binaryType (type) {
+    if (type === 'arraybuffer' || type === 'nodebuffer') {
+      this._binaryType = type;
+    } else {
+      throw new SyntaxError('unsupported binaryType: must be either "nodebuffer" or "arraybuffer"');
     }
-    return;
   }
 
-  try {
-    this.readyState = WebSocket.CLOSING;
-    this._closeCode = code;
-    this._closeMessage = data;
-    var mask = !this._isServer;
-    this._sender.close(code, data, mask, (err) => {
-      if (err) this.emit('error', err);
+  /**
+   * Set up the socket and the internal resources.
+   *
+   * @param {net.Socket} socket The network socket between the server and client
+   * @param {Buffer} head The first packet of the upgraded stream
+   * @private
+   */
+  setSocket (socket, head) {
+    socket.setTimeout(0);
+    socket.setNoDelay();
+
+    this._receiver = new Receiver(this.extensions, this.maxPayload);
+    this._sender = new Sender(socket, this.extensions);
+    this._ultron = new Ultron(socket);
+    this._socket = socket;
+
+    // socket cleanup handlers
+    this._ultron.on('close', this._finalize);
+    this._ultron.on('error', this._finalize);
+    this._ultron.on('end', this._finalize);
+
+    // ensure that the head is added to the receiver
+    if (head && head.length > 0) {
+      socket.unshift(head);
+      head = null;
+    }
 
-      if (this._closeReceived && this._isServer) {
-        this.terminate();
-      } else {
-        // ensure that the connection is cleaned up even when no response of closing handshake.
-        clearTimeout(this._closeTimer);
-        this._closeTimer = setTimeout(cleanupWebsocketResources.bind(this, true), closeTimeout);
-      }
+    // subsequent packets are pushed to the receiver
+    this._ultron.on('data', (data) => {
+      this.bytesReceived += data.length;
+      this._receiver.add(data);
     });
-  } catch (e) {
-    this.emit('error', e);
-  }
-};
 
-/**
- * Pause the client stream
- *
- * @api public
- */
-WebSocket.prototype.pause = function pauser () {
-  if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
+    // receiver event handlers
+    this._receiver.ontext = (data, flags) => this.emit('message', data, flags);
+    this._receiver.onbinary = (data, flags) => {
+      flags.binary = true;
+      this.emit('message', data, flags);
+    };
+    this._receiver.onping = (data, flags) => {
+      this.pong(data, { mask: !this._isServer }, true);
+      this.emit('ping', data, flags);
+    };
+    this._receiver.onpong = (data, flags) => this.emit('pong', data, flags);
+    this._receiver.onclose = (code, data, flags) => {
+      this._closeReceived = true;
+      this.close(code, data);
+    };
+    this._receiver.onerror = (error, errorCode) => {
+      // close the connection when the receiver reports a HyBi error code
+      this.close(errorCode, '');
+      this.emit('error', error);
+    };
 
-  return this._socket.pause();
-};
+    // sender event handlers
+    this._sender.onerror = (error) => {
+      this.close(1002, '');
+      this.emit('error', error);
+    };
 
-/**
- * Sends a ping
- *
- * @param {Object} data to be sent to the server
- * @param {Object} Members - mask: boolean, binary: boolean
- * @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
- * @api public
- */
-WebSocket.prototype.ping = function ping (data, options, dontFailWhenClosed) {
-  if (this.readyState !== WebSocket.OPEN) {
-    if (dontFailWhenClosed === true) return;
-    throw new Error('not opened');
+    this.readyState = WebSocket.OPEN;
+    this.emit('open');
   }
 
-  options = options || {};
+  /**
+   * Clean up and release internal resources and emit the `close` event.
+   *
+   * @param {(Boolean|Error)} Indicates whether or not an error occurred
+   * @private
+   */
+  finalize (error) {
+    if (this.readyState === WebSocket.CLOSED) return;
 
-  if (options.mask === undefined) options.mask = !this._isServer;
+    this.readyState = WebSocket.CLOSED;
 
-  this._sender.ping(data, options);
-};
+    clearTimeout(this._closeTimer);
+    this._closeTimer = null;
 
-/**
- * Sends a pong
- *
- * @param {Object} data to be sent to the server
- * @param {Object} Members - mask: boolean, binary: boolean
- * @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
- * @api public
- */
-WebSocket.prototype.pong = function (data, options, dontFailWhenClosed) {
-  if (this.readyState !== WebSocket.OPEN) {
-    if (dontFailWhenClosed === true) return;
-    throw new Error('not opened');
-  }
+    // If the connection was closed abnormally (with an error), or if
+    // the close control frame was not received then the close code
+    // must default to 1006.
+    if (error || !this._closeReceived) {
+      this._closeCode = 1006;
+    }
+    this.emit('close', this._closeCode || 1000, this._closeMessage || '');
 
-  options = options || {};
+    if (this._socket) {
+      if (this._ultron) this._ultron.destroy();
+      this._socket.on('error', function onerror () {
+        try {
+          this.destroy();
+        } catch (e) {}
+      });
+
+      try {
+        if (!error) this._socket.end();
+        else this._socket.destroy();
+      } catch (e) { /* Ignore termination errors */ }
 
-  if (options.mask === undefined) options.mask = !this._isServer;
+      this._socket = null;
+      this._ultron = null;
+    }
 
-  this._sender.pong(data, options);
-};
+    if (this._sender) {
+      this._sender = this._sender.onerror = null;
+    }
 
-/**
- * Resume the client stream
- *
- * @api public
- */
-WebSocket.prototype.resume = function resume () {
-  if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
+    if (this._receiver) {
+      this._receiver.cleanup();
+      this._receiver = null;
+    }
 
-  return this._socket.resume();
-};
+    if (this.extensions[PerMessageDeflate.extensionName]) {
+      this.extensions[PerMessageDeflate.extensionName].cleanup();
+    }
 
-/**
- * Sends a piece of data
- *
- * @param {Object} data to be sent to the server
- * @param {Object} Members - mask: boolean, binary: boolean, compress: boolean
- * @param {function} Optional callback which is executed after the send completes
- * @api public
- */
+    this.extensions = null;
 
-WebSocket.prototype.send = function send (data, options, cb) {
-  if (typeof options === 'function') {
-    cb = options;
-    options = {};
+    this.removeAllListeners();
+    this.on('error', function onerror () {}); // catch all errors after this
   }
 
-  if (this.readyState !== WebSocket.OPEN) {
-    if (cb) cb(new Error('not opened'));
-    else throw new Error('not opened');
-    return;
+  /**
+   * Pause the socket stream.
+   *
+   * @public
+   */
+  pause () {
+    if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
+
+    this._socket.pause();
   }
 
-  if (!data) data = '';
+  /**
+   * Resume the socket stream
+   *
+   * @public
+   */
+  resume () {
+    if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
+
+    this._socket.resume();
+  }
 
-  options = options || {};
-  if (options.fin !== false) options.fin = true;
+  /**
+   * Start a closing handshake.
+   *
+   * @param {Number} code Status code explaining why the connection is closing
+   * @param {String} data A string explaining why the connection is closing
+   * @public
+   */
+  close (code, data) {
+    if (this.readyState === WebSocket.CLOSED) return;
+
+    if (this.readyState === WebSocket.CONNECTING) {
+      this.readyState = WebSocket.CLOSED;
+      return;
+    }
 
-  if (options.binary === undefined) {
-    options.binary = data instanceof Buffer || data instanceof ArrayBuffer ||
-      ArrayBuffer.isView(data);
+    if (this.readyState === WebSocket.CLOSING) {
+      if (this._closeReceived && this._isServer) {
+        this.terminate();
+      }
+      return;
+    }
+
+    try {
+      this.readyState = WebSocket.CLOSING;
+      this._closeCode = code;
+      this._closeMessage = data;
+      var mask = !this._isServer;
+      this._sender.close(code, data, mask, (err) => {
+        if (err) this.emit('error', err);
+
+        if (this._closeReceived && this._isServer) {
+          this.terminate();
+        } else {
+          //
+          // Ensure that the connection is cleaned up even when the closing
+          // handshake fails.
+          //
+          clearTimeout(this._closeTimer);
+          this._closeTimer = setTimeout(this._finalize, closeTimeout, true);
+        }
+      });
+    } catch (e) {
+      this.emit('error', e);
+    }
   }
 
-  if (options.mask === undefined) options.mask = !this._isServer;
-  if (options.compress === undefined) options.compress = true;
-  if (!this.extensions[PerMessageDeflate.extensionName]) {
-    options.compress = false;
+  /**
+   * Send a ping message.
+   *
+   * @param {*} data The message to send
+   * @param {Object} options Options object
+   * @param {Boolean} options.mask Indicates whether or not to mask `data`
+   * @param {Boolean} dontFailWhenClosed Indicates whether or not to throw an if the connection isn't open
+   * @public
+   */
+  ping (data, options, dontFailWhenClosed) {
+    if (this.readyState !== WebSocket.OPEN) {
+      if (dontFailWhenClosed) return;
+      throw new Error('not opened');
+    }
+
+    options = options || {};
+    if (options.mask === undefined) options.mask = !this._isServer;
+
+    this._sender.ping(data, options);
   }
 
-  this._sender.send(data, options, cb);
-};
+  /**
+   * Send a pong message.
+   *
+   * @param {*} data The message to send
+   * @param {Object} options Options object
+   * @param {Boolean} options.mask Indicates whether or not to mask `data`
+   * @param {Boolean} dontFailWhenClosed Indicates whether or not to throw an if the connection isn't open
+   * @public
+   */
+  pong (data, options, dontFailWhenClosed) {
+    if (this.readyState !== WebSocket.OPEN) {
+      if (dontFailWhenClosed) return;
+      throw new Error('not opened');
+    }
 
-/**
- * Immediately shuts down the connection
- *
- * @api public
- */
-WebSocket.prototype.terminate = function terminate () {
-  if (this.readyState === WebSocket.CLOSED) return;
+    options = options || {};
+    if (options.mask === undefined) options.mask = !this._isServer;
 
-  if (this._socket) {
-    this.readyState = WebSocket.CLOSING;
+    this._sender.pong(data, options);
+  }
 
-    // End the connection
-    try {
-      this._socket.end();
-    } catch (e) {
-      // Socket error during end() call, so just destroy it right now
-      cleanupWebsocketResources.call(this, true);
+  /**
+   * Send a data message.
+   *
+   * @param {*} data The message to send
+   * @param {Object} options Options object
+   * @param {Boolean} options.compress Specifies whether or not to compress `data`
+   * @param {Boolean} options.binary Specifies whether `data` is binary or text
+   * @param {Boolean} options.fin Specifies whether the fragment is the last one
+   * @param {Boolean} options.mask Specifies whether or not to mask `data`
+   * @param {Function} cb Callback which is executed when data is written out
+   * @public
+   */
+  send (data, options, cb) {
+    if (typeof options === 'function') {
+      cb = options;
+      options = {};
+    }
+
+    if (this.readyState !== WebSocket.OPEN) {
+      if (cb) cb(new Error('not opened'));
+      else throw new Error('not opened');
       return;
     }
 
-    // Add a timeout to ensure that the connection is completely
-    // cleaned up within 30 seconds, even if the clean close procedure
-    // fails for whatever reason
-    // First cleanup any pre-existing timeout from an earlier "terminate" call,
-    // if one exists.  Otherwise terminate calls in quick succession will leak timeouts
-    // and hold the program open for `closeTimout` time.
-    if (this._closeTimer) { clearTimeout(this._closeTimer); }
-    this._closeTimer = setTimeout(cleanupWebsocketResources.bind(this, true), closeTimeout);
-  } else if (this.readyState === WebSocket.CONNECTING) {
-    cleanupWebsocketResources.call(this, true);
-  }
-};
+    if (!data) data = '';
 
-/**
- * Expose the `bufferedAmount` attribute.
- *
- * @public
- */
-Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
-  get () {
-    var amount = 0;
-    if (this._socket) {
-      amount = this._socket.bufferSize || 0;
+    options = options || {};
+    if (options.fin !== false) options.fin = true;
+
+    if (options.binary === undefined) {
+      options.binary = data instanceof Buffer || data instanceof ArrayBuffer ||
+        ArrayBuffer.isView(data);
     }
-    return amount;
+
+    if (options.mask === undefined) options.mask = !this._isServer;
+    if (options.compress === undefined) options.compress = true;
+    if (!this.extensions[PerMessageDeflate.extensionName]) {
+      options.compress = false;
+    }
+
+    this._sender.send(data, options, cb);
   }
-});
 
-/**
- * Expose the `binaryType` attribute.
- *
- * This deviates from the WHATWG interface since ws doesn't support the required
- * default "blob" type (instead we define a custom "nodebuffer" type).
- *
- * @see {@link https://html.spec.whatwg.org/multipage/comms.html#dom-websocket-binarytype}
- * @public
- */
-Object.defineProperty(WebSocket.prototype, 'binaryType', {
-  get () {
-    return this._binaryType;
-  },
-  set (type) {
-    if (type === 'arraybuffer' || type === 'nodebuffer') {
-      this._binaryType = type;
-    } else {
-      throw new SyntaxError('unsupported binaryType: must be either "nodebuffer" or "arraybuffer"');
+  /**
+   * Half-close the socket sending a FIN packet.
+   *
+   * @public
+   */
+  terminate () {
+    if (this.readyState === WebSocket.CLOSED) return;
+
+    if (this._socket) {
+      this.readyState = WebSocket.CLOSING;
+
+      try {
+        this._socket.end();
+      } catch (e) {
+        this.finalize(true);
+        return;
+      }
+
+      //
+      // Add a timeout to ensure that the connection is completely cleaned up
+      // within 30 seconds, even if the other peer does not send a FIN packet.
+      //
+      if (this._closeTimer) clearTimeout(this._closeTimer);
+      this._closeTimer = setTimeout(this._finalize, closeTimeout, true);
+    } else if (this.readyState === WebSocket.CONNECTING) {
+      this.finalize(true);
     }
   }
-});
+}
+
+WebSocket.CONNECTING = 0;
+WebSocket.OPEN = 1;
+WebSocket.CLOSING = 2;
+WebSocket.CLOSED = 3;
 
 //
 // Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes.
@@ -375,7 +488,7 @@ function initAsServerClient (req, socket, head, options) {
   this.upgradeReq = req;
   this._isServer = true;
 
-  establishConnection.call(this, socket, head);
+  this.setSocket(socket, head);
 }
 
 /**
@@ -543,7 +656,7 @@ function initAsClient (address, protocols, options) {
 
   req.on('error', (error) => {
     this.emit('error', error);
-    cleanupWebsocketResources.call(this, error);
+    this.finalize(error);
   });
 
   req.on('response', (res) => {
@@ -555,10 +668,10 @@ function initAsClient (address, protocols, options) {
       this.emit('error', error);
     }
 
-    cleanupWebsocketResources.call(this, error);
+    this.finalize(error);
   });
 
-  req.on('upgrade', (res, socket, upgradeHead) => {
+  req.on('upgrade', (res, socket, head) => {
     if (this.readyState === WebSocket.CLOSED) {
       // client closed before server accepted connection
       this.emit('close');
@@ -612,119 +725,9 @@ function initAsClient (address, protocols, options) {
       this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
     }
 
-    establishConnection.call(this, socket, upgradeHead);
+    this.setSocket(socket, head);
 
-    // perform cleanup on http resources
     req.removeAllListeners();
     agent = null;
   });
 }
-
-function establishConnection (socket, upgradeHead) {
-  socket.setTimeout(0);
-  socket.setNoDelay();
-
-  this._receiver = new Receiver(this.extensions, this.maxPayload);
-  this._sender = new Sender(socket, this.extensions);
-  this._ultron = new Ultron(socket);
-  this._socket = socket;
-
-  // socket cleanup handlers
-  this._ultron.on('end', cleanupWebsocketResources.bind(this));
-  this._ultron.on('close', cleanupWebsocketResources.bind(this));
-  this._ultron.on('error', cleanupWebsocketResources.bind(this));
-
-  // ensure that the upgradeHead is added to the receiver
-  if (upgradeHead && upgradeHead.length > 0) {
-    socket.unshift(upgradeHead);
-    upgradeHead = null;
-  }
-
-  // subsequent packets are pushed to the receiver
-  this._ultron.on('data', (data) => {
-    this.bytesReceived += data.length;
-    this._receiver.add(data);
-  });
-
-  // receiver event handlers
-  this._receiver.ontext = (data, flags) => this.emit('message', data, flags);
-  this._receiver.onbinary = (data, flags) => {
-    flags.binary = true;
-    this.emit('message', data, flags);
-  };
-  this._receiver.onping = (data, flags) => {
-    this.pong(data, { mask: !this._isServer }, true);
-    this.emit('ping', data, flags);
-  };
-  this._receiver.onpong = (data, flags) => this.emit('pong', data, flags);
-  this._receiver.onclose = (code, data, flags) => {
-    this._closeReceived = true;
-    this.close(code, data);
-  };
-  this._receiver.onerror = (error, errorCode) => {
-    // close the connection when the receiver reports a HyBi error code
-    this.close(errorCode, '');
-    this.emit('error', error);
-  };
-
-  // sender event handlers
-  this._sender.onerror = (error) => {
-    this.close(1002, '');
-    this.emit('error', error);
-  };
-
-  this.readyState = WebSocket.OPEN;
-  this.emit('open');
-}
-
-function cleanupWebsocketResources (error) {
-  if (this.readyState === WebSocket.CLOSED) return;
-
-  this.readyState = WebSocket.CLOSED;
-
-  clearTimeout(this._closeTimer);
-  this._closeTimer = null;
-
-  // If the connection was closed abnormally (with an error), or if
-  // the close control frame was not received then the close code
-  // must default to 1006.
-  if (error || !this._closeReceived) {
-    this._closeCode = 1006;
-  }
-  this.emit('close', this._closeCode || 1000, this._closeMessage || '');
-
-  if (this._socket) {
-    if (this._ultron) this._ultron.destroy();
-    this._socket.on('error', function onerror () {
-      try {
-        this.destroy();
-      } catch (e) {}
-    });
-
-    try {
-      if (!error) this._socket.end();
-      else this._socket.destroy();
-    } catch (e) { /* Ignore termination errors */ }
-
-    this._socket = null;
-    this._ultron = null;
-  }
-
-  if (this._sender) {
-    this._sender = this._sender.onerror = null;
-  }
-
-  if (this._receiver) {
-    this._receiver.cleanup();
-    this._receiver = null;
-  }
-
-  if (this.extensions[PerMessageDeflate.extensionName]) {
-    this.extensions[PerMessageDeflate.extensionName].cleanup();
-  }
-
-  this.extensions = null;
-
-  this.removeAllListeners();
-  this.on('error', function onerror () {}); // catch all errors after this
-}


Test patch:
diff --git a/test/Sender.test.js b/test/Sender.test.js
index 0600dd9b..1b7ed063 100644
--- a/test/Sender.test.js
+++ b/test/Sender.test.js
@@ -43,13 +43,38 @@ describe('Sender', function () {
     });
   });
 
+  describe('#ping', function () {
+    it('works with multiple types of data', function (done) {
+      let count = 0;
+      const sender = new Sender({
+        write: (data) => {
+          if (++count < 4) {
+            assert.ok(data.equals(Buffer.from([0x89, 0x02, 0x68, 0x69])));
+          } else {
+            assert.ok(data.equals(Buffer.from([0x89, 0x02, 0x31, 0x30])));
+            done();
+          }
+        }
+      });
+
+      const array = new Uint8Array([0x68, 0x69]);
+      const options = { mask: false };
+
+      sender.ping(array.buffer, options);
+      sender.ping(array, options);
+      sender.ping('hi', options);
+      sender.ping(10, options);
+    });
+  });
+
   describe('#send', function () {
     it('compresses data if compress option is enabled', function (done) {
       const perMessageDeflate = new PerMessageDeflate({ threshold: 0 });
+      let count = 0;
       const sender = new Sender({
         write: (data) => {
           assert.strictEqual(data[0] & 0x40, 0x40);
-          done();
+          if (++count === 4) done();
         }
       }, {
         'permessage-deflate': perMessageDeflate
@@ -57,7 +82,13 @@ describe('Sender', function () {
 
       perMessageDeflate.accept([{}]);
 
-      sender.send('hi', { compress: true, fin: true });
+      const options = { compress: true, fin: true };
+      const array = new Uint8Array([0x68, 0x69]);
+
+      sender.send(array.buffer, options);
+      sender.send(array, options);
+      sender.send('hi', options);
+      sender.send(100, options);
     });
 
     it('does not compress data for small payloads', function (done) {
diff --git a/test/WebSocket.test.js b/test/WebSocket.test.js
index 9771978c..cd484655 100644
--- a/test/WebSocket.test.js
+++ b/test/WebSocket.test.js
@@ -20,13 +20,6 @@ class CustomAgent extends http.Agent {
 
 describe('WebSocket', function () {
   describe('#ctor', function () {
-    it('should return a new instance if called without new', function (done) {
-      const ws = WebSocket('ws://localhost');
-
-      assert.ok(ws instanceof WebSocket);
-      ws.on('error', () => done());
-    });
-
     it('throws an error when using an invalid url', function () {
       assert.throws(
         () => new WebSocket('echo.websocket.org'),