From 0d473b73dee15337bac2571fa02e9a20a05355d7 Mon Sep 17 00:00:00 2001 From: Bryce Kahle Date: Sun, 27 Feb 2022 14:34:12 -0800 Subject: [PATCH 1/2] Move to ES modules --- .eslintignore | 4 - .eslintrc | 17 - lib/entry.js | 7 +- lib/event/close.js | 26 +- lib/event/emitter.js | 82 +- lib/event/event.js | 35 +- lib/event/eventtarget.js | 98 +- lib/event/trans-message.js | 22 +- lib/facade.js | 46 +- lib/iframe-bootstrap.js | 147 +- lib/info-ajax.js | 78 +- lib/info-iframe-receiver.js | 46 +- lib/info-iframe.js | 115 +- lib/info-receiver.js | 151 +- lib/location.js | 17 +- lib/main.js | 667 +- lib/shims.js | 452 - lib/transport-list.js | 41 +- lib/transport/browser/abstract-xhr.js | 341 +- lib/transport/browser/eventsource.js | 2 +- lib/transport/browser/websocket.js | 11 +- lib/transport/driver/eventsource.js | 3 +- lib/transport/driver/websocket.js | 3 +- lib/transport/driver/xhr.js | 108 +- lib/transport/eventsource.js | 38 +- lib/transport/htmlfile.js | 35 +- lib/transport/iframe.js | 237 +- lib/transport/jsonp-polling.js | 39 +- lib/transport/lib/ajax-based.js | 44 +- lib/transport/lib/buffered-sender.js | 142 +- lib/transport/lib/iframe-wrap.js | 46 +- lib/transport/lib/polling.js | 96 +- lib/transport/lib/sender-receiver.js | 72 +- lib/transport/receiver/eventsource.js | 107 +- lib/transport/receiver/htmlfile.js | 143 +- lib/transport/receiver/jsonp.js | 312 +- lib/transport/receiver/xhr.js | 107 +- lib/transport/sender/jsonp.js | 79 +- lib/transport/sender/xdr.js | 170 +- lib/transport/sender/xhr-cors.js | 16 +- lib/transport/sender/xhr-fake.js | 33 +- lib/transport/sender/xhr-local.js | 22 +- lib/transport/websocket.js | 170 +- lib/transport/xdr-polling.js | 33 +- lib/transport/xdr-streaming.js | 44 +- lib/transport/xhr-polling.js | 46 +- lib/transport/xhr-streaming.js | 66 +- lib/utils/browser-crypto.js | 20 +- lib/utils/browser.js | 40 +- lib/utils/debug.js | 10 + lib/utils/escape.js | 52 +- lib/utils/event.js | 99 +- lib/utils/iframe.js | 353 +- lib/utils/log.js | 20 +- lib/utils/object.js | 30 +- lib/utils/random.js | 41 +- lib/utils/transport.js | 44 +- lib/utils/url.js | 75 +- lib/version.js | 1 - package-lock.json | 15757 +++++++++++++++++------- package.json | 18 +- 61 files changed, 13538 insertions(+), 7638 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc delete mode 100644 lib/shims.js create mode 100644 lib/utils/debug.js delete mode 100644 lib/version.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 05bc36cc..00000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -dist/* -build/* -tests/html/lib/sockjs.js -tests/html/static/* diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 40602abb..00000000 --- a/.eslintrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": ["eslint:recommended"], - "env": { - "node": true, - "browser": true - }, - "rules": { - "consistent-this": ["error", "self"], - "quotes": ["warn", "single", "avoid-escape"], - "no-space-before-semi": "off", - "no-underscore-dangle": "off", - "no-mixed-requires": "off" - }, - "globals": { - "Uint8Array": true - } -} diff --git a/lib/entry.js b/lib/entry.js index 68eb23e9..4a4297f2 100644 --- a/lib/entry.js +++ b/lib/entry.js @@ -1,8 +1,7 @@ -'use strict'; +import transportList from './transport-list.js'; +import main from './main.js'; -var transportList = require('./transport-list'); - -module.exports = require('./main')(transportList); +export default main(transportList); // TODO can't get rid of this until all servers do if ('_sockjs_onload' in global) { diff --git a/lib/event/close.js b/lib/event/close.js index 755a92e0..7a355edb 100644 --- a/lib/event/close.js +++ b/lib/event/close.js @@ -1,17 +1,13 @@ -'use strict'; - -var inherits = require('inherits') - , Event = require('./event') - ; - -function CloseEvent() { - Event.call(this); - this.initEvent('close', false, false); - this.wasClean = false; - this.code = 0; - this.reason = ''; +import Event from './event.js'; + +class CloseEvent extends Event { + constructor() { + super('close'); + this.initEvent(false, false); + this.wasClean = false; + this.code = 0; + this.reason = ''; + } } -inherits(CloseEvent, Event); - -module.exports = CloseEvent; +export default CloseEvent; diff --git a/lib/event/emitter.js b/lib/event/emitter.js index 798459db..d9f46c44 100644 --- a/lib/event/emitter.js +++ b/lib/event/emitter.js @@ -1,57 +1,51 @@ -'use strict'; +import EventTarget from './eventtarget.js'; + +class EventEmitter extends EventTarget { + removeAllListeners(type) { + if (type) { + delete this._listeners[type]; + } else { + this._listeners = {}; + } + } -var inherits = require('inherits') - , EventTarget = require('./eventtarget') - ; + once(type, listener) { + let fired = false; -function EventEmitter() { - EventTarget.call(this); -} + const g = function (...args) { + this.removeListener(type, g); -inherits(EventEmitter, EventTarget); + if (!fired) { + fired = true; + Reflect.apply(listener, this, args); + } + }.bind(this); -EventEmitter.prototype.removeAllListeners = function(type) { - if (type) { - delete this._listeners[type]; - } else { - this._listeners = {}; + this.on(type, g); } -}; -EventEmitter.prototype.once = function(type, listener) { - var self = this - , fired = false; - - function g() { - self.removeListener(type, g); - - if (!fired) { - fired = true; - listener.apply(this, arguments); + emit(...args) { + const type = args[0]; + const listeners = this._listeners[type]; + if (!listeners) { + return; } - } - this.on(type, g); -}; + // Equivalent of Array.prototype.slice.call(arguments, 1); + const l = args.length; + const copyArgs = Array.from({length: l - 1}); + for (let ai = 1; ai < l; ai++) { + copyArgs[ai - 1] = args[ai]; + } -EventEmitter.prototype.emit = function() { - var type = arguments[0]; - var listeners = this._listeners[type]; - if (!listeners) { - return; - } - // equivalent of Array.prototype.slice.call(arguments, 1); - var l = arguments.length; - var args = new Array(l - 1); - for (var ai = 1; ai < l; ai++) { - args[ai - 1] = arguments[ai]; - } - for (var i = 0; i < listeners.length; i++) { - listeners[i].apply(this, args); + for (const listener of listeners) { + listener.apply(this, copyArgs); + } } -}; +} -EventEmitter.prototype.on = EventEmitter.prototype.addListener = EventTarget.prototype.addEventListener; +EventEmitter.prototype.addListener = EventTarget.prototype.addEventListener; +EventEmitter.prototype.on = EventTarget.prototype.addEventListener; EventEmitter.prototype.removeListener = EventTarget.prototype.removeEventListener; -module.exports.EventEmitter = EventEmitter; +export {EventEmitter}; diff --git a/lib/event/event.js b/lib/event/event.js index dfb35446..de8d3da0 100644 --- a/lib/event/event.js +++ b/lib/event/event.js @@ -1,22 +1,21 @@ -'use strict'; +class Event { + constructor(eventType) { + this.type = eventType; + } -function Event(eventType) { - this.type = eventType; -} - -Event.prototype.initEvent = function(eventType, canBubble, cancelable) { - this.type = eventType; - this.bubbles = canBubble; - this.cancelable = cancelable; - this.timeStamp = +new Date(); - return this; -}; + static CAPTURING_PHASE = 1; + static AT_TARGET = 2; + static BUBBLING_PHASE = 3; -Event.prototype.stopPropagation = function() {}; -Event.prototype.preventDefault = function() {}; + initEvent(canBubble, cancelable) { + this.bubbles = canBubble; + this.cancelable = cancelable; + this.timeStamp = Date.now(); + return this; + } -Event.CAPTURING_PHASE = 1; -Event.AT_TARGET = 2; -Event.BUBBLING_PHASE = 3; + stopPropagation() {} + preventDefault() {} +} -module.exports = Event; +export default Event; diff --git a/lib/event/eventtarget.js b/lib/event/eventtarget.js index 7bd81f3f..f243b774 100644 --- a/lib/event/eventtarget.js +++ b/lib/event/eventtarget.js @@ -1,62 +1,62 @@ -'use strict'; - /* Simplified implementation of DOM2 EventTarget. * http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget */ -function EventTarget() { - this._listeners = {}; -} - -EventTarget.prototype.addEventListener = function(eventType, listener) { - if (!(eventType in this._listeners)) { - this._listeners[eventType] = []; - } - var arr = this._listeners[eventType]; - // #4 - if (arr.indexOf(listener) === -1) { - // Make a copy so as not to interfere with a current dispatchEvent. - arr = arr.concat([listener]); +class EventTarget { + constructor() { + this._listeners = {}; } - this._listeners[eventType] = arr; -}; -EventTarget.prototype.removeEventListener = function(eventType, listener) { - var arr = this._listeners[eventType]; - if (!arr) { - return; - } - var idx = arr.indexOf(listener); - if (idx !== -1) { - if (arr.length > 1) { + addEventListener(eventType, listener) { + if (!(eventType in this._listeners)) { + this._listeners[eventType] = []; + } + + let array = this._listeners[eventType]; + // #4 + if (!array.includes(listener)) { // Make a copy so as not to interfere with a current dispatchEvent. - this._listeners[eventType] = arr.slice(0, idx).concat(arr.slice(idx + 1)); - } else { - delete this._listeners[eventType]; + array = [...array, listener]; } - return; + + this._listeners[eventType] = array; } -}; - -EventTarget.prototype.dispatchEvent = function() { - var event = arguments[0]; - var t = event.type; - // equivalent of Array.prototype.slice.call(arguments, 0); - var args = arguments.length === 1 ? [event] : Array.apply(null, arguments); - // TODO: This doesn't match the real behavior; per spec, onfoo get - // their place in line from the /first/ time they're set from - // non-null. Although WebKit bumps it to the end every time it's - // set. - if (this['on' + t]) { - this['on' + t].apply(this, args); + + removeEventListener(eventType, listener) { + const array = this._listeners[eventType]; + if (!array) { + return; + } + + const idx = array.indexOf(listener); + if (idx !== -1) { + if (array.length > 1) { + // Make a copy so as not to interfere with a current dispatchEvent. + this._listeners[eventType] = [...array.slice(0, idx), ...array.slice(idx + 1)]; + } else { + delete this._listeners[eventType]; + } + } } - if (t in this._listeners) { - // Grab a reference to the listeners list. removeEventListener may alter the list. - var listeners = this._listeners[t]; - for (var i = 0; i < listeners.length; i++) { - listeners[i].apply(this, args); + + dispatchEvent(...args) { + const t = args[0].type; + // TODO: This doesn't match the real behavior; per spec, onfoo get + // their place in line from the /first/ time they're set from + // non-null. Although WebKit bumps it to the end every time it's + // set. + if (this['on' + t]) { + this['on' + t](...args); + } + + if (t in this._listeners) { + // Grab a reference to the listeners list. removeEventListener may alter the list. + const listeners = this._listeners[t]; + for (const listener of listeners) { + listener.apply(this, args); + } } } -}; +} -module.exports = EventTarget; +export default EventTarget; diff --git a/lib/event/trans-message.js b/lib/event/trans-message.js index 9f5d8f47..b4668796 100644 --- a/lib/event/trans-message.js +++ b/lib/event/trans-message.js @@ -1,15 +1,11 @@ -'use strict'; - -var inherits = require('inherits') - , Event = require('./event') - ; - -function TransportMessageEvent(data) { - Event.call(this); - this.initEvent('message', false, false); - this.data = data; +import Event from './event.js'; + +class TransportMessageEvent extends Event { + constructor(data) { + super('message'); + this.initEvent(false, false); + this.data = data; + } } -inherits(TransportMessageEvent, Event); - -module.exports = TransportMessageEvent; +export default TransportMessageEvent; diff --git a/lib/facade.js b/lib/facade.js index f2718dde..5efe8a52 100644 --- a/lib/facade.js +++ b/lib/facade.js @@ -1,26 +1,28 @@ -'use strict'; +import {postMessage} from './utils/iframe.js'; -var iframeUtils = require('./utils/iframe') - ; +class FacadeJS { + constructor(transport) { + this._transport = transport; + transport.on('message', this._transportMessage.bind(this)); + transport.on('close', this._transportClose.bind(this)); + } -function FacadeJS(transport) { - this._transport = transport; - transport.on('message', this._transportMessage.bind(this)); - transport.on('close', this._transportClose.bind(this)); -} + _transportClose(code, reason) { + postMessage('c', JSON.stringify([code, reason])); + } + + _transportMessage(frame) { + postMessage('t', frame); + } -FacadeJS.prototype._transportClose = function(code, reason) { - iframeUtils.postMessage('c', JSON.stringify([code, reason])); -}; -FacadeJS.prototype._transportMessage = function(frame) { - iframeUtils.postMessage('t', frame); -}; -FacadeJS.prototype._send = function(data) { - this._transport.send(data); -}; -FacadeJS.prototype._close = function() { - this._transport.close(); - this._transport.removeAllListeners(); -}; + _send(data) { + this._transport.send(data); + } + + _close() { + this._transport.close(); + this._transport.removeAllListeners(); + } +} -module.exports = FacadeJS; +export default FacadeJS; diff --git a/lib/iframe-bootstrap.js b/lib/iframe-bootstrap.js index 0587b046..d8e9f150 100644 --- a/lib/iframe-bootstrap.js +++ b/lib/iframe-bootstrap.js @@ -1,101 +1,108 @@ -'use strict'; - -var urlUtils = require('./utils/url') - , eventUtils = require('./utils/event') - , FacadeJS = require('./facade') - , InfoIframeReceiver = require('./info-iframe-receiver') - , iframeUtils = require('./utils/iframe') - , loc = require('./location') - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:iframe-bootstrap'); -} +import FacadeJS from './facade.js'; +import {postMessage, currentWindowId, setCurrentWindowId} from './utils/iframe.js'; +import {attachEvent} from './utils/event.js'; +import {isOriginEqual} from './utils/url.js'; +import InfoIframeReceiver from './info-iframe-receiver.js'; +import loc from './location.js'; +import debugFunc from './utils/debug.js'; + +const debug = debugFunc('sockjs-client:iframe-bootstrap'); -module.exports = function(SockJS, availableTransports) { - var transportMap = {}; - availableTransports.forEach(function(at) { +export default function createSockJs(SockJS, availableTransports) { + const transportMap = {}; + for (const at of availableTransports) { if (at.facadeTransport) { transportMap[at.facadeTransport.transportName] = at.facadeTransport; } - }); + } - // hard-coded for the info iframe + // Hard-coded for the info iframe // TODO see if we can make this more dynamic transportMap[InfoIframeReceiver.transportName] = InfoIframeReceiver; - var parentOrigin; + let parentOrigin; /* eslint-disable camelcase */ - SockJS.bootstrap_iframe = function() { + SockJS.bootstrap_iframe = function () { /* eslint-enable camelcase */ - var facade; - iframeUtils.currentWindowId = loc.hash.slice(1); - var onMessage = function(e) { - if (e.source !== parent) { + /* eslint-env browser */ + let facade; + setCurrentWindowId(loc.hash.slice(1)); + const onMessage = function (evt) { + if (evt.source !== parent) { return; } + if (typeof parentOrigin === 'undefined') { - parentOrigin = e.origin; + parentOrigin = evt.origin; } - if (e.origin !== parentOrigin) { + + if (evt.origin !== parentOrigin) { return; } - var iframeMessage; + let iframeMessage; try { - iframeMessage = JSON.parse(e.data); - } catch (ignored) { - debug('bad json', e.data); + iframeMessage = JSON.parse(evt.data); + } catch { + debug('bad json', evt.data); return; } - if (iframeMessage.windowId !== iframeUtils.currentWindowId) { + if (iframeMessage.windowId !== currentWindowId) { return; } + switch (iframeMessage.type) { - case 's': - var p; - try { - p = JSON.parse(iframeMessage.data); - } catch (ignored) { - debug('bad json', iframeMessage.data); + case 's': + { + let p; + try { + p = JSON.parse(iframeMessage.data); + } catch { + debug('bad json', iframeMessage.data); + break; + } + + const version = p[0]; + const transport = p[1]; + const transUrl = p[2]; + const baseUrl = p[3]; + debug(version, transport, transUrl, baseUrl); + // Change this to semver logic + if (version !== SockJS.version) { + throw new Error('Incompatible SockJS! Main site uses:' + + ' "' + version + '", the iframe:' + + ' "' + SockJS.version + '".'); + } + + if (!isOriginEqual(transUrl, loc.href) + || !isOriginEqual(baseUrl, loc.href)) { + throw new Error('Can\'t connect to different domain from within an ' + + 'iframe. (' + loc.href + ', ' + transUrl + ', ' + baseUrl + ')'); + } + + facade = new FacadeJS(new transportMap[transport](transUrl, baseUrl)); + } + + break; + case 'm': + facade._send(iframeMessage.data); + break; + case 'c': + if (facade) { + facade._close(); + } + + facade = null; + break; + default: break; - } - var version = p[0]; - var transport = p[1]; - var transUrl = p[2]; - var baseUrl = p[3]; - debug(version, transport, transUrl, baseUrl); - // change this to semver logic - if (version !== SockJS.version) { - throw new Error('Incompatible SockJS! Main site uses:' + - ' "' + version + '", the iframe:' + - ' "' + SockJS.version + '".'); - } - - if (!urlUtils.isOriginEqual(transUrl, loc.href) || - !urlUtils.isOriginEqual(baseUrl, loc.href)) { - throw new Error('Can\'t connect to different domain from within an ' + - 'iframe. (' + loc.href + ', ' + transUrl + ', ' + baseUrl + ')'); - } - facade = new FacadeJS(new transportMap[transport](transUrl, baseUrl)); - break; - case 'm': - facade._send(iframeMessage.data); - break; - case 'c': - if (facade) { - facade._close(); - } - facade = null; - break; } }; - eventUtils.attachEvent('message', onMessage); + attachEvent('message', onMessage); // Start - iframeUtils.postMessage('s'); + postMessage('s'); }; -}; +} diff --git a/lib/info-ajax.js b/lib/info-ajax.js index 85a5c344..7875aac2 100644 --- a/lib/info-ajax.js +++ b/lib/info-ajax.js @@ -1,48 +1,42 @@ -'use strict'; - -var EventEmitter = require('events').EventEmitter - , inherits = require('inherits') - , objectUtils = require('./utils/object') - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:info-ajax'); -} - -function InfoAjax(url, AjaxObject) { - EventEmitter.call(this); - - var self = this; - var t0 = +new Date(); - this.xo = new AjaxObject('GET', url); +import {EventEmitter} from 'node:events'; +import debugFunc from './utils/debug.js'; +import {isObject} from './utils/object.js'; + +const debug = debugFunc('sockjs-client:info-ajax'); + +class InfoAjax extends EventEmitter { + constructor(url, AjaxObject) { + super(); + const t0 = Date.now(); + this.xo = new AjaxObject('GET', url); + + this.xo.once('finish', (status, text) => { + let info; + let rtt; + if (status === 200) { + rtt = Date.now() - t0; + if (text) { + try { + info = JSON.parse(text); + } catch { + debug('bad json', text); + } + } - this.xo.once('finish', function(status, text) { - var info, rtt; - if (status === 200) { - rtt = (+new Date()) - t0; - if (text) { - try { - info = JSON.parse(text); - } catch (e) { - debug('bad json', text); + if (!isObject(info)) { + info = {}; } } - if (!objectUtils.isObject(info)) { - info = {}; - } - } - self.emit('finish', info, rtt); - self.removeAllListeners(); - }); -} + this.emit('finish', info, rtt); + this.removeAllListeners(); + }); + } -inherits(InfoAjax, EventEmitter); - -InfoAjax.prototype.close = function() { - this.removeAllListeners(); - this.xo.close(); -}; + close() { + this.removeAllListeners(); + this.xo.close(); + } +} -module.exports = InfoAjax; +export default InfoAjax; diff --git a/lib/info-iframe-receiver.js b/lib/info-iframe-receiver.js index 70cbc9b4..d7597f23 100644 --- a/lib/info-iframe-receiver.js +++ b/lib/info-iframe-receiver.js @@ -1,32 +1,28 @@ -'use strict'; +import {EventEmitter} from 'node:events'; +import XHRLocalObject from './transport/sender/xhr-local.js'; +import InfoAjax from './info-ajax.js'; -var inherits = require('inherits') - , EventEmitter = require('events').EventEmitter - , XHRLocalObject = require('./transport/sender/xhr-local') - , InfoAjax = require('./info-ajax') - ; +class InfoReceiverIframe extends EventEmitter { + constructor(transUrl) { + super(); -function InfoReceiverIframe(transUrl) { - var self = this; - EventEmitter.call(this); - - this.ir = new InfoAjax(transUrl, XHRLocalObject); - this.ir.once('finish', function(info, rtt) { - self.ir = null; - self.emit('message', JSON.stringify([info, rtt])); - }); -} + this.ir = new InfoAjax(transUrl, XHRLocalObject); + this.ir.once('finish', (info, rtt) => { + this.ir = null; + this.emit('message', JSON.stringify([info, rtt])); + }); + } -inherits(InfoReceiverIframe, EventEmitter); + static transportName = 'iframe-info-receiver'; -InfoReceiverIframe.transportName = 'iframe-info-receiver'; + close() { + if (this.ir) { + this.ir.close(); + this.ir = null; + } -InfoReceiverIframe.prototype.close = function() { - if (this.ir) { - this.ir.close(); - this.ir = null; + this.removeAllListeners(); } - this.removeAllListeners(); -}; +} -module.exports = InfoReceiverIframe; +export default InfoReceiverIframe; diff --git a/lib/info-iframe.js b/lib/info-iframe.js index 81c4acb9..40b717fe 100644 --- a/lib/info-iframe.js +++ b/lib/info-iframe.js @@ -1,68 +1,63 @@ -'use strict'; - -var EventEmitter = require('events').EventEmitter - , inherits = require('inherits') - , utils = require('./utils/event') - , IframeTransport = require('./transport/iframe') - , InfoReceiverIframe = require('./info-iframe-receiver') - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:info-iframe'); -} - -function InfoIframe(baseUrl, url) { - var self = this; - EventEmitter.call(this); - - var go = function() { - var ifr = self.ifr = new IframeTransport(InfoReceiverIframe.transportName, url, baseUrl); - - ifr.once('message', function(msg) { - if (msg) { - var d; - try { - d = JSON.parse(msg); - } catch (e) { - debug('bad json', msg); - self.emit('finish'); - self.close(); - return; +import {EventEmitter} from 'node:events'; +import debugFunc from './utils/debug.js'; +import IframeTransport from './transport/iframe.js'; +import InfoReceiverIframe from './info-iframe-receiver.js'; +import {attachEvent} from './utils/event.js'; + +const debug = debugFunc('sockjs-client:info-iframe'); + +class InfoIframe extends EventEmitter { + constructor(baseUrl, url) { + super(); + + const go = () => { + this.ifr = new IframeTransport(InfoReceiverIframe.transportName, url, baseUrl); + this.ifr.once('message', message => { + if (message) { + let d; + try { + d = JSON.parse(message); + } catch { + debug('bad json', message); + this.emit('finish'); + this.close(); + return; + } + + const info = d[0]; + const rtt = d[1]; + this.emit('finish', info, rtt); } - var info = d[0], rtt = d[1]; - self.emit('finish', info, rtt); - } - self.close(); - }); - - ifr.once('close', function() { - self.emit('finish'); - self.close(); - }); - }; - - // TODO this seems the same as the 'needBody' from transports - if (!global.document.body) { - utils.attachEvent('load', go); - } else { - go(); + this.close(); + }); + + this.ifr.once('close', () => { + this.emit('finish'); + this.close(); + }); + }; + + // TODO this seems the same as the 'needBody' from transports + if (global.document.body) { + go(); + } else { + attachEvent('load', go); + } } -} -inherits(InfoIframe, EventEmitter); + static enabled() { + return IframeTransport.enabled(); + } -InfoIframe.enabled = function() { - return IframeTransport.enabled(); -}; + close() { + if (this.ifr) { + this.ifr.close(); + } -InfoIframe.prototype.close = function() { - if (this.ifr) { - this.ifr.close(); + this.removeAllListeners(); + this.ifr = null; } - this.removeAllListeners(); - this.ifr = null; -}; +} -module.exports = InfoIframe; +export default InfoIframe; diff --git a/lib/info-receiver.js b/lib/info-receiver.js index afefea5c..c18fefbe 100644 --- a/lib/info-receiver.js +++ b/lib/info-receiver.js @@ -1,89 +1,84 @@ -'use strict'; - -var EventEmitter = require('events').EventEmitter - , inherits = require('inherits') - , urlUtils = require('./utils/url') - , XDR = require('./transport/sender/xdr') - , XHRCors = require('./transport/sender/xhr-cors') - , XHRLocal = require('./transport/sender/xhr-local') - , XHRFake = require('./transport/sender/xhr-fake') - , InfoIframe = require('./info-iframe') - , InfoAjax = require('./info-ajax') - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:info-receiver'); -} +import {EventEmitter} from 'node:events'; +import debugFunc from './utils/debug.js'; +import XDR from './transport/sender/xdr.js'; +import XHRCors from './transport/sender/xhr-cors.js'; +import XHRLocal from './transport/sender/xhr-local.js'; +import XHRFake from './transport/sender/xhr-fake.js'; +import InfoIframe from './info-iframe.js'; +import InfoAjax from './info-ajax.js'; +import {addPath} from './utils/url.js'; -function InfoReceiver(baseUrl, urlInfo) { - debug(baseUrl); - var self = this; - EventEmitter.call(this); +const debug = debugFunc('sockjs-client:info-receiver'); - setTimeout(function() { - self.doXhr(baseUrl, urlInfo); - }, 0); -} +class InfoReceiver extends EventEmitter { + constructor(baseUrl, urlInfo) { + debug(baseUrl); + super(); -inherits(InfoReceiver, EventEmitter); + setTimeout(() => { + this.doXhr(baseUrl, urlInfo); + }, 0); + } -// TODO this is currently ignoring the list of available transports and the whitelist + // TODO this is currently ignoring the list of available transports and the whitelist + static _getReceiver(baseUrl, url, urlInfo) { + // Determine method of CORS support (if needed) + if (urlInfo.sameOrigin) { + return new InfoAjax(url, XHRLocal); + } -InfoReceiver._getReceiver = function(baseUrl, url, urlInfo) { - // determine method of CORS support (if needed) - if (urlInfo.sameOrigin) { - return new InfoAjax(url, XHRLocal); - } - if (XHRCors.enabled) { - return new InfoAjax(url, XHRCors); - } - if (XDR.enabled && urlInfo.sameScheme) { - return new InfoAjax(url, XDR); + if (XHRCors.enabled) { + return new InfoAjax(url, XHRCors); + } + + if (XDR.enabled && urlInfo.sameScheme) { + return new InfoAjax(url, XDR); + } + + if (InfoIframe.enabled()) { + return new InfoIframe(baseUrl, url); + } + + return new InfoAjax(url, XHRFake); } - if (InfoIframe.enabled()) { - return new InfoIframe(baseUrl, url); + + doXhr(baseUrl, urlInfo) { + const url = addPath(baseUrl, '/info'); + debug('doXhr', url); + + this.xo = InfoReceiver._getReceiver(baseUrl, url, urlInfo); + + this.timeoutRef = setTimeout(() => { + debug('timeout'); + this._cleanup(false); + this.emit('finish'); + }, InfoReceiver.timeout); + + this.xo.once('finish', (info, rtt) => { + debug('finish', info, rtt); + this._cleanup(true); + this.emit('finish', info, rtt); + }); } - return new InfoAjax(url, XHRFake); -}; - -InfoReceiver.prototype.doXhr = function(baseUrl, urlInfo) { - var self = this - , url = urlUtils.addPath(baseUrl, '/info') - ; - debug('doXhr', url); - - this.xo = InfoReceiver._getReceiver(baseUrl, url, urlInfo); - - this.timeoutRef = setTimeout(function() { - debug('timeout'); - self._cleanup(false); - self.emit('finish'); - }, InfoReceiver.timeout); - - this.xo.once('finish', function(info, rtt) { - debug('finish', info, rtt); - self._cleanup(true); - self.emit('finish', info, rtt); - }); -}; - -InfoReceiver.prototype._cleanup = function(wasClean) { - debug('_cleanup'); - clearTimeout(this.timeoutRef); - this.timeoutRef = null; - if (!wasClean && this.xo) { - this.xo.close(); + + _cleanup(wasClean) { + debug('_cleanup'); + clearTimeout(this.timeoutRef); + this.timeoutRef = null; + if (!wasClean && this.xo) { + this.xo.close(); + } + + this.xo = null; } - this.xo = null; -}; -InfoReceiver.prototype.close = function() { - debug('close'); - this.removeAllListeners(); - this._cleanup(false); -}; + close() { + debug('close'); + this.removeAllListeners(); + this._cleanup(false); + } -InfoReceiver.timeout = 8000; + static timeout = 8000; +} -module.exports = InfoReceiver; +export default InfoReceiver; diff --git a/lib/location.js b/lib/location.js index be64fe81..7677323b 100644 --- a/lib/location.js +++ b/lib/location.js @@ -1,10 +1,9 @@ -'use strict'; - -module.exports = global.location || { - origin: 'http://localhost:80' -, protocol: 'http:' -, host: 'localhost' -, port: 80 -, href: 'http://localhost/' -, hash: '' +const loc = global.location || { + origin: 'http://localhost:80', + protocol: 'http:', + host: 'localhost', + port: 80, + href: 'http://localhost/', + hash: '', }; +export default loc; diff --git a/lib/main.js b/lib/main.js index 90dc0428..12c81783 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,388 +1,397 @@ -'use strict'; - -require('./shims'); - -var URL = require('url-parse') - , inherits = require('inherits') - , random = require('./utils/random') - , escape = require('./utils/escape') - , urlUtils = require('./utils/url') - , eventUtils = require('./utils/event') - , transport = require('./utils/transport') - , objectUtils = require('./utils/object') - , browser = require('./utils/browser') - , log = require('./utils/log') - , Event = require('./event/event') - , EventTarget = require('./event/eventtarget') - , loc = require('./location') - , CloseEvent = require('./event/close') - , TransportMessageEvent = require('./event/trans-message') - , InfoReceiver = require('./info-receiver') - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:main'); -} +import {URL} from 'url-parse'; +import {version} from '../package.json'; +import debugFunc from './utils/debug.js'; +import Event from './event/event.js'; +import EventTarget from './event/eventtarget.js'; +import CloseEvent from './event/close.js'; +import TransportMessageEvent from './event/trans-message.js'; +import InfoReceiver from './info-receiver.js'; +import loc from './location.js'; +import log from './utils/log.js'; +import {string as randomString, numberString as randomNumberString} from './utils/random.js'; +import {quote} from './utils/escape.js'; +import * as urlUtils from './utils/url.js'; +import {attachEvent} from './utils/event.js'; +import transport from './utils/transport.js'; +import {extend} from './utils/object.js'; +import {hasDomain} from './utils/browser.js'; +import iframeBootstrap from './iframe-bootstrap.js'; + +const debug = debugFunc('sockjs-client:main'); + +let transports; + +// Follow constructor steps defined at http://dev.w3.org/html5/websockets/#the-websocket-interface +class SockJS extends EventTarget { + // eslint-disable-next-line complexity + constructor(url, protocols, options) { + if (arguments.length === 0) { + throw new TypeError('Failed to construct \'SockJS: 1 argument required, but only 0 present'); + } -var transports; + super(); -// follow constructor steps defined at http://dev.w3.org/html5/websockets/#the-websocket-interface -function SockJS(url, protocols, options) { - if (!(this instanceof SockJS)) { - return new SockJS(url, protocols, options); - } - if (arguments.length < 1) { - throw new TypeError("Failed to construct 'SockJS: 1 argument required, but only 0 present"); - } - EventTarget.call(this); + this.readyState = SockJS.CONNECTING; + this.extensions = ''; + this.protocol = ''; - this.readyState = SockJS.CONNECTING; - this.extensions = ''; - this.protocol = ''; + // Non-standard extension + options = options || {}; + if (options.protocols_whitelist) { + log.warn('\'protocols_whitelist\' is DEPRECATED. Use \'transports\' instead.'); + } - // non-standard extension - options = options || {}; - if (options.protocols_whitelist) { - log.warn("'protocols_whitelist' is DEPRECATED. Use 'transports' instead."); - } - this._transportsWhitelist = options.transports; - this._transportOptions = options.transportOptions || {}; - this._timeout = options.timeout || 0; - - var sessionId = options.sessionId || 8; - if (typeof sessionId === 'function') { - this._generateSessionId = sessionId; - } else if (typeof sessionId === 'number') { - this._generateSessionId = function() { - return random.string(sessionId); - }; - } else { - throw new TypeError('If sessionId is used in the options, it needs to be a number or a function.'); - } + this._transportsWhitelist = options.transports; + this._transportOptions = options.transportOptions || {}; + this._timeout = options.timeout || 0; + + const sessionId = options.sessionId || 8; + if (typeof sessionId === 'function') { + this._generateSessionId = sessionId; + } else if (typeof sessionId === 'number') { + this._generateSessionId = function () { + return randomString(sessionId); + }; + } else { + throw new TypeError('If sessionId is used in the options, it needs to be a number or a function.'); + } - this._server = options.server || random.numberString(1000); + this._server = options.server || randomNumberString(1000); - // Step 1 of WS spec - parse and validate the url. Issue #8 - var parsedUrl = new URL(url); - if (!parsedUrl.host || !parsedUrl.protocol) { - throw new SyntaxError("The URL '" + url + "' is invalid"); - } else if (parsedUrl.hash) { - throw new SyntaxError('The URL must not contain a fragment'); - } else if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { - throw new SyntaxError("The URL's scheme must be either 'http:' or 'https:'. '" + parsedUrl.protocol + "' is not allowed."); - } + // Step 1 of WS spec - parse and validate the url. Issue #8 + const parsedUrl = new URL(url); + if (!parsedUrl.host || !parsedUrl.protocol) { + throw new SyntaxError('The URL \'' + url + '\' is invalid'); + } else if (parsedUrl.hash) { + throw new SyntaxError('The URL must not contain a fragment'); + } else if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + throw new SyntaxError('The URL\'s scheme must be either \'http:\' or \'https:\'. \'' + parsedUrl.protocol + '\' is not allowed.'); + } - var secure = parsedUrl.protocol === 'https:'; - // Step 2 - don't allow secure origin with an insecure protocol - if (loc.protocol === 'https:' && !secure) { - // exception is 127.0.0.0/8 and ::1 urls - if (!urlUtils.isLoopbackAddr(parsedUrl.hostname)) { + const secure = parsedUrl.protocol === 'https:'; + // Step 2 - don't allow secure origin with an insecure protocol + if (loc.protocol === 'https:' && !secure // Exception is 127.0.0.0/8 and ::1 urls + && !urlUtils.isLoopbackAddr(parsedUrl.hostname)) { throw new Error('SecurityError: An insecure SockJS connection may not be initiated from a page loaded over HTTPS'); } - } - - // Step 3 - check port access - no need here - // Step 4 - parse protocols argument - if (!protocols) { - protocols = []; - } else if (!Array.isArray(protocols)) { - protocols = [protocols]; - } - // Step 5 - check protocols argument - var sortedProtocols = protocols.sort(); - sortedProtocols.forEach(function(proto, i) { - if (!proto) { - throw new SyntaxError("The protocols entry '" + proto + "' is invalid."); + // Step 3 - check port access - no need here + // Step 4 - parse protocols argument + if (!protocols) { + protocols = []; + } else if (!Array.isArray(protocols)) { + protocols = [protocols]; } - if (i < (sortedProtocols.length - 1) && proto === sortedProtocols[i + 1]) { - throw new SyntaxError("The protocols entry '" + proto + "' is duplicated."); + + // Step 5 - check protocols argument + const sortedProtocols = protocols.sort(); + for (const [i, proto] of sortedProtocols.entries()) { + if (!proto) { + throw new SyntaxError('The protocols entry \'' + proto + '\' is invalid.'); + } + + if (i < (sortedProtocols.length - 1) && proto === sortedProtocols[i + 1]) { + throw new SyntaxError('The protocols entry \'' + proto + '\' is duplicated.'); + } } - }); - - // Step 6 - convert origin - var o = urlUtils.getOrigin(loc.href); - this._origin = o ? o.toLowerCase() : null; - - // remove the trailing slash - parsedUrl.set('pathname', parsedUrl.pathname.replace(/\/+$/, '')); - - // store the sanitized url - this.url = parsedUrl.href; - debug('using url', this.url); - - // Step 7 - start connection in background - // obtain server info - // http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-26 - this._urlInfo = { - nullOrigin: !browser.hasDomain() - , sameOrigin: urlUtils.isOriginEqual(this.url, loc.href) - , sameScheme: urlUtils.isSchemeEqual(this.url, loc.href) - }; - - this._ir = new InfoReceiver(this.url, this._urlInfo); - this._ir.once('finish', this._receiveInfo.bind(this)); -} -inherits(SockJS, EventTarget); + // Step 6 - convert origin + const o = urlUtils.getOrigin(loc.href); + this._origin = o ? o.toLowerCase() : null; -function userSetCode(code) { - return code === 1000 || (code >= 3000 && code <= 4999); -} + // Remove the trailing slash + parsedUrl.set('pathname', parsedUrl.pathname.replace(/\/+$/, '')); -SockJS.prototype.close = function(code, reason) { - // Step 1 - if (code && !userSetCode(code)) { - throw new Error('InvalidAccessError: Invalid code'); - } - // Step 2.4 states the max is 123 bytes, but we are just checking length - if (reason && reason.length > 123) { - throw new SyntaxError('reason argument has an invalid length'); - } + // Store the sanitized url + this.url = parsedUrl.href; + debug('using url', this.url); + + // Step 7 - start connection in background + // obtain server info + // http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-26 + this._urlInfo = { + nullOrigin: !hasDomain(), + sameOrigin: urlUtils.isOriginEqual(this.url, loc.href), + sameScheme: urlUtils.isSchemeEqual(this.url, loc.href), + }; - // Step 3.1 - if (this.readyState === SockJS.CLOSING || this.readyState === SockJS.CLOSED) { - return; + this._ir = new InfoReceiver(this.url, this._urlInfo); + this._ir.once('finish', this._receiveInfo.bind(this)); } - // TODO look at docs to determine how to set this - var wasClean = true; - this._close(code || 1000, reason || 'Normal closure', wasClean); -}; + close(code, reason) { + // Step 1 + if (code && !userSetCode(code)) { + throw new Error('InvalidAccessError: Invalid code'); + } -SockJS.prototype.send = function(data) { - // #13 - convert anything non-string to string - // TODO this currently turns objects into [object Object] - if (typeof data !== 'string') { - data = '' + data; - } - if (this.readyState === SockJS.CONNECTING) { - throw new Error('InvalidStateError: The connection has not been established yet'); + // Step 2.4 states the max is 123 bytes, but we are just checking length + if (reason && reason.length > 123) { + throw new SyntaxError('reason argument has an invalid length'); + } + + // Step 3.1 + if (this.readyState === SockJS.CLOSING || this.readyState === SockJS.CLOSED) { + return; + } + + // TODO look at docs to determine how to set this + const wasClean = true; + this._close(code || 1000, reason || 'Normal closure', wasClean); } - if (this.readyState !== SockJS.OPEN) { - return; + + send(data) { + // #13 - convert anything non-string to string + // TODO this currently turns objects into [object Object] + if (typeof data !== 'string') { + data = String(data); + } + + if (this.readyState === SockJS.CONNECTING) { + throw new Error('InvalidStateError: The connection has not been established yet'); + } + + if (this.readyState !== SockJS.OPEN) { + return; + } + + this._transport.send(quote(data)); } - this._transport.send(escape.quote(data)); -}; - -SockJS.version = require('./version'); - -SockJS.CONNECTING = 0; -SockJS.OPEN = 1; -SockJS.CLOSING = 2; -SockJS.CLOSED = 3; - -SockJS.prototype._receiveInfo = function(info, rtt) { - debug('_receiveInfo', rtt); - this._ir = null; - if (!info) { - this._close(1002, 'Cannot connect to server'); - return; + + static version = version; + + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + _receiveInfo(info, rtt) { + debug('_receiveInfo', rtt); + this._ir = null; + if (!info) { + this._close(1002, 'Cannot connect to server'); + return; + } + + // Establish a round-trip timeout (RTO) based on the + // round-trip time (RTT) + this._rto = this.countRTO(rtt); + // Allow server to override url used for the actual transport + this._transUrl = info.base_url ? info.base_url : this.url; + info = extend(info, this._urlInfo); + debug('info', info); + // Determine list of desired and supported transports + const enabledTransports = transports.filterToEnabled(this._transportsWhitelist, info); + this._transports = enabledTransports.main; + debug(this._transports.length + ' enabled transports'); + + this._connect(); } - // establish a round-trip timeout (RTO) based on the - // round-trip time (RTT) - this._rto = this.countRTO(rtt); - // allow server to override url used for the actual transport - this._transUrl = info.base_url ? info.base_url : this.url; - info = objectUtils.extend(info, this._urlInfo); - debug('info', info); - // determine list of desired and supported transports - var enabledTransports = transports.filterToEnabled(this._transportsWhitelist, info); - this._transports = enabledTransports.main; - debug(this._transports.length + ' enabled transports'); - - this._connect(); -}; - -SockJS.prototype._connect = function() { - for (var Transport = this._transports.shift(); Transport; Transport = this._transports.shift()) { + _connect() { + const Transport = this._transports.shift(); + if (!Transport) { + this._close(2000, 'All transports failed', false); + return; + } + debug('attempt', Transport.transportName); - if (Transport.needBody) { - if (!global.document.body || - (typeof global.document.readyState !== 'undefined' && - global.document.readyState !== 'complete' && - global.document.readyState !== 'interactive')) { - debug('waiting for body'); - this._transports.unshift(Transport); - eventUtils.attachEvent('load', this._connect.bind(this)); - return; - } + if (Transport.needBody && (!global.document.body + || (typeof global.document.readyState !== 'undefined' + && global.document.readyState !== 'complete' + && global.document.readyState !== 'interactive'))) { + debug('waiting for body'); + this._transports.unshift(Transport); + attachEvent('load', this._connect.bind(this)); + return; } - // calculate timeout based on RTO and round trips. Default to 5s - var timeoutMs = Math.max(this._timeout, (this._rto * Transport.roundTrips) || 5000); + // Calculate timeout based on RTO and round trips. Default to 5s + const timeoutMs = Math.max(this._timeout, (this._rto * Transport.roundTrips) || 5000); this._transportTimeoutId = setTimeout(this._transportTimeout.bind(this), timeoutMs); debug('using timeout', timeoutMs); - var transportUrl = urlUtils.addPath(this._transUrl, '/' + this._server + '/' + this._generateSessionId()); - var options = this._transportOptions[Transport.transportName]; + const transportUrl = urlUtils.addPath(this._transUrl, '/' + this._server + '/' + this._generateSessionId()); + const options = this._transportOptions[Transport.transportName]; debug('transport url', transportUrl); - var transportObj = new Transport(transportUrl, this._transUrl, options); - transportObj.on('message', this._transportMessage.bind(this)); - transportObj.once('close', this._transportClose.bind(this)); - transportObj.transportName = Transport.transportName; - this._transport = transportObj; + const transportObject = new Transport(transportUrl, this._transUrl, options); + transportObject.on('message', this._transportMessage.bind(this)); + transportObject.once('close', this._transportClose.bind(this)); + transportObject.transportName = Transport.transportName; + this._transport = transportObject; - return; + this._close(2000, 'All transports failed', false); } - this._close(2000, 'All transports failed', false); -}; -SockJS.prototype._transportTimeout = function() { - debug('_transportTimeout'); - if (this.readyState === SockJS.CONNECTING) { - if (this._transport) { - this._transport.close(); - } + _transportTimeout() { + debug('_transportTimeout'); + if (this.readyState === SockJS.CONNECTING) { + if (this._transport) { + this._transport.close(); + } - this._transportClose(2007, 'Transport timed out'); + this._transportClose(2007, 'Transport timed out'); + } } -}; - -SockJS.prototype._transportMessage = function(msg) { - debug('_transportMessage', msg); - var self = this - , type = msg.slice(0, 1) - , content = msg.slice(1) - , payload - ; - - // first check for messages that don't need a payload - switch (type) { - case 'o': - this._open(); - return; - case 'h': - this.dispatchEvent(new Event('heartbeat')); - debug('heartbeat', this.transport); + + _transportMessage(message) { + debug('_transportMessage', message); + const type = message.slice(0, 1); + const content = message.slice(1); + let payload; + + // First check for messages that don't need a payload + switch (type) { + case 'o': + this._open(); + return; + case 'h': + this.dispatchEvent(new Event('heartbeat')); + debug('heartbeat', this.transport); + return; + default: + break; + } + + if (content) { + try { + payload = JSON.parse(content); + } catch { + debug('bad json', content); + } + } + + if (typeof payload === 'undefined') { + debug('empty payload', content); return; - } + } - if (content) { - try { - payload = JSON.parse(content); - } catch (e) { - debug('bad json', content); + switch (type) { + case 'a': + if (Array.isArray(payload)) { + for (const p of payload) { + debug('message', this.transport, p); + this.dispatchEvent(new TransportMessageEvent(p)); + } + } + + break; + case 'm': + debug('message', this.transport, payload); + this.dispatchEvent(new TransportMessageEvent(payload)); + break; + case 'c': + if (Array.isArray(payload) && payload.length === 2) { + this._close(payload[0], payload[1], true); + } + + break; + default: + break; } } - if (typeof payload === 'undefined') { - debug('empty payload', content); - return; + _transportClose(code, reason) { + debug('_transportClose', this.transport, code, reason); + if (this._transport) { + this._transport.removeAllListeners(); + this._transport = null; + this.transport = null; + } + + if (!userSetCode(code) && code !== 2000 && this.readyState === SockJS.CONNECTING) { + this._connect(); + return; + } + + this._close(code, reason); } - switch (type) { - case 'a': - if (Array.isArray(payload)) { - payload.forEach(function(p) { - debug('message', self.transport, p); - self.dispatchEvent(new TransportMessageEvent(p)); - }); + _open() { + debug('_open', this._transport && this._transport.transportName, this.readyState); + if (this.readyState === SockJS.CONNECTING) { + if (this._transportTimeoutId) { + clearTimeout(this._transportTimeoutId); + this._transportTimeoutId = null; } - break; - case 'm': - debug('message', this.transport, payload); - this.dispatchEvent(new TransportMessageEvent(payload)); - break; - case 'c': - if (Array.isArray(payload) && payload.length === 2) { - this._close(payload[0], payload[1], true); - } - break; - } -}; - -SockJS.prototype._transportClose = function(code, reason) { - debug('_transportClose', this.transport, code, reason); - if (this._transport) { - this._transport.removeAllListeners(); - this._transport = null; - this.transport = null; - } - if (!userSetCode(code) && code !== 2000 && this.readyState === SockJS.CONNECTING) { - this._connect(); - return; + this.readyState = SockJS.OPEN; + this.transport = this._transport.transportName; + this.dispatchEvent(new Event('open')); + debug('connected', this.transport); + } else { + // The server might have been restarted, and lost track of our + // connection. + this._close(1006, 'Server lost session'); + } } - this._close(code, reason); -}; + _close(code, reason, wasClean) { + debug('_close', this.transport, code, reason, wasClean, this.readyState); + let forceFail = false; -SockJS.prototype._open = function() { - debug('_open', this._transport && this._transport.transportName, this.readyState); - if (this.readyState === SockJS.CONNECTING) { - if (this._transportTimeoutId) { - clearTimeout(this._transportTimeoutId); - this._transportTimeoutId = null; + if (this._ir) { + forceFail = true; + this._ir.close(); + this._ir = null; } - this.readyState = SockJS.OPEN; - this.transport = this._transport.transportName; - this.dispatchEvent(new Event('open')); - debug('connected', this.transport); - } else { - // The server might have been restarted, and lost track of our - // connection. - this._close(1006, 'Server lost session'); - } -}; -SockJS.prototype._close = function(code, reason, wasClean) { - debug('_close', this.transport, code, reason, wasClean, this.readyState); - var forceFail = false; + if (this._transport) { + this._transport.close(); + this._transport = null; + this.transport = null; + } - if (this._ir) { - forceFail = true; - this._ir.close(); - this._ir = null; - } - if (this._transport) { - this._transport.close(); - this._transport = null; - this.transport = null; - } + if (this.readyState === SockJS.CLOSED) { + throw new Error('InvalidStateError: SockJS has already been closed'); + } - if (this.readyState === SockJS.CLOSED) { - throw new Error('InvalidStateError: SockJS has already been closed'); - } + this.readyState = SockJS.CLOSING; + setTimeout(() => { + this.readyState = SockJS.CLOSED; - this.readyState = SockJS.CLOSING; - setTimeout(function() { - this.readyState = SockJS.CLOSED; + if (forceFail) { + this.dispatchEvent(new Event('error')); + } + + const evt = new CloseEvent('close'); + evt.wasClean = wasClean || false; + evt.code = code || 1000; + evt.reason = reason; + + this.dispatchEvent(evt); + /* eslint-disable unicorn/prefer-add-event-listener */ + this.onerror = null; + this.onclose = null; + this.onmessage = null; + /* eslint-enable unicorn/prefer-add-event-listener */ + debug('disconnected'); + }, 0); + } - if (forceFail) { - this.dispatchEvent(new Event('error')); + // See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/ + // and RFC 2988. + countRTO(rtt) { + // In a local environment, when using IE8/9 and the `jsonp-polling` + // transport the time needed to establish a connection (the time that pass + // from the opening of the transport to the call of `_dispatchOpen`) is + // around 200msec (the lower bound used in the article above) and this + // causes spurious timeouts. For this reason we calculate a value slightly + // larger than that used in the article. + if (rtt > 100) { + return 4 * rtt; // Rto > 400msec } - var e = new CloseEvent('close'); - e.wasClean = wasClean || false; - e.code = code || 1000; - e.reason = reason; - - this.dispatchEvent(e); - this.onmessage = this.onclose = this.onerror = null; - debug('disconnected'); - }.bind(this), 0); -}; - -// See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/ -// and RFC 2988. -SockJS.prototype.countRTO = function(rtt) { - // In a local environment, when using IE8/9 and the `jsonp-polling` - // transport the time needed to establish a connection (the time that pass - // from the opening of the transport to the call of `_dispatchOpen`) is - // around 200msec (the lower bound used in the article above) and this - // causes spurious timeouts. For this reason we calculate a value slightly - // larger than that used in the article. - if (rtt > 100) { - return 4 * rtt; // rto > 400msec + return 300 + rtt; // 300msec < rto <= 400msec } - return 300 + rtt; // 300msec < rto <= 400msec -}; +} + +function userSetCode(code) { + return code === 1000 || (code >= 3000 && code <= 4999); +} -module.exports = function(availableTransports) { +export default function bootstrap(availableTransports) { transports = transport(availableTransports); - require('./iframe-bootstrap')(SockJS, availableTransports); + iframeBootstrap(SockJS, availableTransports); return SockJS; -}; +} diff --git a/lib/shims.js b/lib/shims.js deleted file mode 100644 index 5eaef13f..00000000 --- a/lib/shims.js +++ /dev/null @@ -1,452 +0,0 @@ -/* eslint-disable */ -/* jscs: disable */ -'use strict'; - -// pulled specific shims from https://github.com/es-shims/es5-shim - -var ArrayPrototype = Array.prototype; -var ObjectPrototype = Object.prototype; -var FunctionPrototype = Function.prototype; -var StringPrototype = String.prototype; -var array_slice = ArrayPrototype.slice; - -var _toString = ObjectPrototype.toString; -var isFunction = function (val) { - return ObjectPrototype.toString.call(val) === '[object Function]'; -}; -var isArray = function isArray(obj) { - return _toString.call(obj) === '[object Array]'; -}; -var isString = function isString(obj) { - return _toString.call(obj) === '[object String]'; -}; - -var supportsDescriptors = Object.defineProperty && (function () { - try { - Object.defineProperty({}, 'x', {}); - return true; - } catch (e) { /* this is ES3 */ - return false; - } -}()); - -// Define configurable, writable and non-enumerable props -// if they don't exist. -var defineProperty; -if (supportsDescriptors) { - defineProperty = function (object, name, method, forceAssign) { - if (!forceAssign && (name in object)) { return; } - Object.defineProperty(object, name, { - configurable: true, - enumerable: false, - writable: true, - value: method - }); - }; -} else { - defineProperty = function (object, name, method, forceAssign) { - if (!forceAssign && (name in object)) { return; } - object[name] = method; - }; -} -var defineProperties = function (object, map, forceAssign) { - for (var name in map) { - if (ObjectPrototype.hasOwnProperty.call(map, name)) { - defineProperty(object, name, map[name], forceAssign); - } - } -}; - -var toObject = function (o) { - if (o == null) { // this matches both null and undefined - throw new TypeError("can't convert " + o + ' to object'); - } - return Object(o); -}; - -// -// Util -// ====== -// - -// ES5 9.4 -// http://es5.github.com/#x9.4 -// http://jsperf.com/to-integer - -function toInteger(num) { - var n = +num; - if (n !== n) { // isNaN - n = 0; - } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - return n; -} - -function ToUint32(x) { - return x >>> 0; -} - -// -// Function -// ======== -// - -// ES-5 15.3.4.5 -// http://es5.github.com/#x15.3.4.5 - -function Empty() {} - -defineProperties(FunctionPrototype, { - bind: function bind(that) { // .length is 1 - // 1. Let Target be the this value. - var target = this; - // 2. If IsCallable(Target) is false, throw a TypeError exception. - if (!isFunction(target)) { - throw new TypeError('Function.prototype.bind called on incompatible ' + target); - } - // 3. Let A be a new (possibly empty) internal list of all of the - // argument values provided after thisArg (arg1, arg2 etc), in order. - // XXX slicedArgs will stand in for "A" if used - var args = array_slice.call(arguments, 1); // for normal call - // 4. Let F be a new native ECMAScript object. - // 11. Set the [[Prototype]] internal property of F to the standard - // built-in Function prototype object as specified in 15.3.3.1. - // 12. Set the [[Call]] internal property of F as described in - // 15.3.4.5.1. - // 13. Set the [[Construct]] internal property of F as described in - // 15.3.4.5.2. - // 14. Set the [[HasInstance]] internal property of F as described in - // 15.3.4.5.3. - var binder = function () { - - if (this instanceof bound) { - // 15.3.4.5.2 [[Construct]] - // When the [[Construct]] internal method of a function object, - // F that was created using the bind function is called with a - // list of arguments ExtraArgs, the following steps are taken: - // 1. Let target be the value of F's [[TargetFunction]] - // internal property. - // 2. If target has no [[Construct]] internal method, a - // TypeError exception is thrown. - // 3. Let boundArgs be the value of F's [[BoundArgs]] internal - // property. - // 4. Let args be a new list containing the same values as the - // list boundArgs in the same order followed by the same - // values as the list ExtraArgs in the same order. - // 5. Return the result of calling the [[Construct]] internal - // method of target providing args as the arguments. - - var result = target.apply( - this, - args.concat(array_slice.call(arguments)) - ); - if (Object(result) === result) { - return result; - } - return this; - - } else { - // 15.3.4.5.1 [[Call]] - // When the [[Call]] internal method of a function object, F, - // which was created using the bind function is called with a - // this value and a list of arguments ExtraArgs, the following - // steps are taken: - // 1. Let boundArgs be the value of F's [[BoundArgs]] internal - // property. - // 2. Let boundThis be the value of F's [[BoundThis]] internal - // property. - // 3. Let target be the value of F's [[TargetFunction]] internal - // property. - // 4. Let args be a new list containing the same values as the - // list boundArgs in the same order followed by the same - // values as the list ExtraArgs in the same order. - // 5. Return the result of calling the [[Call]] internal method - // of target providing boundThis as the this value and - // providing args as the arguments. - - // equiv: target.call(this, ...boundArgs, ...args) - return target.apply( - that, - args.concat(array_slice.call(arguments)) - ); - - } - - }; - - // 15. If the [[Class]] internal property of Target is "Function", then - // a. Let L be the length property of Target minus the length of A. - // b. Set the length own property of F to either 0 or L, whichever is - // larger. - // 16. Else set the length own property of F to 0. - - var boundLength = Math.max(0, target.length - args.length); - - // 17. Set the attributes of the length own property of F to the values - // specified in 15.3.5.1. - var boundArgs = []; - for (var i = 0; i < boundLength; i++) { - boundArgs.push('$' + i); - } - - // XXX Build a dynamic function with desired amount of arguments is the only - // way to set the length property of a function. - // In environments where Content Security Policies enabled (Chrome extensions, - // for ex.) all use of eval or Function costructor throws an exception. - // However in all of these environments Function.prototype.bind exists - // and so this code will never be executed. - var bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder); - - if (target.prototype) { - Empty.prototype = target.prototype; - bound.prototype = new Empty(); - // Clean up dangling references. - Empty.prototype = null; - } - - // TODO - // 18. Set the [[Extensible]] internal property of F to true. - - // TODO - // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3). - // 20. Call the [[DefineOwnProperty]] internal method of F with - // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]: - // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and - // false. - // 21. Call the [[DefineOwnProperty]] internal method of F with - // arguments "arguments", PropertyDescriptor {[[Get]]: thrower, - // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, - // and false. - - // TODO - // NOTE Function objects created using Function.prototype.bind do not - // have a prototype property or the [[Code]], [[FormalParameters]], and - // [[Scope]] internal properties. - // XXX can't delete prototype in pure-js. - - // 22. Return F. - return bound; - } -}); - -// -// Array -// ===== -// - -// ES5 15.4.3.2 -// http://es5.github.com/#x15.4.3.2 -// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray -defineProperties(Array, { isArray: isArray }); - - -var boxedString = Object('a'); -var splitString = boxedString[0] !== 'a' || !(0 in boxedString); - -var properlyBoxesContext = function properlyBoxed(method) { - // Check node 0.6.21 bug where third parameter is not boxed - var properlyBoxesNonStrict = true; - var properlyBoxesStrict = true; - if (method) { - method.call('foo', function (_, __, context) { - if (typeof context !== 'object') { properlyBoxesNonStrict = false; } - }); - - method.call([1], function () { - 'use strict'; - properlyBoxesStrict = typeof this === 'string'; - }, 'x'); - } - return !!method && properlyBoxesNonStrict && properlyBoxesStrict; -}; - -defineProperties(ArrayPrototype, { - forEach: function forEach(fun /*, thisp*/) { - var object = toObject(this), - self = splitString && isString(this) ? this.split('') : object, - thisp = arguments[1], - i = -1, - length = self.length >>> 0; - - // If no callback function or if callback is not a callable function - if (!isFunction(fun)) { - throw new TypeError(); // TODO message - } - - while (++i < length) { - if (i in self) { - // Invoke the callback function with call, passing arguments: - // context, property value, property key, thisArg object - // context - fun.call(thisp, self[i], i, object); - } - } - } -}, !properlyBoxesContext(ArrayPrototype.forEach)); - -// ES5 15.4.4.14 -// http://es5.github.com/#x15.4.4.14 -// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf -var hasFirefox2IndexOfBug = Array.prototype.indexOf && [0, 1].indexOf(1, 2) !== -1; -defineProperties(ArrayPrototype, { - indexOf: function indexOf(sought /*, fromIndex */ ) { - var self = splitString && isString(this) ? this.split('') : toObject(this), - length = self.length >>> 0; - - if (!length) { - return -1; - } - - var i = 0; - if (arguments.length > 1) { - i = toInteger(arguments[1]); - } - - // handle negative indices - i = i >= 0 ? i : Math.max(0, length + i); - for (; i < length; i++) { - if (i in self && self[i] === sought) { - return i; - } - } - return -1; - } -}, hasFirefox2IndexOfBug); - -// -// String -// ====== -// - -// ES5 15.5.4.14 -// http://es5.github.com/#x15.5.4.14 - -// [bugfix, IE lt 9, firefox 4, Konqueror, Opera, obscure browsers] -// Many browsers do not split properly with regular expressions or they -// do not perform the split correctly under obscure conditions. -// See http://blog.stevenlevithan.com/archives/cross-browser-split -// I've tested in many browsers and this seems to cover the deviant ones: -// 'ab'.split(/(?:ab)*/) should be ["", ""], not [""] -// '.'.split(/(.?)(.?)/) should be ["", ".", "", ""], not ["", ""] -// 'tesst'.split(/(s)*/) should be ["t", undefined, "e", "s", "t"], not -// [undefined, "t", undefined, "e", ...] -// ''.split(/.?/) should be [], not [""] -// '.'.split(/()()/) should be ["."], not ["", "", "."] - -var string_split = StringPrototype.split; -if ( - 'ab'.split(/(?:ab)*/).length !== 2 || - '.'.split(/(.?)(.?)/).length !== 4 || - 'tesst'.split(/(s)*/)[1] === 't' || - 'test'.split(/(?:)/, -1).length !== 4 || - ''.split(/.?/).length || - '.'.split(/()()/).length > 1 -) { - (function () { - var compliantExecNpcg = /()??/.exec('')[1] === void 0; // NPCG: nonparticipating capturing group - - StringPrototype.split = function (separator, limit) { - var string = this; - if (separator === void 0 && limit === 0) { - return []; - } - - // If `separator` is not a regex, use native split - if (_toString.call(separator) !== '[object RegExp]') { - return string_split.call(this, separator, limit); - } - - var output = [], - flags = (separator.ignoreCase ? 'i' : '') + - (separator.multiline ? 'm' : '') + - (separator.extended ? 'x' : '') + // Proposed for ES6 - (separator.sticky ? 'y' : ''), // Firefox 3+ - lastLastIndex = 0, - // Make `global` and avoid `lastIndex` issues by working with a copy - separator2, match, lastIndex, lastLength; - separator = new RegExp(separator.source, flags + 'g'); - string += ''; // Type-convert - if (!compliantExecNpcg) { - // Doesn't need flags gy, but they don't hurt - separator2 = new RegExp('^' + separator.source + '$(?!\\s)', flags); - } - /* Values for `limit`, per the spec: - * If undefined: 4294967295 // Math.pow(2, 32) - 1 - * If 0, Infinity, or NaN: 0 - * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296; - * If negative number: 4294967296 - Math.floor(Math.abs(limit)) - * If other: Type-convert, then use the above rules - */ - limit = limit === void 0 ? - -1 >>> 0 : // Math.pow(2, 32) - 1 - ToUint32(limit); - while (match = separator.exec(string)) { - // `separator.lastIndex` is not reliable cross-browser - lastIndex = match.index + match[0].length; - if (lastIndex > lastLastIndex) { - output.push(string.slice(lastLastIndex, match.index)); - // Fix browsers whose `exec` methods don't consistently return `undefined` for - // nonparticipating capturing groups - if (!compliantExecNpcg && match.length > 1) { - match[0].replace(separator2, function () { - for (var i = 1; i < arguments.length - 2; i++) { - if (arguments[i] === void 0) { - match[i] = void 0; - } - } - }); - } - if (match.length > 1 && match.index < string.length) { - ArrayPrototype.push.apply(output, match.slice(1)); - } - lastLength = match[0].length; - lastLastIndex = lastIndex; - if (output.length >= limit) { - break; - } - } - if (separator.lastIndex === match.index) { - separator.lastIndex++; // Avoid an infinite loop - } - } - if (lastLastIndex === string.length) { - if (lastLength || !separator.test('')) { - output.push(''); - } - } else { - output.push(string.slice(lastLastIndex)); - } - return output.length > limit ? output.slice(0, limit) : output; - }; - }()); - -// [bugfix, chrome] -// If separator is undefined, then the result array contains just one String, -// which is the this value (converted to a String). If limit is not undefined, -// then the output array is truncated so that it contains no more than limit -// elements. -// "0".split(undefined, 0) -> [] -} else if ('0'.split(void 0, 0).length) { - StringPrototype.split = function split(separator, limit) { - if (separator === void 0 && limit === 0) { return []; } - return string_split.call(this, separator, limit); - }; -} - -// ECMA-262, 3rd B.2.3 -// Not an ECMAScript standard, although ECMAScript 3rd Edition has a -// non-normative section suggesting uniform semantics and it should be -// normalized across all browsers -// [bugfix, IE lt 9] IE < 9 substr() with negative value not working in IE -var string_substr = StringPrototype.substr; -var hasNegativeSubstrBug = ''.substr && '0b'.substr(-1) !== 'b'; -defineProperties(StringPrototype, { - substr: function substr(start, length) { - return string_substr.call( - this, - start < 0 ? ((start = this.length + start) < 0 ? 0 : start) : start, - length - ); - } -}, hasNegativeSubstrBug); diff --git a/lib/transport-list.js b/lib/transport-list.js index b79e7d06..290a3555 100644 --- a/lib/transport-list.js +++ b/lib/transport-list.js @@ -1,18 +1,29 @@ -'use strict'; +import iframeWrap from './transport/lib/iframe-wrap.js'; +import websocket from './transport/websocket.js'; +import xhrStreaming from './transport/xhr-streaming.js'; +import xdrStreaming from './transport/xdr-streaming.js'; +import eventsource from './transport/eventsource.js'; -module.exports = [ - // streaming transports - require('./transport/websocket') -, require('./transport/xhr-streaming') -, require('./transport/xdr-streaming') -, require('./transport/eventsource') -, require('./transport/lib/iframe-wrap')(require('./transport/eventsource')) +import htmlfile from './transport/htmlfile.js'; +import xhrPolling from './transport/xhr-polling.js'; +import xdrPolling from './transport/xdr-polling.js'; +import jsonpPolling from './transport/jsonp-polling.js'; - // polling transports -, require('./transport/htmlfile') -, require('./transport/lib/iframe-wrap')(require('./transport/htmlfile')) -, require('./transport/xhr-polling') -, require('./transport/xdr-polling') -, require('./transport/lib/iframe-wrap')(require('./transport/xhr-polling')) -, require('./transport/jsonp-polling') +const transports = [ + // Streaming transports + websocket, + xhrStreaming, + xdrStreaming, + eventsource, + iframeWrap(eventsource), + + // Polling transports + htmlfile, + iframeWrap(htmlfile), + xhrPolling, + xdrPolling, + iframeWrap(xhrPolling), + jsonpPolling, ]; + +export default transports; diff --git a/lib/transport/browser/abstract-xhr.js b/lib/transport/browser/abstract-xhr.js index ae04862e..b7a22a69 100644 --- a/lib/transport/browser/abstract-xhr.js +++ b/lib/transport/browser/abstract-xhr.js @@ -1,193 +1,202 @@ -'use strict'; - -var EventEmitter = require('events').EventEmitter - , inherits = require('inherits') - , utils = require('../../utils/event') - , urlUtils = require('../../utils/url') - , XHR = global.XMLHttpRequest - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:browser:xhr'); -} - -function AbstractXHRObject(method, url, payload, opts) { - debug(method, url); - var self = this; - EventEmitter.call(this); +import {EventEmitter} from 'node:events'; +import {unloadAdd, unloadDel} from '../../utils/event.js'; +import {addQuery} from '../../utils/url.js'; +import debugFunc from './utils/debug.js'; - setTimeout(function () { - self._start(method, url, payload, opts); - }, 0); -} +let XHR = global.XMLHttpRequest; -inherits(AbstractXHRObject, EventEmitter); +const debug = debugFunc('sockjs-client:browser:xhr'); -AbstractXHRObject.prototype._start = function(method, url, payload, opts) { - var self = this; +class AbstractXHRObject extends EventEmitter { + constructor(method, url, payload, options) { + super(); + debug(method, url); - try { - this.xhr = new XHR(); - } catch (x) { - // intentionally empty + setTimeout(() => { + this._start(method, url, payload, options); + }, 0); } - if (!this.xhr) { - debug('no xhr'); - this.emit('finish', 0, 'no xhr support'); - this._cleanup(); - return; - } + _start(method, url, payload, options) { + try { + this.xhr = new XHR(); + } catch { + // Intentionally empty + } - // several browsers cache POSTs - url = urlUtils.addQuery(url, 't=' + (+new Date())); - - // Explorer tends to keep connection open, even after the - // tab gets closed: http://bugs.jquery.com/ticket/5280 - this.unloadRef = utils.unloadAdd(function() { - debug('unload cleanup'); - self._cleanup(true); - }); - try { - this.xhr.open(method, url, true); - if (this.timeout && 'timeout' in this.xhr) { - this.xhr.timeout = this.timeout; - this.xhr.ontimeout = function() { - debug('xhr timeout'); - self.emit('finish', 0, ''); - self._cleanup(false); - }; + if (!this.xhr) { + debug('no xhr'); + this.emit('finish', 0, 'no xhr support'); + this._cleanup(); + return; } - } catch (e) { - debug('exception', e); - // IE raises an exception on wrong port. - this.emit('finish', 0, ''); - this._cleanup(false); - return; - } - if ((!opts || !opts.noCredentials) && AbstractXHRObject.supportsCORS) { - debug('withCredentials'); - // Mozilla docs says https://developer.mozilla.org/en/XMLHttpRequest : - // "This never affects same-site requests." + // Several browsers cache POSTs + url = addQuery(url, 't=' + (Date.now())); - this.xhr.withCredentials = true; - } - if (opts && opts.headers) { - for (var key in opts.headers) { - this.xhr.setRequestHeader(key, opts.headers[key]); + // Explorer tends to keep connection open, even after the + // tab gets closed: http://bugs.jquery.com/ticket/5280 + this.unloadRef = unloadAdd(() => { + debug('unload cleanup'); + this._cleanup(true); + }); + try { + this.xhr.open(method, url, true); + if (this.timeout && 'timeout' in this.xhr) { + this.xhr.timeout = this.timeout; + this.xhr.ontimeout = () => { + debug('xhr timeout'); + this.emit('finish', 0, ''); + this._cleanup(false); + }; + } + } catch (error) { + debug('exception', error); + // IE raises an exception on wrong port. + this.emit('finish', 0, ''); + this._cleanup(false); + return; } - } - this.xhr.onreadystatechange = function() { - if (self.xhr) { - var x = self.xhr; - var text, status; - debug('readyState', x.readyState); - switch (x.readyState) { - case 3: - // IE doesn't like peeking into responseText or status - // on Microsoft.XMLHTTP and readystate=3 - try { - status = x.status; - text = x.responseText; - } catch (e) { - // intentionally empty - } - debug('status', status); - // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 - if (status === 1223) { - status = 204; - } + if ((!options || !options.noCredentials) && AbstractXHRObject.supportsCORS) { + debug('withCredentials'); + // Mozilla docs says https://developer.mozilla.org/en/XMLHttpRequest : + // "This never affects same-site requests." - // IE does return readystate == 3 for 404 answers. - if (status === 200 && text && text.length > 0) { - debug('chunk'); - self.emit('chunk', status, text); - } - break; - case 4: - status = x.status; - debug('status', status); - // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 - if (status === 1223) { - status = 204; - } - // IE returns this for a bad port - // http://msdn.microsoft.com/en-us/library/windows/desktop/aa383770(v=vs.85).aspx - if (status === 12005 || status === 12029) { - status = 0; - } + this.xhr.withCredentials = true; + } - debug('finish', status, x.responseText); - self.emit('finish', status, x.responseText); - self._cleanup(false); - break; + if (options && options.headers) { + for (const key in options.headers) { + if (Object.prototype.hasOwnProperty.call(options.headers, key)) { + this.xhr.setRequestHeader(key, options.headers[key]); + } } } - }; - try { - self.xhr.send(payload); - } catch (e) { - self.emit('finish', 0, ''); - self._cleanup(false); - } -}; + this.xhr.addEventListener('readystatechange', () => { + if (this.xhr) { + const x = this.xhr; + let text; + let status; + debug('readyState', x.readyState); + switch (x.readyState) { + case 3: + // IE doesn't like peeking into responseText or status + // on Microsoft.XMLHTTP and readystate=3 + try { + status = x.status; + text = x.responseText; + } catch { + // Intentionally empty + } + + debug('status', status); + // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 + if (status === 1223) { + status = 204; + } + + // IE does return readystate == 3 for 404 answers. + if (status === 200 && text && text.length > 0) { + debug('chunk'); + this.emit('chunk', status, text); + } + + break; + case 4: + status = x.status; + debug('status', status); + // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 + if (status === 1223) { + status = 204; + } + + // IE returns this for a bad port + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa383770(v=vs.85).aspx + if (status === 12_005 || status === 12_029) { + status = 0; + } + + debug('finish', status, x.responseText); + this.emit('finish', status, x.responseText); + this._cleanup(false); + break; + default: + break; + } + } + }); -AbstractXHRObject.prototype._cleanup = function(abort) { - debug('cleanup'); - if (!this.xhr) { - return; + try { + this.xhr.send(payload); + } catch { + this.emit('finish', 0, ''); + this._cleanup(false); + } } - this.removeAllListeners(); - utils.unloadDel(this.unloadRef); - // IE needs this field to be a function - this.xhr.onreadystatechange = function() {}; - if (this.xhr.ontimeout) { - this.xhr.ontimeout = null; - } + _cleanup = function (abort) { + debug('cleanup'); + if (!this.xhr) { + return; + } - if (abort) { - try { - this.xhr.abort(); - } catch (x) { - // intentionally empty + this.removeAllListeners(); + unloadDel(this.unloadRef); + + // IE needs this field to be a function + this.xhr.addEventListener('readystatechange', () => {}); + if (this.xhr.ontimeout) { + this.xhr.ontimeout = null; } - } - this.unloadRef = this.xhr = null; -}; - -AbstractXHRObject.prototype.close = function() { - debug('close'); - this._cleanup(true); -}; - -AbstractXHRObject.enabled = !!XHR; -// override XMLHttpRequest for IE6/7 -// obfuscate to avoid firewalls -var axo = ['Active'].concat('Object').join('X'); -if (!AbstractXHRObject.enabled && (axo in global)) { - debug('overriding xmlhttprequest'); - XHR = function() { - try { - return new global[axo]('Microsoft.XMLHTTP'); - } catch (e) { - return null; + + if (abort) { + try { + this.xhr.abort(); + } catch { + // Intentionally empty + } } + + this.xhr = null; + this.unloadRef = null; }; - AbstractXHRObject.enabled = !!new XHR(); -} -var cors = false; -try { - cors = 'withCredentials' in new XHR(); -} catch (ignored) { - // intentionally empty -} + close = function () { + debug('close'); + this._cleanup(true); + }; + + static supportsCORS = false; + static enabled = (function () { + let xhrPresent = Boolean(XHR); + // Override XMLHttpRequest for IE6/7 + // obfuscate to avoid firewalls + const axo = ['Active', 'Object'].join('X'); + if (!xhrPresent && (axo in global)) { + debug('overriding xmlhttprequest'); + XHR = function () { + try { + return new global[axo]('Microsoft.XMLHTTP'); + } catch { + return null; + } + }; -AbstractXHRObject.supportsCORS = cors; + xhrPresent = Boolean(new XHR()); + } + + if (xhrPresent) { + try { + AbstractXHRObject.supportsCORS = 'withCredentials' in new XHR(); + } catch { + // Intentionally empty + } + } + + return xhrPresent; + })(); +} -module.exports = AbstractXHRObject; +export default AbstractXHRObject; diff --git a/lib/transport/browser/eventsource.js b/lib/transport/browser/eventsource.js index 719c69ce..e81cc711 100644 --- a/lib/transport/browser/eventsource.js +++ b/lib/transport/browser/eventsource.js @@ -1 +1 @@ -module.exports = global.EventSource; +export default global.EventSource; diff --git a/lib/transport/browser/websocket.js b/lib/transport/browser/websocket.js index 01bf814f..ad3cc6b5 100644 --- a/lib/transport/browser/websocket.js +++ b/lib/transport/browser/websocket.js @@ -1,10 +1 @@ -'use strict'; - -var Driver = global.WebSocket || global.MozWebSocket; -if (Driver) { - module.exports = function WebSocketBrowserDriver(url) { - return new Driver(url); - }; -} else { - module.exports = undefined; -} +export default (global.WebSocket || global.MozWebSocket); diff --git a/lib/transport/driver/eventsource.js b/lib/transport/driver/eventsource.js index 6c8d6499..5ff49a6a 100644 --- a/lib/transport/driver/eventsource.js +++ b/lib/transport/driver/eventsource.js @@ -1 +1,2 @@ -module.exports = require('eventsource'); + +export {default} from 'eventsource'; diff --git a/lib/transport/driver/websocket.js b/lib/transport/driver/websocket.js index 82903afa..6d4ab8a5 100644 --- a/lib/transport/driver/websocket.js +++ b/lib/transport/driver/websocket.js @@ -1 +1,2 @@ -module.exports = require('faye-websocket').Client; + +export {Client as default} from 'faye-websocket'; diff --git a/lib/transport/driver/xhr.js b/lib/transport/driver/xhr.js index 9e1217b2..d2b33e38 100644 --- a/lib/transport/driver/xhr.js +++ b/lib/transport/driver/xhr.js @@ -1,71 +1,65 @@ -'use strict'; +import {EventEmitter} from 'node:events'; +import http from 'node:http'; +import https from 'node:https'; +import {URL} from 'url-parse'; +import debugFunc from './utils/debug.js'; -var EventEmitter = require('events').EventEmitter - , inherits = require('inherits') - , http = require('http') - , https = require('https') - , URL = require('url-parse') - ; +const debug = debugFunc('sockjs-client:driver:xhr'); -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:driver:xhr'); -} - -function XhrDriver(method, url, payload, opts) { - debug(method, url, payload); - var self = this; - EventEmitter.call(this); +class XhrDriver extends EventEmitter { + constructor(method, url, payload, options_) { + debug(method, url, payload); + super(); - var parsedUrl = new URL(url); - var options = { - method: method - , hostname: parsedUrl.hostname.replace(/\[|\]/g, '') - , port: parsedUrl.port - , path: parsedUrl.pathname + (parsedUrl.query || '') - , headers: opts && opts.headers - }; + const parsedUrl = new URL(url); + const options = { + method, + hostname: parsedUrl.hostname.replace(/\[]/g, ''), + port: parsedUrl.port, + path: parsedUrl.pathname + (parsedUrl.query || ''), + headers: options_ && options_.headers, + }; - var protocol = parsedUrl.protocol === 'https:' ? https : http; - this.req = protocol.request(options, function(res) { - res.setEncoding('utf8'); - var responseText = ''; + const protocol = parsedUrl.protocol === 'https:' ? https : http; + this.req = protocol.request(options, resp => { + resp.setEncoding('utf8'); + let responseText = ''; - res.on('data', function(chunk) { - debug('data', chunk); - responseText += chunk; - self.emit('chunk', 200, responseText); + resp.on('data', chunk => { + debug('data', chunk); + responseText += chunk; + this.emit('chunk', 200, responseText); + }); + resp.once('end', () => { + debug('end'); + this.emit('finish', resp.statusCode, responseText); + this.req = null; + }); }); - res.once('end', function() { - debug('end'); - self.emit('finish', res.statusCode, responseText); - self.req = null; + + this.req.on('error', evt => { + debug('error', evt); + this.emit('finish', 0, evt.message); }); - }); - this.req.on('error', function(e) { - debug('error', e); - self.emit('finish', 0, e.message); - }); + if (payload) { + this.req.write(payload); + } - if (payload) { - this.req.write(payload); + this.req.end(); } - this.req.end(); -} - -inherits(XhrDriver, EventEmitter); -XhrDriver.prototype.close = function() { - debug('close'); - this.removeAllListeners(); - if (this.req) { - this.req.abort(); - this.req = null; + close() { + debug('close'); + this.removeAllListeners(); + if (this.req) { + this.req.abort(); + this.req = null; + } } -}; -XhrDriver.enabled = true; -XhrDriver.supportsCORS = true; + static enabled = true; + static supportsCORS = true; +} -module.exports = XhrDriver; +export default XhrDriver; diff --git a/lib/transport/eventsource.js b/lib/transport/eventsource.js index 62685cfd..57a7127c 100644 --- a/lib/transport/eventsource.js +++ b/lib/transport/eventsource.js @@ -1,27 +1,23 @@ -'use strict'; +import EventSourceDriver from 'eventsource'; +import AjaxBasedTransport from './lib/ajax-based.js'; +import EventSourceReceiver from './receiver/eventsource.js'; +import XHRCorsObject from './sender/xhr-cors.js'; -var inherits = require('inherits') - , AjaxBasedTransport = require('./lib/ajax-based') - , EventSourceReceiver = require('./receiver/eventsource') - , XHRCorsObject = require('./sender/xhr-cors') - , EventSourceDriver = require('eventsource') - ; +class EventSourceTransport extends AjaxBasedTransport { + constructor(transUrl) { + if (!EventSourceTransport.enabled()) { + throw new Error('Transport created when disabled'); + } -function EventSourceTransport(transUrl) { - if (!EventSourceTransport.enabled()) { - throw new Error('Transport created when disabled'); + super(transUrl, '/eventsource', EventSourceReceiver, XHRCorsObject); } - AjaxBasedTransport.call(this, transUrl, '/eventsource', EventSourceReceiver, XHRCorsObject); -} - -inherits(EventSourceTransport, AjaxBasedTransport); - -EventSourceTransport.enabled = function() { - return !!EventSourceDriver; -}; + static enabled() { + return Boolean(EventSourceDriver); + } -EventSourceTransport.transportName = 'eventsource'; -EventSourceTransport.roundTrips = 2; + static transportName = 'eventsource'; + static roundTrips = 2; +} -module.exports = EventSourceTransport; +export default EventSourceTransport; diff --git a/lib/transport/htmlfile.js b/lib/transport/htmlfile.js index 633b0774..bf26f4a6 100644 --- a/lib/transport/htmlfile.js +++ b/lib/transport/htmlfile.js @@ -1,25 +1,22 @@ -'use strict'; +import AjaxBasedTransport from './lib/ajax-based.js'; +import XHRLocalObject from './sender/xhr-local.js'; +import HtmlfileReceiver from './receiver/htmlfile.js'; -var inherits = require('inherits') - , HtmlfileReceiver = require('./receiver/htmlfile') - , XHRLocalObject = require('./sender/xhr-local') - , AjaxBasedTransport = require('./lib/ajax-based') - ; +class HtmlFileTransport extends AjaxBasedTransport { + constructor(transUrl) { + if (!HtmlfileReceiver.enabled) { + throw new Error('Transport created when disabled'); + } -function HtmlFileTransport(transUrl) { - if (!HtmlfileReceiver.enabled) { - throw new Error('Transport created when disabled'); + super(transUrl, '/htmlfile', HtmlfileReceiver, XHRLocalObject); } - AjaxBasedTransport.call(this, transUrl, '/htmlfile', HtmlfileReceiver, XHRLocalObject); -} - -inherits(HtmlFileTransport, AjaxBasedTransport); -HtmlFileTransport.enabled = function(info) { - return HtmlfileReceiver.enabled && info.sameOrigin; -}; + static enabled(info) { + return HtmlfileReceiver.enabled && info.sameOrigin; + } -HtmlFileTransport.transportName = 'htmlfile'; -HtmlFileTransport.roundTrips = 2; + static transportName = 'htmlfile'; + static roundTrips = 2; +} -module.exports = HtmlFileTransport; +export default HtmlFileTransport; diff --git a/lib/transport/iframe.js b/lib/transport/iframe.js index d567339f..a0b43a51 100644 --- a/lib/transport/iframe.js +++ b/lib/transport/iframe.js @@ -1,5 +1,3 @@ -'use strict'; - // Few cool transports do work only for same-origin. In order to make // them work cross-domain we shall use iframe, served from the // remote domain. New browsers have capabilities to communicate with @@ -8,133 +6,136 @@ // http://msdn.microsoft.com/en-us/library/cc197015(v=VS.85).aspx // http://stevesouders.com/misc/test-postmessage.php -var inherits = require('inherits') - , EventEmitter = require('events').EventEmitter - , version = require('../version') - , urlUtils = require('../utils/url') - , iframeUtils = require('../utils/iframe') - , eventUtils = require('../utils/event') - , random = require('../utils/random') - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:transport:iframe'); -} +import {EventEmitter} from 'node:events'; +import {string as randomString} from '../utils/random.js'; +import {attachEvent, detachEvent} from '../utils/event.js'; +import {createIframe, iframeEnabled} from '../utils/iframe.js'; +import {getOrigin, addPath, isOriginEqual} from '../utils/url.js'; +import {version} from '../package.json'; +import debugFunc from './utils/debug.js'; + +const debug = debugFunc('sockjs-client:transport:iframe'); + +class IframeTransport extends EventEmitter { + constructor(transport, transUrl, baseUrl) { + if (!IframeTransport.enabled()) { + throw new Error('Transport created when disabled'); + } + + super(); + + this.origin = getOrigin(baseUrl); + this.baseUrl = baseUrl; + this.transUrl = transUrl; + this.transport = transport; + this.windowId = randomString(8); -function IframeTransport(transport, transUrl, baseUrl) { - if (!IframeTransport.enabled()) { - throw new Error('Transport created when disabled'); + const iframeUrl = addPath(baseUrl, '/iframe.html') + '#' + this.windowId; + debug(transport, transUrl, iframeUrl); + + this.iframeObj = createIframe(iframeUrl, r => { + debug('err callback'); + this.emit('close', 1006, 'Unable to load an iframe (' + r + ')'); + this.close(); + }); + + this.onmessageCallback = this._message.bind(this); + attachEvent('message', this.onmessageCallback); } - EventEmitter.call(this); - - var self = this; - this.origin = urlUtils.getOrigin(baseUrl); - this.baseUrl = baseUrl; - this.transUrl = transUrl; - this.transport = transport; - this.windowId = random.string(8); - - var iframeUrl = urlUtils.addPath(baseUrl, '/iframe.html') + '#' + this.windowId; - debug(transport, transUrl, iframeUrl); - - this.iframeObj = iframeUtils.createIframe(iframeUrl, function(r) { - debug('err callback'); - self.emit('close', 1006, 'Unable to load an iframe (' + r + ')'); - self.close(); - }); - - this.onmessageCallback = this._message.bind(this); - eventUtils.attachEvent('message', this.onmessageCallback); -} -inherits(IframeTransport, EventEmitter); + close() { + debug('close'); + this.removeAllListeners(); + if (this.iframeObj) { + detachEvent('message', this.onmessageCallback); + try { + // When the iframe is not loaded, IE raises an exception + // on 'contentWindow'. + this.postMessage('c'); + } catch { + // Intentionally empty + } + + this.iframeObj.cleanup(); + this.iframeObj = null; + this.onmessageCallback = null; + } + } -IframeTransport.prototype.close = function() { - debug('close'); - this.removeAllListeners(); - if (this.iframeObj) { - eventUtils.detachEvent('message', this.onmessageCallback); + _message(evt) { + debug('message', evt.data); + if (!isOriginEqual(evt.origin, this.origin)) { + debug('not same origin', evt.origin, this.origin); + return; + } + + let iframeMessage; try { - // When the iframe is not loaded, IE raises an exception - // on 'contentWindow'. - this.postMessage('c'); - } catch (x) { - // intentionally empty + iframeMessage = JSON.parse(evt.data); + } catch { + debug('bad json', evt.data); + return; } - this.iframeObj.cleanup(); - this.iframeObj = null; - this.onmessageCallback = this.iframeObj = null; - } -}; -IframeTransport.prototype._message = function(e) { - debug('message', e.data); - if (!urlUtils.isOriginEqual(e.origin, this.origin)) { - debug('not same origin', e.origin, this.origin); - return; + if (iframeMessage.windowId !== this.windowId) { + debug('mismatched window id', iframeMessage.windowId, this.windowId); + return; + } + + switch (iframeMessage.type) { + case 's': + this.iframeObj.loaded(); + // Window global dependency + this.postMessage('s', JSON.stringify([ + version, + this.transport, + this.transUrl, + this.baseUrl, + ])); + break; + case 't': + this.emit('message', iframeMessage.data); + break; + case 'c': + { + let cdata; + try { + cdata = JSON.parse(iframeMessage.data); + } catch { + debug('bad json', iframeMessage.data); + return; + } + + this.emit('close', cdata[0], cdata[1]); + this.close(); + } + + break; + default: + break; + } } - var iframeMessage; - try { - iframeMessage = JSON.parse(e.data); - } catch (ignored) { - debug('bad json', e.data); - return; + postMessage(type, data) { + debug('postMessage', type, data); + this.iframeObj.post(JSON.stringify({ + windowId: this.windowId, + type, + data: data || '', + }), this.origin); } - if (iframeMessage.windowId !== this.windowId) { - debug('mismatched window id', iframeMessage.windowId, this.windowId); - return; + send(message) { + debug('send', message); + this.postMessage('m', message); } - switch (iframeMessage.type) { - case 's': - this.iframeObj.loaded(); - // window global dependency - this.postMessage('s', JSON.stringify([ - version - , this.transport - , this.transUrl - , this.baseUrl - ])); - break; - case 't': - this.emit('message', iframeMessage.data); - break; - case 'c': - var cdata; - try { - cdata = JSON.parse(iframeMessage.data); - } catch (ignored) { - debug('bad json', iframeMessage.data); - return; - } - this.emit('close', cdata[0], cdata[1]); - this.close(); - break; + static enabled() { + return iframeEnabled; } -}; - -IframeTransport.prototype.postMessage = function(type, data) { - debug('postMessage', type, data); - this.iframeObj.post(JSON.stringify({ - windowId: this.windowId - , type: type - , data: data || '' - }), this.origin); -}; - -IframeTransport.prototype.send = function(message) { - debug('send', message); - this.postMessage('m', message); -}; - -IframeTransport.enabled = function() { - return iframeUtils.iframeEnabled; -}; - -IframeTransport.transportName = 'iframe'; -IframeTransport.roundTrips = 2; - -module.exports = IframeTransport; + + static transportName = 'iframe'; + static roundTrips = 2; +} + +export default IframeTransport; diff --git a/lib/transport/jsonp-polling.js b/lib/transport/jsonp-polling.js index 9883ab95..2b601f4f 100644 --- a/lib/transport/jsonp-polling.js +++ b/lib/transport/jsonp-polling.js @@ -1,5 +1,3 @@ -'use strict'; - // The simplest and most robust transport, using the well-know cross // domain hack - JSONP. This transport is quite inefficient - one // message could use up to one http request. But at least it works almost @@ -8,27 +6,26 @@ // o you will get a spinning cursor // o for Konqueror a dumb timer is needed to detect errors -var inherits = require('inherits') - , SenderReceiver = require('./lib/sender-receiver') - , JsonpReceiver = require('./receiver/jsonp') - , jsonpSender = require('./sender/jsonp') - ; +import SenderReceiver from './lib/sender-receiver.js'; +import JsonpReceiver from './receiver/jsonp.js'; +import jsonpSender from './sender/jsonp.js'; -function JsonPTransport(transUrl) { - if (!JsonPTransport.enabled()) { - throw new Error('Transport created when disabled'); - } - SenderReceiver.call(this, transUrl, '/jsonp', jsonpSender, JsonpReceiver); -} +class JsonPTransport extends SenderReceiver { + constructor(transUrl) { + if (!JsonPTransport.enabled()) { + throw new Error('Transport created when disabled'); + } -inherits(JsonPTransport, SenderReceiver); + super(transUrl, '/jsonp', jsonpSender, JsonpReceiver); + } -JsonPTransport.enabled = function() { - return !!global.document; -}; + static enabled() { + return Boolean(global.document); + } -JsonPTransport.transportName = 'jsonp-polling'; -JsonPTransport.roundTrips = 1; -JsonPTransport.needBody = true; + static transportName = 'jsonp-polling'; + static roundTrips = 1; + static needBody = true; +} -module.exports = JsonPTransport; +export default JsonPTransport; diff --git a/lib/transport/lib/ajax-based.js b/lib/transport/lib/ajax-based.js index 29e104e7..9f30ee18 100644 --- a/lib/transport/lib/ajax-based.js +++ b/lib/transport/lib/ajax-based.js @@ -1,49 +1,45 @@ -'use strict'; +import {addPath} from '../../utils/url.js'; +import debugFunc from './utils/debug.js'; +import SenderReceiver from './sender-receiver.js'; -var inherits = require('inherits') - , urlUtils = require('../../utils/url') - , SenderReceiver = require('./sender-receiver') - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:ajax-based'); -} +const debug = debugFunc('sockjs-client:ajax-based'); function createAjaxSender(AjaxObject) { - return function(url, payload, callback) { + return function (url, payload, callback) { debug('create ajax sender', url, payload); - var opt = {}; + const opt = {}; if (typeof payload === 'string') { opt.headers = {'Content-type': 'text/plain'}; } - var ajaxUrl = urlUtils.addPath(url, '/xhr_send'); - var xo = new AjaxObject('POST', ajaxUrl, payload, opt); - xo.once('finish', function(status) { + + const ajaxUrl = addPath(url, '/xhr_send'); + let xo = new AjaxObject('POST', ajaxUrl, payload, opt); + xo.once('finish', status => { debug('finish', status); xo = null; if (status !== 200 && status !== 204) { return callback(new Error('http status ' + status)); } + callback(); }); - return function() { + return function () { debug('abort'); xo.close(); xo = null; - var err = new Error('Aborted'); - err.code = 1000; - callback(err); + const error = new Error('Aborted'); + error.code = 1000; + callback(error); }; }; } -function AjaxBasedTransport(transUrl, urlSuffix, Receiver, AjaxObject) { - SenderReceiver.call(this, transUrl, urlSuffix, createAjaxSender(AjaxObject), Receiver, AjaxObject); +class AjaxBasedTransport extends SenderReceiver { + constructor(transUrl, urlSuffix, Receiver, AjaxObject) { + super(transUrl, urlSuffix, createAjaxSender(AjaxObject), Receiver, AjaxObject); + } } -inherits(AjaxBasedTransport, SenderReceiver); - -module.exports = AjaxBasedTransport; +export default AjaxBasedTransport; diff --git a/lib/transport/lib/buffered-sender.js b/lib/transport/lib/buffered-sender.js index dda9163f..8ac7e050 100644 --- a/lib/transport/lib/buffered-sender.js +++ b/lib/transport/lib/buffered-sender.js @@ -1,87 +1,79 @@ -'use strict'; +import {EventEmitter} from 'node:events'; +import debugFunc from './utils/debug.js'; -var inherits = require('inherits') - , EventEmitter = require('events').EventEmitter - ; +const debug = debugFunc('sockjs-client:buffered-sender'); -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:buffered-sender'); -} +class BufferedSender extends EventEmitter { + constructor(url, sender) { + debug(url); + super(); + this.sendBuffer = []; + this.sender = sender; + this.url = url; + } -function BufferedSender(url, sender) { - debug(url); - EventEmitter.call(this); - this.sendBuffer = []; - this.sender = sender; - this.url = url; -} + send(message) { + debug('send', message); + this.sendBuffer.push(message); + if (!this.sendStop) { + this.sendSchedule(); + } + } -inherits(BufferedSender, EventEmitter); + // For polling transports in a situation when in the message callback, + // new message is being send. If the sending connection was started + // before receiving one, it is possible to saturate the network and + // timeout due to the lack of receiving socket. To avoid that we delay + // sending messages by some small time, in order to let receiving + // connection be started beforehand. This is only a halfmeasure and + // does not fix the big problem, but it does make the tests go more + // stable on slow networks. + sendScheduleWait() { + debug('sendScheduleWait'); + this.sendStop = () => { + debug('sendStop'); + this.sendStop = null; + clearTimeout(tref); + }; -BufferedSender.prototype.send = function(message) { - debug('send', message); - this.sendBuffer.push(message); - if (!this.sendStop) { - this.sendSchedule(); + const tref = setTimeout(() => { + debug('timeout'); + this.sendStop = null; + this.sendSchedule(); + }, 25); } -}; -// For polling transports in a situation when in the message callback, -// new message is being send. If the sending connection was started -// before receiving one, it is possible to saturate the network and -// timeout due to the lack of receiving socket. To avoid that we delay -// sending messages by some small time, in order to let receiving -// connection be started beforehand. This is only a halfmeasure and -// does not fix the big problem, but it does make the tests go more -// stable on slow networks. -BufferedSender.prototype.sendScheduleWait = function() { - debug('sendScheduleWait'); - var self = this; - var tref; - this.sendStop = function() { - debug('sendStop'); - self.sendStop = null; - clearTimeout(tref); - }; - tref = setTimeout(function() { - debug('timeout'); - self.sendStop = null; - self.sendSchedule(); - }, 25); -}; - -BufferedSender.prototype.sendSchedule = function() { - debug('sendSchedule', this.sendBuffer.length); - var self = this; - if (this.sendBuffer.length > 0) { - var payload = '[' + this.sendBuffer.join(',') + ']'; - this.sendStop = this.sender(this.url, payload, function(err) { - self.sendStop = null; - if (err) { - debug('error', err); - self.emit('close', err.code || 1006, 'Sending error: ' + err); - self.close(); - } else { - self.sendScheduleWait(); - } - }); - this.sendBuffer = []; + sendSchedule() { + debug('sendSchedule', this.sendBuffer.length); + if (this.sendBuffer.length > 0) { + const payload = '[' + this.sendBuffer.join(',') + ']'; + this.sendStop = this.sender(this.url, payload, error => { + this.sendStop = null; + if (error) { + debug('error', error); + this.emit('close', error.code || 1006, 'Sending error: ' + error); + this.close(); + } else { + this.sendScheduleWait(); + } + }); + this.sendBuffer = []; + } } -}; -BufferedSender.prototype._cleanup = function() { - debug('_cleanup'); - this.removeAllListeners(); -}; + _cleanup() { + debug('_cleanup'); + this.removeAllListeners(); + } -BufferedSender.prototype.close = function() { - debug('close'); - this._cleanup(); - if (this.sendStop) { - this.sendStop(); - this.sendStop = null; + close() { + debug('close'); + this._cleanup(); + if (this.sendStop) { + this.sendStop(); + this.sendStop = null; + } } -}; +} -module.exports = BufferedSender; +export default BufferedSender; diff --git a/lib/transport/lib/iframe-wrap.js b/lib/transport/lib/iframe-wrap.js index eb0e881d..abe02d26 100644 --- a/lib/transport/lib/iframe-wrap.js +++ b/lib/transport/lib/iframe-wrap.js @@ -1,33 +1,27 @@ -'use strict'; +import IframeTransport from '../iframe.js'; +import {extend} from '../../utils/object.js'; -var inherits = require('inherits') - , IframeTransport = require('../iframe') - , objectUtils = require('../../utils/object') - ; - -module.exports = function(transport) { - - function IframeWrapTransport(transUrl, baseUrl) { - IframeTransport.call(this, transport.transportName, transUrl, baseUrl); - } +export default function iframeWrap(transport) { + const wrappedClass = class IframeWrapTransport extends IframeTransport { + constructor(transUrl, baseUrl) { + super(transport.transportName, transUrl, baseUrl); + } - inherits(IframeWrapTransport, IframeTransport); + static enabled(url, info) { + if (!global.document) { + return false; + } - IframeWrapTransport.enabled = function(url, info) { - if (!global.document) { - return false; + const iframeInfo = extend({}, info); + iframeInfo.sameOrigin = true; + return transport.enabled(iframeInfo) && IframeTransport.enabled(); } - var iframeInfo = objectUtils.extend({}, info); - iframeInfo.sameOrigin = true; - return transport.enabled(iframeInfo) && IframeTransport.enabled(); + static transportName = 'iframe-' + transport.transportName; + static needBody = true; + static roundTrips = IframeTransport.roundTrips + transport.roundTrips - 1; // Html, javascript (2) + transport - no CORS (1) + static facadeTransport = transport; }; - IframeWrapTransport.transportName = 'iframe-' + transport.transportName; - IframeWrapTransport.needBody = true; - IframeWrapTransport.roundTrips = IframeTransport.roundTrips + transport.roundTrips - 1; // html, javascript (2) + transport - no CORS (1) - - IframeWrapTransport.facadeTransport = transport; - - return IframeWrapTransport; -}; + return wrappedClass; +} diff --git a/lib/transport/lib/polling.js b/lib/transport/lib/polling.js index 8108432b..dbd558e5 100644 --- a/lib/transport/lib/polling.js +++ b/lib/transport/lib/polling.js @@ -1,57 +1,49 @@ -'use strict'; - -var inherits = require('inherits') - , EventEmitter = require('events').EventEmitter - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:polling'); -} - -function Polling(Receiver, receiveUrl, AjaxObject) { - debug(receiveUrl); - EventEmitter.call(this); - this.Receiver = Receiver; - this.receiveUrl = receiveUrl; - this.AjaxObject = AjaxObject; - this._scheduleReceiver(); -} - -inherits(Polling, EventEmitter); - -Polling.prototype._scheduleReceiver = function() { - debug('_scheduleReceiver'); - var self = this; - var poll = this.poll = new this.Receiver(this.receiveUrl, this.AjaxObject); - - poll.on('message', function(msg) { - debug('message', msg); - self.emit('message', msg); - }); - - poll.once('close', function(code, reason) { - debug('close', code, reason, self.pollIsClosing); - self.poll = poll = null; +import {EventEmitter} from 'node:events'; +import debugFunc from './utils/debug.js'; + +const debug = debugFunc('sockjs-client:polling'); + +class Polling extends EventEmitter { + constructor(Receiver, receiveUrl, AjaxObject) { + debug(receiveUrl); + super(); + this.Receiver = Receiver; + this.receiveUrl = receiveUrl; + this.AjaxObject = AjaxObject; + this._scheduleReceiver(); + } - if (!self.pollIsClosing) { - if (reason === 'network') { - self._scheduleReceiver(); - } else { - self.emit('close', code || 1006, reason); - self.removeAllListeners(); + _scheduleReceiver() { + debug('_scheduleReceiver'); + this.poll = new this.Receiver(this.receiveUrl, this.AjaxObject); + this.poll.on('message', message => { + debug('message', message); + this.emit('message', message); + }); + + this.poll.once('close', (code, reason) => { + debug('close', code, reason, this.pollIsClosing); + this.poll = null; + + if (!this.pollIsClosing) { + if (reason === 'network') { + this._scheduleReceiver(); + } else { + this.emit('close', code || 1006, reason); + this.removeAllListeners(); + } } - } - }); -}; + }); + } -Polling.prototype.abort = function() { - debug('abort'); - this.removeAllListeners(); - this.pollIsClosing = true; - if (this.poll) { - this.poll.abort(); + abort() { + debug('abort'); + this.removeAllListeners(); + this.pollIsClosing = true; + if (this.poll) { + this.poll.abort(); + } } -}; +} -module.exports = Polling; +export default Polling; diff --git a/lib/transport/lib/sender-receiver.js b/lib/transport/lib/sender-receiver.js index 8154994c..2cb2c1a2 100644 --- a/lib/transport/lib/sender-receiver.js +++ b/lib/transport/lib/sender-receiver.js @@ -1,45 +1,39 @@ -'use strict'; +import {addPath} from '../../utils/url.js'; +import debugFunc from './utils/debug.js'; +import BufferedSender from './buffered-sender.js'; +import Polling from './polling.js'; -var inherits = require('inherits') - , urlUtils = require('../../utils/url') - , BufferedSender = require('./buffered-sender') - , Polling = require('./polling') - ; +const debug = debugFunc('sockjs-client:sender-receiver'); -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:sender-receiver'); -} - -function SenderReceiver(transUrl, urlSuffix, senderFunc, Receiver, AjaxObject) { - var pollUrl = urlUtils.addPath(transUrl, urlSuffix); - debug(pollUrl); - var self = this; - BufferedSender.call(this, transUrl, senderFunc); - - this.poll = new Polling(Receiver, pollUrl, AjaxObject); - this.poll.on('message', function(msg) { - debug('poll message', msg); - self.emit('message', msg); - }); - this.poll.once('close', function(code, reason) { - debug('poll close', code, reason); - self.poll = null; - self.emit('close', code, reason); - self.close(); - }); -} +class SenderReceiver extends BufferedSender { + // eslint-disable-next-line max-params + constructor(transUrl, urlSuffix, senderFunc, Receiver, AjaxObject) { + const pollUrl = addPath(transUrl, urlSuffix); + debug(pollUrl); + super(transUrl, senderFunc); -inherits(SenderReceiver, BufferedSender); + this.poll = new Polling(Receiver, pollUrl, AjaxObject); + this.poll.on('message', message => { + debug('poll message', message); + this.emit('message', message); + }); + this.poll.once('close', (code, reason) => { + debug('poll close', code, reason); + this.poll = null; + this.emit('close', code, reason); + this.close(); + }); + } -SenderReceiver.prototype.close = function() { - BufferedSender.prototype.close.call(this); - debug('close'); - this.removeAllListeners(); - if (this.poll) { - this.poll.abort(); - this.poll = null; + close() { + super.close(); + debug('close'); + this.removeAllListeners(); + if (this.poll) { + this.poll.abort(); + this.poll = null; + } } -}; +} -module.exports = SenderReceiver; +export default SenderReceiver; diff --git a/lib/transport/receiver/eventsource.js b/lib/transport/receiver/eventsource.js index f9b3cc96..870963b6 100644 --- a/lib/transport/receiver/eventsource.js +++ b/lib/transport/receiver/eventsource.js @@ -1,63 +1,58 @@ -'use strict'; - -var inherits = require('inherits') - , EventEmitter = require('events').EventEmitter - , EventSourceDriver = require('eventsource') - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:receiver:eventsource'); -} - -function EventSourceReceiver(url) { - debug(url); - EventEmitter.call(this); +import {EventEmitter} from 'node:events'; +import EventSourceDriver from 'eventsource'; +import debugFunc from './utils/debug.js'; + +const debug = debugFunc('sockjs-client:receiver:eventsource'); + +class EventSourceReceiver extends EventEmitter { + constructor(url) { + debug(url); + super(); + this.es = new EventSourceDriver(url); + this.es.addEventListener('message', this._esmessage); + this.es.addEventListener('error', this._eserror); + } - var self = this; - var es = this.es = new EventSourceDriver(url); - es.onmessage = function(e) { - debug('message', e.data); - self.emit('message', decodeURI(e.data)); + _esmessage = evt => { + debug('message', evt.data); + this.emit('message', decodeURI(evt.data)); }; - es.onerror = function(e) { - debug('error', es.readyState, e); + + _eserror = evt => { + debug('error', this.es.readyState, evt); // ES on reconnection has readyState = 0 or 1. // on network error it's CLOSED = 2 - var reason = (es.readyState !== 2 ? 'network' : 'permanent'); - self._cleanup(); - self._close(reason); + const reason = (this.es.readyState === 2 ? 'permanent' : 'network'); + this._cleanup(); + this._close(reason); }; -} -inherits(EventSourceReceiver, EventEmitter); - -EventSourceReceiver.prototype.abort = function() { - debug('abort'); - this._cleanup(); - this._close('user'); -}; - -EventSourceReceiver.prototype._cleanup = function() { - debug('cleanup'); - var es = this.es; - if (es) { - es.onmessage = es.onerror = null; - es.close(); - this.es = null; + abort() { + debug('abort'); + this._cleanup(); + this._close('user'); } -}; - -EventSourceReceiver.prototype._close = function(reason) { - debug('close', reason); - var self = this; - // Safari and chrome < 15 crash if we close window before - // waiting for ES cleanup. See: - // https://code.google.com/p/chromium/issues/detail?id=89155 - setTimeout(function() { - self.emit('close', null, reason); - self.removeAllListeners(); - }, 200); -}; - -module.exports = EventSourceReceiver; + + _cleanup() { + debug('cleanup'); + if (this.es) { + this.es.removeEventListener('error', this._eserror); + this.es.removeEventListener('message', this._esmessage); + this.es.close(); + this.es = null; + } + } + + _close(reason) { + debug('close', reason); + // Safari and chrome < 15 crash if we close window before + // waiting for ES cleanup. See: + // https://code.google.com/p/chromium/issues/detail?id=89155 + setTimeout(() => { + this.emit('close', null, reason); + this.removeAllListeners(); + }, 200); + } +} + +export default EventSourceReceiver; diff --git a/lib/transport/receiver/htmlfile.js b/lib/transport/receiver/htmlfile.js index 38850319..22f86052 100644 --- a/lib/transport/receiver/htmlfile.js +++ b/lib/transport/receiver/htmlfile.js @@ -1,87 +1,84 @@ -'use strict'; +import {EventEmitter} from 'node:events'; +import {addQuery} from '../../utils/url.js'; +import {polluteGlobalNamespace, iframeEnabled, createHtmlfile, createIframe, WPrefix} from '../../utils/iframe.js'; +import {string as randomString} from '../../utils/random.js'; +import debugFunc from './utils/debug.js'; -var inherits = require('inherits') - , iframeUtils = require('../../utils/iframe') - , urlUtils = require('../../utils/url') - , EventEmitter = require('events').EventEmitter - , random = require('../../utils/random') - ; +const debug = debugFunc('sockjs-client:receiver:htmlfile'); -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:receiver:htmlfile'); -} - -function HtmlfileReceiver(url) { - debug(url); - EventEmitter.call(this); - var self = this; - iframeUtils.polluteGlobalNamespace(); +class HtmlfileReceiver extends EventEmitter { + constructor(url) { + debug(url); + super(); + polluteGlobalNamespace(); - this.id = 'a' + random.string(6); - url = urlUtils.addQuery(url, 'c=' + decodeURIComponent(iframeUtils.WPrefix + '.' + this.id)); + this.id = 'a' + randomString(6); + url = addQuery(url, 'c=' + decodeURIComponent(WPrefix + '.' + this.id)); - debug('using htmlfile', HtmlfileReceiver.htmlfileEnabled); - var constructFunc = HtmlfileReceiver.htmlfileEnabled ? - iframeUtils.createHtmlfile : iframeUtils.createIframe; + debug('using htmlfile', HtmlfileReceiver.htmlfileEnabled); + const constructFunc = HtmlfileReceiver.htmlfileEnabled + ? createHtmlfile : createIframe; - global[iframeUtils.WPrefix][this.id] = { - start: function() { - debug('start'); - self.iframeObj.loaded(); - } - , message: function(data) { - debug('message', data); - self.emit('message', data); - } - , stop: function() { - debug('stop'); - self._cleanup(); - self._close('network'); - } - }; - this.iframeObj = constructFunc(url, function() { - debug('callback'); - self._cleanup(); - self._close('permanent'); - }); -} + global[WPrefix][this.id] = { + start: function () { + debug('start'); + this.iframeObj.loaded(); + }.bind(this), + message: function (data) { + debug('message', data); + this.emit('message', data); + }.bind(this), + stop: function () { + debug('stop'); + this._cleanup(); + this._close('network'); + }.bind(this), + }; + this.iframeObj = constructFunc(url, () => { + debug('callback'); + this._cleanup(); + this._close('permanent'); + }); + } -inherits(HtmlfileReceiver, EventEmitter); + abort() { + debug('abort'); + this._cleanup(); + this._close('user'); + } -HtmlfileReceiver.prototype.abort = function() { - debug('abort'); - this._cleanup(); - this._close('user'); -}; + _cleanup() { + debug('_cleanup'); + if (this.iframeObj) { + this.iframeObj.cleanup(); + this.iframeObj = null; + } -HtmlfileReceiver.prototype._cleanup = function() { - debug('_cleanup'); - if (this.iframeObj) { - this.iframeObj.cleanup(); - this.iframeObj = null; + delete global[WPrefix][this.id]; } - delete global[iframeUtils.WPrefix][this.id]; -}; -HtmlfileReceiver.prototype._close = function(reason) { - debug('_close', reason); - this.emit('close', null, reason); - this.removeAllListeners(); -}; + _close(reason) { + debug('_close', reason); + this.emit('close', null, reason); + this.removeAllListeners(); + } -HtmlfileReceiver.htmlfileEnabled = false; + static enabled = false; + static htmlfileEnabled = (function () { + let htmlfilePresent = false; + // Obfuscate to avoid firewalls + const axo = ['Active', 'Object'].join('X'); + if (axo in global) { + try { + htmlfilePresent = Boolean(new global[axo]('htmlfile')); + } catch { + // Intentionally empty + } + } -// obfuscate to avoid firewalls -var axo = ['Active'].concat('Object').join('X'); -if (axo in global) { - try { - HtmlfileReceiver.htmlfileEnabled = !!new global[axo]('htmlfile'); - } catch (x) { - // intentionally empty - } + HtmlfileReceiver.enabled = htmlfilePresent || iframeEnabled; + return htmlfilePresent; + })(); } -HtmlfileReceiver.enabled = HtmlfileReceiver.htmlfileEnabled || iframeUtils.iframeEnabled; - -module.exports = HtmlfileReceiver; +export default HtmlfileReceiver; diff --git a/lib/transport/receiver/jsonp.js b/lib/transport/receiver/jsonp.js index 365fecc2..7c84d7d9 100644 --- a/lib/transport/receiver/jsonp.js +++ b/lib/transport/receiver/jsonp.js @@ -1,183 +1,193 @@ -'use strict'; - -var utils = require('../../utils/iframe') - , random = require('../../utils/random') - , browser = require('../../utils/browser') - , urlUtils = require('../../utils/url') - , inherits = require('inherits') - , EventEmitter = require('events').EventEmitter - ; - -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:receiver:jsonp'); -} +import {EventEmitter} from 'node:events'; +import {polluteGlobalNamespace, WPrefix} from '../../utils/iframe.js'; +import {string as randomString} from '../../utils/random.js'; +import {isOpera} from '../../utils/browser.js'; +import {addQuery} from '../../utils/url.js'; +import debugFunc from './utils/debug.js'; -function JsonpReceiver(url) { - debug(url); - var self = this; - EventEmitter.call(this); +const debug = debugFunc('sockjs-client:receiver:jsonp'); - utils.polluteGlobalNamespace(); +class JsonpReceiver extends EventEmitter { + constructor(url) { + debug(url); + super(); - this.id = 'a' + random.string(6); - var urlWithId = urlUtils.addQuery(url, 'c=' + encodeURIComponent(utils.WPrefix + '.' + this.id)); + polluteGlobalNamespace(); - global[utils.WPrefix][this.id] = this._callback.bind(this); - this._createScript(urlWithId); + this.id = 'a' + randomString(6); + const urlWithId = addQuery(url, 'c=' + encodeURIComponent(WPrefix + '.' + this.id)); - // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty. - this.timeoutId = setTimeout(function() { - debug('timeout'); - self._abort(new Error('JSONP script loaded abnormally (timeout)')); - }, JsonpReceiver.timeout); -} + global[WPrefix][this.id] = this._callback.bind(this); + this._createScript(urlWithId); -inherits(JsonpReceiver, EventEmitter); + // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty. + this.timeoutId = setTimeout(() => { + debug('timeout'); + this._abort(new Error('JSONP script loaded abnormally (timeout)')); + }, JsonpReceiver.timeout); + } -JsonpReceiver.prototype.abort = function() { - debug('abort'); - if (global[utils.WPrefix][this.id]) { - var err = new Error('JSONP user aborted read'); - err.code = 1000; - this._abort(err); + abort() { + debug('abort'); + if (global[WPrefix][this.id]) { + const error = new Error('JSONP user aborted read'); + error.code = 1000; + this._abort(error); + } } -}; -JsonpReceiver.timeout = 35000; -JsonpReceiver.scriptErrorTimeout = 1000; + static timeout = 35_000; + static scriptErrorTimeout = 1000; -JsonpReceiver.prototype._callback = function(data) { - debug('_callback', data); - this._cleanup(); + _callback(data) { + debug('_callback', data); + this._cleanup(); - if (this.aborting) { - return; - } + if (this.aborting) { + return; + } - if (data) { - debug('message', data); - this.emit('message', data); - } - this.emit('close', null, 'network'); - this.removeAllListeners(); -}; - -JsonpReceiver.prototype._abort = function(err) { - debug('_abort', err); - this._cleanup(); - this.aborting = true; - this.emit('close', err.code, err.message); - this.removeAllListeners(); -}; - -JsonpReceiver.prototype._cleanup = function() { - debug('_cleanup'); - clearTimeout(this.timeoutId); - if (this.script2) { - this.script2.parentNode.removeChild(this.script2); - this.script2 = null; + if (data) { + debug('message', data); + this.emit('message', data); + } + + this.emit('close', null, 'network'); + this.removeAllListeners(); } - if (this.script) { - var script = this.script; - // Unfortunately, you can't really abort script loading of - // the script. - script.parentNode.removeChild(script); - script.onreadystatechange = script.onerror = - script.onload = script.onclick = null; - this.script = null; + + _abort(error) { + debug('_abort', error); + this._cleanup(); + this.aborting = true; + this.emit('close', error.code, error.message); + this.removeAllListeners(); } - delete global[utils.WPrefix][this.id]; -}; - -JsonpReceiver.prototype._scriptError = function() { - debug('_scriptError'); - var self = this; - if (this.errorTimer) { - return; + + _cleanup() { + debug('_cleanup'); + clearTimeout(this.timeoutId); + if (this.script2) { + this.script2.remove(); + this.script2 = null; + } + + if (this.script) { + // Unfortunately, you can't really abort script loading of + // the script. + this.script.remove(); + // We don't have a reference to the onclick handler + // eslint-disable-next-line unicorn/prefer-add-event-listener + this.script.onclick = null; + this.script.removeEventListener('load', this._scriptLoad); + this.script.removeEventListener('error', this._scriptError); + this.script.removeEventListener('readystatechange', this._scriptReadyStateChange); + this.script = null; + } + + delete global[WPrefix][this.id]; } - this.errorTimer = setTimeout(function() { - if (!self.loadedOkay) { - self._abort(new Error('JSONP script loaded abnormally (onerror)')); + _scriptError = () => { + debug('_scriptError'); + if (this.errorTimer) { + return; } - }, JsonpReceiver.scriptErrorTimeout); -}; - -JsonpReceiver.prototype._createScript = function(url) { - debug('_createScript', url); - var self = this; - var script = this.script = global.document.createElement('script'); - var script2; // Opera synchronous load trick. - - script.id = 'a' + random.string(8); - script.src = url; - script.type = 'text/javascript'; - script.charset = 'UTF-8'; - script.onerror = this._scriptError.bind(this); - script.onload = function() { + + this.errorTimer = setTimeout(() => { + if (!this.loadedOkay) { + this._abort(new Error('JSONP script loaded abnormally (onerror)')); + } + }, JsonpReceiver.scriptErrorTimeout); + }; + + _scriptLoad = () => { debug('onload'); - self._abort(new Error('JSONP script loaded abnormally (onload)')); + this._abort(new Error('JSONP script loaded abnormally (onload)')); }; - // IE9 fires 'error' event after onreadystatechange or before, in random order. - // Use loadedOkay to determine if actually errored - script.onreadystatechange = function() { - debug('onreadystatechange', script.readyState); - if (/loaded|closed/.test(script.readyState)) { - if (script && script.htmlFor && script.onclick) { - self.loadedOkay = true; + _scriptReadyStateChange = () => { + if (!this.script) { + return; + } + + debug('onreadystatechange', this.script.readyState); + if (/loaded|closed/.test(this.script.readyState)) { + if (this.script.htmlFor && this.script.onclick) { + this.loadedOkay = true; try { // In IE, actually execute the script. - script.onclick(); - } catch (x) { - // intentionally empty + this.script.onclick(); + } catch { + // Intentionally empty } } - if (script) { - self._abort(new Error('JSONP script loaded abnormally (onreadystatechange)')); + + if (this.script) { + this._abort(new Error('JSONP script loaded abnormally (onreadystatechange)')); } } }; - // IE: event/htmlFor/onclick trick. - // One can't rely on proper order for onreadystatechange. In order to - // make sure, set a 'htmlFor' and 'event' properties, so that - // script code will be installed as 'onclick' handler for the - // script object. Later, onreadystatechange, manually execute this - // code. FF and Chrome doesn't work with 'event' and 'htmlFor' - // set. For reference see: - // http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html - // Also, read on that about script ordering: - // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order - if (typeof script.async === 'undefined' && global.document.attachEvent) { - // According to mozilla docs, in recent browsers script.async defaults - // to 'true', so we may use it to detect a good browser: - // https://developer.mozilla.org/en/HTML/Element/script - if (!browser.isOpera()) { - // Naively assume we're in IE - try { - script.htmlFor = script.id; - script.event = 'onclick'; - } catch (x) { - // intentionally empty + + _createScript(url) { + debug('_createScript', url); + this.script = global.document.createElement('script'); + let script2; // Opera synchronous load trick. + + this.script.id = 'a' + randomString(8); + this.script.src = url; + this.script.type = 'text/javascript'; + this.script.charset = 'UTF-8'; + this.script.addEventListener('error', this._scriptError); + this.script.addEventListener('load', this._scriptLoad); + + // IE9 fires 'error' event after onreadystatechange or before, in random order. + // Use loadedOkay to determine if actually errored + this.script.addEventListener('readystatechange', this._scriptReadyStateChange); + + // IE: event/htmlFor/onclick trick. + // One can't rely on proper order for onreadystatechange. In order to + // make sure, set a 'htmlFor' and 'event' properties, so that + // script code will be installed as 'onclick' handler for the + // script object. Later, onreadystatechange, manually execute this + // code. FF and Chrome doesn't work with 'event' and 'htmlFor' + // set. For reference see: + // http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html + // Also, read on that about script ordering: + // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order + if (typeof this.script.async === 'undefined' && global.document.attachEvent) { + // According to mozilla docs, in recent browsers script.async defaults + // to 'true', so we may use it to detect a good browser: + // https://developer.mozilla.org/en/HTML/Element/script + if (isOpera()) { + // Opera, second sync script hack + this.script2 = global.document.createElement('script'); + this.script2.text = 'try{const a = document.getElementById(\'' + this.script.id + '\'); if(a)a.onerror();}catch(x){};'; + this.script2.async = false; + this.script.async = false; + } else { + // Naively assume we're in IE + try { + this.script.htmlFor = this.script.id; + this.script.event = 'onclick'; + } catch { + // Intentionally empty + } + + this.script.async = true; } - script.async = true; - } else { - // Opera, second sync script hack - script2 = this.script2 = global.document.createElement('script'); - script2.text = "try{var a = document.getElementById('" + script.id + "'); if(a)a.onerror();}catch(x){};"; - script.async = script2.async = false; } - } - if (typeof script.async !== 'undefined') { - script.async = true; - } - var head = global.document.getElementsByTagName('head')[0]; - head.insertBefore(script, head.firstChild); - if (script2) { - head.insertBefore(script2, head.firstChild); + if (typeof this.script.async !== 'undefined') { + this.script.async = true; + } + + const head = global.document.querySelectorAll('head')[0]; + head.insertBefore(this.script, head.firstChild); + if (script2) { + head.insertBefore(script2, head.firstChild); + } } -}; +} -module.exports = JsonpReceiver; +export default JsonpReceiver; diff --git a/lib/transport/receiver/xhr.js b/lib/transport/receiver/xhr.js index 8ec7bc84..b738ce50 100644 --- a/lib/transport/receiver/xhr.js +++ b/lib/transport/receiver/xhr.js @@ -1,70 +1,65 @@ -'use strict'; +import {EventEmitter} from 'node:events'; +import debugFunc from './utils/debug.js'; -var inherits = require('inherits') - , EventEmitter = require('events').EventEmitter - ; +const debug = debugFunc('sockjs-client:receiver:xhr'); -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:receiver:xhr'); -} +class XhrReceiver extends EventEmitter { + constructor(url, AjaxObject) { + debug(url); + super(); -function XhrReceiver(url, AjaxObject) { - debug(url); - EventEmitter.call(this); - var self = this; + this.bufferPosition = 0; - this.bufferPosition = 0; + this.xo = new AjaxObject('POST', url, null); + this.xo.on('chunk', this._chunkHandler.bind(this)); + this.xo.once('finish', (status, text) => { + debug('finish', status, text); + this._chunkHandler(status, text); + this.xo = null; + const reason = status === 200 ? 'network' : 'permanent'; + debug('close', reason); + this.emit('close', null, reason); + this._cleanup(); + }); + } - this.xo = new AjaxObject('POST', url, null); - this.xo.on('chunk', this._chunkHandler.bind(this)); - this.xo.once('finish', function(status, text) { - debug('finish', status, text); - self._chunkHandler(status, text); - self.xo = null; - var reason = status === 200 ? 'network' : 'permanent'; - debug('close', reason); - self.emit('close', null, reason); - self._cleanup(); - }); -} + _chunkHandler(status, text) { + debug('_chunkHandler', status); + if (status !== 200 || !text) { + return; + } -inherits(XhrReceiver, EventEmitter); + for (let idx = -1; ; this.bufferPosition += idx + 1) { + const buf = text.slice(this.bufferPosition); + idx = buf.indexOf('\n'); + if (idx === -1) { + break; + } -XhrReceiver.prototype._chunkHandler = function(status, text) { - debug('_chunkHandler', status); - if (status !== 200 || !text) { - return; + const message = buf.slice(0, idx); + if (message) { + debug('message', message); + this.emit('message', message); + } + } } - for (var idx = -1; ; this.bufferPosition += idx + 1) { - var buf = text.slice(this.bufferPosition); - idx = buf.indexOf('\n'); - if (idx === -1) { - break; - } - var msg = buf.slice(0, idx); - if (msg) { - debug('message', msg); - this.emit('message', msg); - } + _cleanup() { + debug('_cleanup'); + this.removeAllListeners(); } -}; -XhrReceiver.prototype._cleanup = function() { - debug('_cleanup'); - this.removeAllListeners(); -}; + abort() { + debug('abort'); + if (this.xo) { + this.xo.close(); + debug('close'); + this.emit('close', null, 'user'); + this.xo = null; + } -XhrReceiver.prototype.abort = function() { - debug('abort'); - if (this.xo) { - this.xo.close(); - debug('close'); - this.emit('close', null, 'user'); - this.xo = null; + this._cleanup(); } - this._cleanup(); -}; +} -module.exports = XhrReceiver; +export default XhrReceiver; diff --git a/lib/transport/sender/jsonp.js b/lib/transport/sender/jsonp.js index a0326ad8..aff75ef1 100644 --- a/lib/transport/sender/jsonp.js +++ b/lib/transport/sender/jsonp.js @@ -1,23 +1,19 @@ -'use strict'; +import {string as randomString} from '../../utils/random.js'; +import {addQuery, addPath} from '../../utils/url.js'; +import debugFunc from './utils/debug.js'; -var random = require('../../utils/random') - , urlUtils = require('../../utils/url') - ; +const debug = debugFunc('sockjs-client:sender:jsonp'); -var debug = function() {}; -if (process.env.NODE_ENV !== 'production') { - debug = require('debug')('sockjs-client:sender:jsonp'); -} - -var form, area; +let form; +let area; function createIframe(id) { debug('createIframe', id); try { - // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) + // Ie6 dynamic iframes with target="" support (thanks Chris Lambacher) return global.document.createElement('