websocket.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  1. 'use strict'
  2. const { isArrayBuffer } = require('node:util/types')
  3. const { webidl } = require('../webidl')
  4. const { URLSerializer } = require('../fetch/data-url')
  5. const { environmentSettingsObject } = require('../fetch/util')
  6. const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
  7. const {
  8. isConnecting,
  9. isEstablished,
  10. isClosing,
  11. isClosed,
  12. isValidSubprotocol,
  13. fireEvent,
  14. utf8Decode,
  15. toArrayBuffer,
  16. getURLRecord
  17. } = require('./util')
  18. const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketConnection } = require('./connection')
  19. const { ByteParser } = require('./receiver')
  20. const { kEnumerableProperty } = require('../../core/util')
  21. const { getGlobalDispatcher } = require('../../global')
  22. const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
  23. const { SendQueue } = require('./sender')
  24. const { WebsocketFrameSend } = require('./frame')
  25. const { channels } = require('../../core/diagnostics')
  26. /**
  27. * @typedef {object} Handler
  28. * @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
  29. * @property {(opcode: number, data: Buffer) => void} onMessage
  30. * @property {(error: Error) => void} onParserError
  31. * @property {() => void} onParserDrain
  32. * @property {(chunk: Buffer) => void} onSocketData
  33. * @property {(err: Error) => void} onSocketError
  34. * @property {() => void} onSocketClose
  35. * @property {(body: Buffer) => void} onPing
  36. * @property {(body: Buffer) => void} onPong
  37. *
  38. * @property {number} readyState
  39. * @property {import('stream').Duplex} socket
  40. * @property {Set<number>} closeState
  41. * @property {import('../fetch/index').Fetch} controller
  42. * @property {boolean} [wasEverConnected=false]
  43. */
  44. // https://websockets.spec.whatwg.org/#interface-definition
  45. class WebSocket extends EventTarget {
  46. #events = {
  47. open: null,
  48. error: null,
  49. close: null,
  50. message: null
  51. }
  52. #bufferedAmount = 0
  53. #protocol = ''
  54. #extensions = ''
  55. /** @type {SendQueue} */
  56. #sendQueue
  57. /** @type {Handler} */
  58. #handler = {
  59. onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
  60. onMessage: (opcode, data) => this.#onMessage(opcode, data),
  61. onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
  62. onParserDrain: () => this.#onParserDrain(),
  63. onSocketData: (chunk) => {
  64. if (!this.#parser.write(chunk)) {
  65. this.#handler.socket.pause()
  66. }
  67. },
  68. onSocketError: (err) => {
  69. this.#handler.readyState = states.CLOSING
  70. if (channels.socketError.hasSubscribers) {
  71. channels.socketError.publish(err)
  72. }
  73. this.#handler.socket.destroy()
  74. },
  75. onSocketClose: () => this.#onSocketClose(),
  76. onPing: (body) => {
  77. if (channels.ping.hasSubscribers) {
  78. channels.ping.publish({
  79. payload: body,
  80. websocket: this
  81. })
  82. }
  83. },
  84. onPong: (body) => {
  85. if (channels.pong.hasSubscribers) {
  86. channels.pong.publish({
  87. payload: body,
  88. websocket: this
  89. })
  90. }
  91. },
  92. readyState: states.CONNECTING,
  93. socket: null,
  94. closeState: new Set(),
  95. controller: null,
  96. wasEverConnected: false
  97. }
  98. #url
  99. #binaryType
  100. /** @type {import('./receiver').ByteParser} */
  101. #parser
  102. /**
  103. * @param {string} url
  104. * @param {string|string[]} protocols
  105. */
  106. constructor (url, protocols = []) {
  107. super()
  108. webidl.util.markAsUncloneable(this)
  109. const prefix = 'WebSocket constructor'
  110. webidl.argumentLengthCheck(arguments, 1, prefix)
  111. const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols, prefix, 'options')
  112. url = webidl.converters.USVString(url)
  113. protocols = options.protocols
  114. // 1. Let baseURL be this's relevant settings object's API base URL.
  115. const baseURL = environmentSettingsObject.settingsObject.baseUrl
  116. // 2. Let urlRecord be the result of getting a URL record given url and baseURL.
  117. const urlRecord = getURLRecord(url, baseURL)
  118. // 3. If protocols is a string, set protocols to a sequence consisting
  119. // of just that string.
  120. if (typeof protocols === 'string') {
  121. protocols = [protocols]
  122. }
  123. // 4. If any of the values in protocols occur more than once or otherwise
  124. // fail to match the requirements for elements that comprise the value
  125. // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
  126. // protocol, then throw a "SyntaxError" DOMException.
  127. if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
  128. throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
  129. }
  130. if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
  131. throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
  132. }
  133. // 5. Set this's url to urlRecord.
  134. this.#url = new URL(urlRecord.href)
  135. // 6. Let client be this's relevant settings object.
  136. const client = environmentSettingsObject.settingsObject
  137. // 7. Run this step in parallel:
  138. // 7.1. Establish a WebSocket connection given urlRecord, protocols,
  139. // and client.
  140. this.#handler.controller = establishWebSocketConnection(
  141. urlRecord,
  142. protocols,
  143. client,
  144. this.#handler,
  145. options
  146. )
  147. // Each WebSocket object has an associated ready state, which is a
  148. // number representing the state of the connection. Initially it must
  149. // be CONNECTING (0).
  150. this.#handler.readyState = WebSocket.CONNECTING
  151. // The extensions attribute must initially return the empty string.
  152. // The protocol attribute must initially return the empty string.
  153. // Each WebSocket object has an associated binary type, which is a
  154. // BinaryType. Initially it must be "blob".
  155. this.#binaryType = 'blob'
  156. }
  157. /**
  158. * @see https://websockets.spec.whatwg.org/#dom-websocket-close
  159. * @param {number|undefined} code
  160. * @param {string|undefined} reason
  161. */
  162. close (code = undefined, reason = undefined) {
  163. webidl.brandCheck(this, WebSocket)
  164. const prefix = 'WebSocket.close'
  165. if (code !== undefined) {
  166. code = webidl.converters['unsigned short'](code, prefix, 'code', webidl.attributes.Clamp)
  167. }
  168. if (reason !== undefined) {
  169. reason = webidl.converters.USVString(reason)
  170. }
  171. // 1. If code is the special value "missing", then set code to null.
  172. code ??= null
  173. // 2. If reason is the special value "missing", then set reason to the empty string.
  174. reason ??= ''
  175. // 3. Close the WebSocket with this, code, and reason.
  176. closeWebSocketConnection(this.#handler, code, reason, true)
  177. }
  178. /**
  179. * @see https://websockets.spec.whatwg.org/#dom-websocket-send
  180. * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
  181. */
  182. send (data) {
  183. webidl.brandCheck(this, WebSocket)
  184. const prefix = 'WebSocket.send'
  185. webidl.argumentLengthCheck(arguments, 1, prefix)
  186. data = webidl.converters.WebSocketSendData(data, prefix, 'data')
  187. // 1. If this's ready state is CONNECTING, then throw an
  188. // "InvalidStateError" DOMException.
  189. if (isConnecting(this.#handler.readyState)) {
  190. throw new DOMException('Sent before connected.', 'InvalidStateError')
  191. }
  192. // 2. Run the appropriate set of steps from the following list:
  193. // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1
  194. // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
  195. if (!isEstablished(this.#handler.readyState) || isClosing(this.#handler.readyState)) {
  196. return
  197. }
  198. // If data is a string
  199. if (typeof data === 'string') {
  200. // If the WebSocket connection is established and the WebSocket
  201. // closing handshake has not yet started, then the user agent
  202. // must send a WebSocket Message comprised of the data argument
  203. // using a text frame opcode; if the data cannot be sent, e.g.
  204. // because it would need to be buffered but the buffer is full,
  205. // the user agent must flag the WebSocket as full and then close
  206. // the WebSocket connection. Any invocation of this method with a
  207. // string argument that does not throw an exception must increase
  208. // the bufferedAmount attribute by the number of bytes needed to
  209. // express the argument as UTF-8.
  210. const buffer = Buffer.from(data)
  211. this.#bufferedAmount += buffer.byteLength
  212. this.#sendQueue.add(buffer, () => {
  213. this.#bufferedAmount -= buffer.byteLength
  214. }, sendHints.text)
  215. } else if (isArrayBuffer(data)) {
  216. // If the WebSocket connection is established, and the WebSocket
  217. // closing handshake has not yet started, then the user agent must
  218. // send a WebSocket Message comprised of data using a binary frame
  219. // opcode; if the data cannot be sent, e.g. because it would need
  220. // to be buffered but the buffer is full, the user agent must flag
  221. // the WebSocket as full and then close the WebSocket connection.
  222. // The data to be sent is the data stored in the buffer described
  223. // by the ArrayBuffer object. Any invocation of this method with an
  224. // ArrayBuffer argument that does not throw an exception must
  225. // increase the bufferedAmount attribute by the length of the
  226. // ArrayBuffer in bytes.
  227. this.#bufferedAmount += data.byteLength
  228. this.#sendQueue.add(data, () => {
  229. this.#bufferedAmount -= data.byteLength
  230. }, sendHints.arrayBuffer)
  231. } else if (ArrayBuffer.isView(data)) {
  232. // If the WebSocket connection is established, and the WebSocket
  233. // closing handshake has not yet started, then the user agent must
  234. // send a WebSocket Message comprised of data using a binary frame
  235. // opcode; if the data cannot be sent, e.g. because it would need to
  236. // be buffered but the buffer is full, the user agent must flag the
  237. // WebSocket as full and then close the WebSocket connection. The
  238. // data to be sent is the data stored in the section of the buffer
  239. // described by the ArrayBuffer object that data references. Any
  240. // invocation of this method with this kind of argument that does
  241. // not throw an exception must increase the bufferedAmount attribute
  242. // by the length of data’s buffer in bytes.
  243. this.#bufferedAmount += data.byteLength
  244. this.#sendQueue.add(data, () => {
  245. this.#bufferedAmount -= data.byteLength
  246. }, sendHints.typedArray)
  247. } else if (webidl.is.Blob(data)) {
  248. // If the WebSocket connection is established, and the WebSocket
  249. // closing handshake has not yet started, then the user agent must
  250. // send a WebSocket Message comprised of data using a binary frame
  251. // opcode; if the data cannot be sent, e.g. because it would need to
  252. // be buffered but the buffer is full, the user agent must flag the
  253. // WebSocket as full and then close the WebSocket connection. The data
  254. // to be sent is the raw data represented by the Blob object. Any
  255. // invocation of this method with a Blob argument that does not throw
  256. // an exception must increase the bufferedAmount attribute by the size
  257. // of the Blob object’s raw data, in bytes.
  258. this.#bufferedAmount += data.size
  259. this.#sendQueue.add(data, () => {
  260. this.#bufferedAmount -= data.size
  261. }, sendHints.blob)
  262. }
  263. }
  264. get readyState () {
  265. webidl.brandCheck(this, WebSocket)
  266. // The readyState getter steps are to return this's ready state.
  267. return this.#handler.readyState
  268. }
  269. get bufferedAmount () {
  270. webidl.brandCheck(this, WebSocket)
  271. return this.#bufferedAmount
  272. }
  273. get url () {
  274. webidl.brandCheck(this, WebSocket)
  275. // The url getter steps are to return this's url, serialized.
  276. return URLSerializer(this.#url)
  277. }
  278. get extensions () {
  279. webidl.brandCheck(this, WebSocket)
  280. return this.#extensions
  281. }
  282. get protocol () {
  283. webidl.brandCheck(this, WebSocket)
  284. return this.#protocol
  285. }
  286. get onopen () {
  287. webidl.brandCheck(this, WebSocket)
  288. return this.#events.open
  289. }
  290. set onopen (fn) {
  291. webidl.brandCheck(this, WebSocket)
  292. if (this.#events.open) {
  293. this.removeEventListener('open', this.#events.open)
  294. }
  295. const listener = webidl.converters.EventHandlerNonNull(fn)
  296. if (listener !== null) {
  297. this.addEventListener('open', listener)
  298. this.#events.open = fn
  299. } else {
  300. this.#events.open = null
  301. }
  302. }
  303. get onerror () {
  304. webidl.brandCheck(this, WebSocket)
  305. return this.#events.error
  306. }
  307. set onerror (fn) {
  308. webidl.brandCheck(this, WebSocket)
  309. if (this.#events.error) {
  310. this.removeEventListener('error', this.#events.error)
  311. }
  312. const listener = webidl.converters.EventHandlerNonNull(fn)
  313. if (listener !== null) {
  314. this.addEventListener('error', listener)
  315. this.#events.error = fn
  316. } else {
  317. this.#events.error = null
  318. }
  319. }
  320. get onclose () {
  321. webidl.brandCheck(this, WebSocket)
  322. return this.#events.close
  323. }
  324. set onclose (fn) {
  325. webidl.brandCheck(this, WebSocket)
  326. if (this.#events.close) {
  327. this.removeEventListener('close', this.#events.close)
  328. }
  329. const listener = webidl.converters.EventHandlerNonNull(fn)
  330. if (listener !== null) {
  331. this.addEventListener('close', listener)
  332. this.#events.close = fn
  333. } else {
  334. this.#events.close = null
  335. }
  336. }
  337. get onmessage () {
  338. webidl.brandCheck(this, WebSocket)
  339. return this.#events.message
  340. }
  341. set onmessage (fn) {
  342. webidl.brandCheck(this, WebSocket)
  343. if (this.#events.message) {
  344. this.removeEventListener('message', this.#events.message)
  345. }
  346. const listener = webidl.converters.EventHandlerNonNull(fn)
  347. if (listener !== null) {
  348. this.addEventListener('message', listener)
  349. this.#events.message = fn
  350. } else {
  351. this.#events.message = null
  352. }
  353. }
  354. get binaryType () {
  355. webidl.brandCheck(this, WebSocket)
  356. return this.#binaryType
  357. }
  358. set binaryType (type) {
  359. webidl.brandCheck(this, WebSocket)
  360. if (type !== 'blob' && type !== 'arraybuffer') {
  361. this.#binaryType = 'blob'
  362. } else {
  363. this.#binaryType = type
  364. }
  365. }
  366. /**
  367. * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
  368. */
  369. #onConnectionEstablished (response, parsedExtensions) {
  370. // processResponse is called when the "response’s header list has been received and initialized."
  371. // once this happens, the connection is open
  372. this.#handler.socket = response.socket
  373. const parser = new ByteParser(this.#handler, parsedExtensions)
  374. parser.on('drain', () => this.#handler.onParserDrain())
  375. parser.on('error', (err) => this.#handler.onParserError(err))
  376. this.#parser = parser
  377. this.#sendQueue = new SendQueue(response.socket)
  378. // 1. Change the ready state to OPEN (1).
  379. this.#handler.readyState = states.OPEN
  380. // 2. Change the extensions attribute’s value to the extensions in use, if
  381. // it is not the null value.
  382. // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
  383. const extensions = response.headersList.get('sec-websocket-extensions')
  384. if (extensions !== null) {
  385. this.#extensions = extensions
  386. }
  387. // 3. Change the protocol attribute’s value to the subprotocol in use, if
  388. // it is not the null value.
  389. // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
  390. const protocol = response.headersList.get('sec-websocket-protocol')
  391. if (protocol !== null) {
  392. this.#protocol = protocol
  393. }
  394. // 4. Fire an event named open at the WebSocket object.
  395. fireEvent('open', this)
  396. if (channels.open.hasSubscribers) {
  397. // Convert headers to a plain object for the event
  398. const headers = response.headersList.entries
  399. channels.open.publish({
  400. address: response.socket.address(),
  401. protocol: this.#protocol,
  402. extensions: this.#extensions,
  403. websocket: this,
  404. handshakeResponse: {
  405. status: response.status,
  406. statusText: response.statusText,
  407. headers
  408. }
  409. })
  410. }
  411. }
  412. #onMessage (type, data) {
  413. // 1. If ready state is not OPEN (1), then return.
  414. if (this.#handler.readyState !== states.OPEN) {
  415. return
  416. }
  417. // 2. Let dataForEvent be determined by switching on type and binary type:
  418. let dataForEvent
  419. if (type === opcodes.TEXT) {
  420. // -> type indicates that the data is Text
  421. // a new DOMString containing data
  422. try {
  423. dataForEvent = utf8Decode(data)
  424. } catch {
  425. failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
  426. return
  427. }
  428. } else if (type === opcodes.BINARY) {
  429. if (this.#binaryType === 'blob') {
  430. // -> type indicates that the data is Binary and binary type is "blob"
  431. // a new Blob object, created in the relevant Realm of the WebSocket
  432. // object, that represents data as its raw data
  433. dataForEvent = new Blob([data])
  434. } else {
  435. // -> type indicates that the data is Binary and binary type is "arraybuffer"
  436. // a new ArrayBuffer object, created in the relevant Realm of the
  437. // WebSocket object, whose contents are data
  438. dataForEvent = toArrayBuffer(data)
  439. }
  440. }
  441. // 3. Fire an event named message at the WebSocket object, using MessageEvent,
  442. // with the origin attribute initialized to the serialization of the WebSocket
  443. // object’s url's origin, and the data attribute initialized to dataForEvent.
  444. fireEvent('message', this, createFastMessageEvent, {
  445. origin: this.#url.origin,
  446. data: dataForEvent
  447. })
  448. }
  449. #onParserDrain () {
  450. this.#handler.socket.resume()
  451. }
  452. /**
  453. * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
  454. * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
  455. */
  456. #onSocketClose () {
  457. // If the TCP connection was closed after the
  458. // WebSocket closing handshake was completed, the WebSocket connection
  459. // is said to have been closed _cleanly_.
  460. const wasClean =
  461. this.#handler.closeState.has(sentCloseFrameState.SENT) &&
  462. this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
  463. let code = 1005
  464. let reason = ''
  465. const result = this.#parser?.closingInfo
  466. if (result && !result.error) {
  467. code = result.code ?? 1005
  468. reason = result.reason
  469. }
  470. // 1. Change the ready state to CLOSED (3).
  471. this.#handler.readyState = states.CLOSED
  472. // 2. If the user agent was required to fail the WebSocket
  473. // connection, or if the WebSocket connection was closed
  474. // after being flagged as full, fire an event named error
  475. // at the WebSocket object.
  476. if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
  477. // If _The WebSocket
  478. // Connection is Closed_ and no Close control frame was received by the
  479. // endpoint (such as could occur if the underlying transport connection
  480. // is lost), _The WebSocket Connection Close Code_ is considered to be
  481. // 1006.
  482. code = 1006
  483. fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
  484. error: new TypeError(reason)
  485. })
  486. }
  487. // 3. Fire an event named close at the WebSocket object,
  488. // using CloseEvent, with the wasClean attribute
  489. // initialized to true if the connection closed cleanly
  490. // and false otherwise, the code attribute initialized to
  491. // the WebSocket connection close code, and the reason
  492. // attribute initialized to the result of applying UTF-8
  493. // decode without BOM to the WebSocket connection close
  494. // reason.
  495. // TODO: process.nextTick
  496. fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
  497. wasClean, code, reason
  498. })
  499. if (channels.close.hasSubscribers) {
  500. channels.close.publish({
  501. websocket: this,
  502. code,
  503. reason
  504. })
  505. }
  506. }
  507. /**
  508. * @param {WebSocket} ws
  509. * @param {Buffer|undefined} buffer
  510. */
  511. static ping (ws, buffer) {
  512. if (Buffer.isBuffer(buffer)) {
  513. if (buffer.length > 125) {
  514. throw new TypeError('A PING frame cannot have a body larger than 125 bytes.')
  515. }
  516. } else if (buffer !== undefined) {
  517. throw new TypeError('Expected buffer payload')
  518. }
  519. // An endpoint MAY send a Ping frame any time after the connection is
  520. // established and before the connection is closed.
  521. const readyState = ws.#handler.readyState
  522. if (isEstablished(readyState) && !isClosing(readyState) && !isClosed(readyState)) {
  523. const frame = new WebsocketFrameSend(buffer)
  524. ws.#handler.socket.write(frame.createFrame(opcodes.PING))
  525. }
  526. }
  527. }
  528. const { ping } = WebSocket
  529. Reflect.deleteProperty(WebSocket, 'ping')
  530. // https://websockets.spec.whatwg.org/#dom-websocket-connecting
  531. WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
  532. // https://websockets.spec.whatwg.org/#dom-websocket-open
  533. WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
  534. // https://websockets.spec.whatwg.org/#dom-websocket-closing
  535. WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
  536. // https://websockets.spec.whatwg.org/#dom-websocket-closed
  537. WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
  538. Object.defineProperties(WebSocket.prototype, {
  539. CONNECTING: staticPropertyDescriptors,
  540. OPEN: staticPropertyDescriptors,
  541. CLOSING: staticPropertyDescriptors,
  542. CLOSED: staticPropertyDescriptors,
  543. url: kEnumerableProperty,
  544. readyState: kEnumerableProperty,
  545. bufferedAmount: kEnumerableProperty,
  546. onopen: kEnumerableProperty,
  547. onerror: kEnumerableProperty,
  548. onclose: kEnumerableProperty,
  549. close: kEnumerableProperty,
  550. onmessage: kEnumerableProperty,
  551. binaryType: kEnumerableProperty,
  552. send: kEnumerableProperty,
  553. extensions: kEnumerableProperty,
  554. protocol: kEnumerableProperty,
  555. [Symbol.toStringTag]: {
  556. value: 'WebSocket',
  557. writable: false,
  558. enumerable: false,
  559. configurable: true
  560. }
  561. })
  562. Object.defineProperties(WebSocket, {
  563. CONNECTING: staticPropertyDescriptors,
  564. OPEN: staticPropertyDescriptors,
  565. CLOSING: staticPropertyDescriptors,
  566. CLOSED: staticPropertyDescriptors
  567. })
  568. webidl.converters['sequence<DOMString>'] = webidl.sequenceConverter(
  569. webidl.converters.DOMString
  570. )
  571. webidl.converters['DOMString or sequence<DOMString>'] = function (V, prefix, argument) {
  572. if (webidl.util.Type(V) === webidl.util.Types.OBJECT && Symbol.iterator in V) {
  573. return webidl.converters['sequence<DOMString>'](V)
  574. }
  575. return webidl.converters.DOMString(V, prefix, argument)
  576. }
  577. // This implements the proposal made in https://github.com/whatwg/websockets/issues/42
  578. webidl.converters.WebSocketInit = webidl.dictionaryConverter([
  579. {
  580. key: 'protocols',
  581. converter: webidl.converters['DOMString or sequence<DOMString>'],
  582. defaultValue: () => []
  583. },
  584. {
  585. key: 'dispatcher',
  586. converter: webidl.converters.any,
  587. defaultValue: () => getGlobalDispatcher()
  588. },
  589. {
  590. key: 'headers',
  591. converter: webidl.nullableConverter(webidl.converters.HeadersInit)
  592. }
  593. ])
  594. webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) {
  595. if (webidl.util.Type(V) === webidl.util.Types.OBJECT && !(Symbol.iterator in V)) {
  596. return webidl.converters.WebSocketInit(V)
  597. }
  598. return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) }
  599. }
  600. webidl.converters.WebSocketSendData = function (V) {
  601. if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
  602. if (webidl.is.Blob(V)) {
  603. return V
  604. }
  605. if (webidl.is.BufferSource(V)) {
  606. return V
  607. }
  608. }
  609. return webidl.converters.USVString(V)
  610. }
  611. module.exports = {
  612. WebSocket,
  613. ping
  614. }