xhr-shaper
Advanced tools
Comparing version 2.2.1 to 2.2.2
133
demo.js
@@ -27,4 +27,6 @@ XHRShaper.useGlobal(); | ||
stats.timestamps.push(Date.now() - stats.t0); | ||
stats.bytes.push(bytes); | ||
stats.timestamps.push(Date.now() - stats.t0); | ||
stats.bytes.push(bytes); | ||
console.log('stats pushed:', stats); | ||
} | ||
@@ -38,15 +40,17 @@ | ||
Plotly.plot(el, [{ | ||
x: stats.timestamps, | ||
y: stats.bytes | ||
Plotly.plot( | ||
el, | ||
[{ | ||
x: stats.timestamps, | ||
y: stats.bytes | ||
}], | ||
{ | ||
margin: { t: 0 } | ||
} | ||
); | ||
margin: { t: 0 } | ||
} | ||
); | ||
document.querySelector('#throughput').innerHTML = stats.throughput; | ||
document.querySelector('#duration').innerHTML = stats.totalDuration; | ||
document.querySelector('#throughput').innerHTML = stats.throughput; | ||
document.querySelector('#duration').innerHTML = stats.totalDuration; | ||
document.querySelector('#status').innerHTML = 'Ready'; | ||
document.querySelector('#status').innerHTML = 'Ready'; | ||
} | ||
@@ -79,76 +83,81 @@ | ||
function makeRequest(url, done, minLatency, maxBandwidth) { | ||
var loaded, total; | ||
function makeRequest(url, onRequestDone, minLatency, maxBandwidth) { | ||
var loaded, total; | ||
console.log('url:', url); | ||
console.log('url:', url); | ||
requestActive = true; | ||
requestActive = true; | ||
var xhr = new XMLHttpRequest(); | ||
var xhr = new XMLHttpRequest(); | ||
xhr.shaper.minLatency = minLatency; | ||
xhr.shaper.maxBandwidth = maxBandwidth; | ||
xhr.caching = false; | ||
xhr.onreadystatechange = function(e) { | ||
console.log('readyState: ' + xhr.readyState); | ||
xhr.shaper.minLatency = minLatency; | ||
xhr.shaper.maxBandwidth = maxBandwidth; | ||
if (xhr.readyState === 4) { | ||
doneTime = Date.now(); | ||
var duration = doneTime - reqTime; | ||
var bitrate = Math.round(8 * total / duration); | ||
xhr.onreadystatechange = function(e) { | ||
console.log('readyState changed: ' + xhr.readyState); | ||
stats.totalDuration = duration; | ||
stats.throughput = bitrate; | ||
if (xhr.readyState === 4) { | ||
doneTime = Date.now(); | ||
var duration = doneTime - reqTime; | ||
var bitrate = Math.round(8 * total / duration); | ||
console.log('Loaded ' + total + ' bytes in ' + duration + ' ms, computed bitrate: ' + bitrate + ' kbps'); | ||
} | ||
}; | ||
xhr.onprogress = function(e) { | ||
loaded = e.loaded; | ||
total = e.total; | ||
stats.totalDuration = duration; | ||
stats.throughput = bitrate; | ||
pushStats(e.loaded, e.total); | ||
console.log('Loaded ' + total + ' bytes in ' + duration + ' ms, computed bitrate: ' + bitrate + ' kbps'); | ||
} | ||
}; | ||
xhr.onprogress = function(e) { | ||
loaded = e.loaded; | ||
total = e.total; | ||
console.log('Progress: ' + e.loaded + ' of ' + e.total); | ||
}; | ||
pushStats(e.loaded, e.total); | ||
xhr.onload = function(e) { | ||
console.log('Progress: ' + e.loaded + ' of ' + e.total); | ||
}; | ||
if (xhr.readyState < 4) { | ||
console.warn('onload called with readyState:', xhr.readyState, ' id:', xhr.id); | ||
return; | ||
} | ||
xhr.onload = function(e) { | ||
console.log('Loading done'); | ||
//console.log(e); | ||
if (xhr.readyState < 4) { | ||
console.warn('onload called with readyState:', xhr.readyState, ' id:', xhr.id); | ||
return; | ||
} | ||
console.log('readyState:', xhr.readyState); | ||
console.log('Loading done'); | ||
//console.log(e); | ||
if (xhr.response.byteLength < 1024) { | ||
console.log(new TextDecoder("utf-8").decode(xhr.response)); | ||
} | ||
console.log('readyState:', xhr.readyState); | ||
done(xhr); | ||
} | ||
if (xhr.response.byteLength < 1024) { | ||
console.log(new TextDecoder("utf-8").decode(xhr.response)); | ||
} | ||
xhr.onloadend = function(e) { | ||
console.log('Loading ended'); | ||
//console.log(e); | ||
} | ||
xhr.withCredentials = false; | ||
xhr.open('GET', url, true); | ||
xhr.responseType = 'arraybuffer'; | ||
} | ||
console.debug('Max bandwidth: ' + xhr.shaper.maxBandwidth); | ||
console.debug('Min latency: ' + xhr.shaper.minLatency); | ||
xhr.onloadend = function(e) { | ||
console.log('Loading ended'); | ||
var reqTime = Date.now(), doneTime; | ||
pushStats(loaded, total); | ||
xhr.send(); | ||
onRequestDone(xhr); | ||
} | ||
resetStats(); | ||
pushStats(0, 0); | ||
xhr.withCredentials = false; | ||
xhr.open('GET', url, true); | ||
xhr.responseType = 'arraybuffer'; | ||
return xhr; | ||
console.debug('Max bandwidth: ' + xhr.shaper.maxBandwidth); | ||
console.debug('Min latency: ' + xhr.shaper.minLatency); | ||
var reqTime = Date.now(), doneTime; | ||
xhr.send(); | ||
resetStats(); | ||
pushStats(0, 0); | ||
return xhr; | ||
} |
@@ -304,14 +304,22 @@ this["XHRShaper"] = | ||
var _xhr = __webpack_require__(0); | ||
var _xhrProxy = __webpack_require__(0); | ||
var _xhr2 = _interopRequireDefault(_xhr); | ||
var _xhrProxy2 = _interopRequireDefault(_xhrProxy); | ||
var _shaper = __webpack_require__(4); | ||
var _shaper = __webpack_require__(6); | ||
var _shaper2 = _interopRequireDefault(_shaper); | ||
var _setupThrottledXhr = __webpack_require__(3); | ||
var _setupThrottledXhr = __webpack_require__(5); | ||
var _setupThrottledXhr2 = _interopRequireDefault(_setupThrottledXhr); | ||
var _setupCachedXhr = __webpack_require__(4); | ||
var _setupCachedXhr2 = _interopRequireDefault(_setupCachedXhr); | ||
var _cache = __webpack_require__(3); | ||
var _cache2 = _interopRequireDefault(_cache); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -325,2 +333,4 @@ | ||
var DEBUG = false; | ||
var PASSTHROUGH_EVENTS = ['loadstart', 'timeout', 'abort', 'error']; | ||
@@ -334,6 +344,6 @@ | ||
var ThrottledXHR = function (_XHRProxy) { | ||
_inherits(ThrottledXHR, _XHRProxy); | ||
var XHR = function (_XHRProxy) { | ||
_inherits(XHR, _XHRProxy); | ||
_createClass(ThrottledXHR, null, [{ | ||
_createClass(XHR, null, [{ | ||
key: 'Shaper', | ||
@@ -343,13 +353,35 @@ get: function get() { | ||
} | ||
}, { | ||
key: 'cache', | ||
get: function get() { | ||
return _cache2.default; | ||
} | ||
}]); | ||
function ThrottledXHR() { | ||
_classCallCheck(this, ThrottledXHR); | ||
function XHR() { | ||
_classCallCheck(this, XHR); | ||
var _this = _possibleConstructorReturn(this, (ThrottledXHR.__proto__ || Object.getPrototypeOf(ThrottledXHR)).call(this)); | ||
var _this = _possibleConstructorReturn(this, (XHR.__proto__ || Object.getPrototypeOf(XHR)).call(this)); | ||
_this._cacheInstance = _cache2.default.instance; | ||
_this._shaper = new _shaper2.default(); | ||
_this._listenersMap = new Map(); | ||
_this._dispatchedEventsList = []; | ||
_this._response = null; | ||
_this._responseText = null; | ||
_this._responseXML = null; | ||
_this._headers = null; | ||
_this._readyState = 0; | ||
_this._caching = false; | ||
_this._cacheHit = false; | ||
_this._cacheWrite = true; | ||
_this._onSend = null; | ||
_this.addEventListener('loadend', function () { | ||
if (_this._caching && _this._cacheWrite) { | ||
DEBUG && console.log('CACHE WRITE:', _this._xhr.responseURL); | ||
_this._cacheInstance.put(_this._xhr.responseURL, _this._response, _this._responseText, _this._responseXML, null, _this._xhr.getAllResponseHeaders()); | ||
} | ||
}); | ||
(0, _setupThrottledXhr2.default)(_this._xhr, _this); | ||
@@ -359,3 +391,3 @@ return _this; | ||
_createClass(ThrottledXHR, [{ | ||
_createClass(XHR, [{ | ||
key: '_dispatchWrappedEventType', | ||
@@ -377,2 +409,63 @@ value: function _dispatchWrappedEventType(type) { | ||
}, { | ||
key: '_handleCacheHitOnSend', | ||
value: function _handleCacheHitOnSend(cachedResource) { | ||
var _this3 = this; | ||
this._readyState = 1; | ||
this._cacheHit = true; | ||
var onSend = function onSend() { | ||
setTimeout(function () { | ||
_this3._readyState = 2; | ||
_this3.onreadystatechange && _this3.onreadystatechange(); | ||
_this3._dispatchWrappedEventType('readystatechange'); | ||
_this3._readyState = 3; | ||
_this3.onreadystatechange && _this3.onreadystatechange(); | ||
_this3._dispatchWrappedEventType('readystatechange'); | ||
_this3.onloadstart && _this3.onloadstart(); | ||
_this3._dispatchWrappedEventType('loadstart'); | ||
_this3._response = cachedResource.data; | ||
_this3._responseText = cachedResource.dataText; | ||
_this3._responseXML = cachedResource.dataXML; | ||
_this3._headersAll = cachedResource.headers; | ||
_this3._readyState = 4; | ||
_this3.onreadystatechange && _this3.onreadystatechange(); | ||
_this3._dispatchWrappedEventType('readystatechange'); | ||
_this3.onreadystatechange && _this3.onreadystatechange(); | ||
_this3._dispatchWrappedEventType('readystatechange'); | ||
_this3.onload && _this3.onload(); | ||
_this3._dispatchWrappedEventType('load'); | ||
_this3.onloadend && _this3.onloadend(); | ||
_this3._dispatchWrappedEventType('loadend'); | ||
}, 0); | ||
}; | ||
this._onSend = onSend; | ||
} | ||
}, { | ||
key: '_setupWrappedResponseData', | ||
value: function _setupWrappedResponseData() { | ||
try { | ||
this._response = this._xhr.response; | ||
} catch (e) { | ||
DEBUG && console.warn(e); | ||
} | ||
try { | ||
this._responseText = this._xhr.responseText; | ||
} catch (e) { | ||
DEBUG && console.warn(e); | ||
} | ||
try { | ||
this._responseXML = this._xhr.responseXML; | ||
} catch (e) { | ||
DEBUG && console.warn(e); | ||
} | ||
} | ||
}, { | ||
key: '_setupWrappedHeaders', | ||
value: function _setupWrappedHeaders() { | ||
try { | ||
this._headersAll = this._xhr.getAllResponseHeaders(); | ||
} catch (e) {} | ||
} | ||
}, { | ||
key: 'addEventListener', | ||
@@ -382,3 +475,3 @@ value: function addEventListener(type, listener, optionsOrUseCapture, wantsUntrusted) { | ||
if (PASSTHROUGH_EVENTS.includes(type)) { | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'addEventListener', this).call(this, type, listener, optionsOrUseCapture, wantsUntrusted); | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'addEventListener', this).call(this, type, listener, optionsOrUseCapture, wantsUntrusted); | ||
} | ||
@@ -388,3 +481,3 @@ | ||
this._listenersMap.set(listener, listenerWrapper); | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'addEventListener', this).call(this, type, listenerWrapper, optionsOrUseCapture, wantsUntrusted); | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'addEventListener', this).call(this, type, listenerWrapper, optionsOrUseCapture, wantsUntrusted); | ||
} | ||
@@ -396,3 +489,3 @@ }, { | ||
if (PASSTHROUGH_EVENTS.includes(type)) { | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'removeEventListener', this).call(this, type, listener, optionsOrUseCapture); | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'removeEventListener', this).call(this, type, listener, optionsOrUseCapture); | ||
} | ||
@@ -405,10 +498,70 @@ | ||
this._listenersMap.delete(listener); | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'removeEventListener', this).call(this, type, listenerWrapper, optionsOrUseCapture); | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'removeEventListener', this).call(this, type, listenerWrapper, optionsOrUseCapture); | ||
} | ||
}, { | ||
key: 'dispatchEvent', | ||
value: function dispatchEvent(event) { | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'dispatchEvent', this).call(this, event); | ||
key: 'open', | ||
value: function open(method, url, async, user, password) { | ||
if (this._caching) { | ||
if (!this._cacheInstance) { | ||
throw new Error('no cache setup'); | ||
} | ||
DEBUG && console.log('CACHE GET:', url); | ||
var cachedResource = this._cacheInstance.get(url, false); | ||
if (cachedResource) { | ||
DEBUG && console.log('CACHE HIT'); | ||
this._handleCacheHitOnSend(cachedResource); | ||
} | ||
} | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'open', this).call(this, method, url, async, user, password); | ||
} | ||
}, { | ||
key: 'send', | ||
value: function send(data) { | ||
if (this._caching && this._cacheHit && this._onSend) { | ||
return this._onSend(); | ||
} | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'send', this).call(this, data); | ||
} | ||
}, { | ||
key: 'getAllResponseHeaders', | ||
value: function getAllResponseHeaders() { | ||
return this._headersAll; | ||
} | ||
}, { | ||
key: 'caching', | ||
get: function get() { | ||
return this._caching; | ||
}, | ||
set: function set(enabled) { | ||
this._caching = enabled; | ||
} | ||
}, { | ||
key: 'cacheInstance', | ||
set: function set(instance) { | ||
this._cacheInstance = instance; | ||
}, | ||
get: function get() { | ||
return this._cacheInstance; | ||
} | ||
}, { | ||
key: 'isCacheHit', | ||
get: function get() { | ||
return this._cacheHit; | ||
} | ||
}, { | ||
key: 'cacheWriteEnabled', | ||
set: function set(enable) { | ||
this._cacheWrite = enable; | ||
} | ||
}, { | ||
key: 'enableCacheWrite', | ||
get: function get() { | ||
return this._cacheWrite; | ||
} | ||
}, { | ||
key: 'shaper', | ||
@@ -419,2 +572,9 @@ get: function get() { | ||
}, { | ||
key: 'readyState', | ||
get: function get() { | ||
if (typeof this._readyState === 'number') { | ||
return this._readyState; | ||
} | ||
} | ||
}, { | ||
key: 'onloadend', | ||
@@ -451,8 +611,23 @@ set: function set(fn) { | ||
} | ||
}, { | ||
key: 'response', | ||
get: function get() { | ||
return this._response; | ||
} | ||
}, { | ||
key: 'responseText', | ||
get: function get() { | ||
return this._responseText; | ||
} | ||
}, { | ||
key: 'responseXML', | ||
get: function get() { | ||
return this._responseXML; | ||
} | ||
}]); | ||
return ThrottledXHR; | ||
}(_xhr2.default); | ||
return XHR; | ||
}(_xhrProxy2.default); | ||
exports.default = ThrottledXHR; | ||
exports.default = XHR; | ||
@@ -466,9 +641,9 @@ /***/ }), | ||
var _xhr = __webpack_require__(0); | ||
var _xhrProxy = __webpack_require__(0); | ||
var _xhr2 = _interopRequireDefault(_xhr); | ||
var _xhrProxy2 = _interopRequireDefault(_xhrProxy); | ||
var _throttledXhr = __webpack_require__(1); | ||
var _xhr = __webpack_require__(1); | ||
var _throttledXhr2 = _interopRequireDefault(_throttledXhr); | ||
var _xhr2 = _interopRequireDefault(_xhr); | ||
@@ -481,9 +656,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
// Overload native window constructor | ||
global.XMLHttpRequest = _throttledXhr2.default; | ||
global.XMLHttpRequest = _xhr2.default; | ||
} | ||
module.exports = { | ||
XMLHttpRequest: _throttledXhr2.default, | ||
ThrottledXHR: _throttledXhr2.default, | ||
XHRProxy: _xhr2.default, | ||
XMLHttpRequest: _xhr2.default, | ||
XHRProxy: _xhrProxy2.default, | ||
useGlobal: useGlobal | ||
@@ -500,4 +674,169 @@ }; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
var DEFAULT_ALLOW_UPDATES = false; | ||
var MAX_CACHE_SIZE_BYTES = 1024 * 1e6; // 1024 Mbytes | ||
var cache = new Map(); | ||
var bytesRead = 0; | ||
var bytesWritten = 0; | ||
var misses = 0; | ||
var hits = 0; | ||
var instance = { | ||
allowUpdates: DEFAULT_ALLOW_UPDATES, | ||
errorOnOverflow: false, | ||
get: function get(uri) { | ||
var onlyData = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; | ||
var resource = void 0; | ||
if (!cache.has(uri)) { | ||
misses++; | ||
return null; | ||
} | ||
hits++; | ||
resource = cache.get(uri); | ||
resource.accessedAt = Date.now(); | ||
if (typeof resource.data.byteLength === 'number') { | ||
bytesRead += resource.data.byteLength; | ||
} | ||
if (onlyData) { | ||
return resource.data; | ||
} else { | ||
return resource; | ||
} | ||
}, | ||
put: function put(uri, data, dataText, dataXML, headers, headersAll) { | ||
if (!instance.allowUpdates && cache.has(uri)) { | ||
throw new Error('Cache updates not allowed. Purge first! URI:', uri); | ||
} | ||
var createdAt = Date.now(); | ||
var accessedAt = null; | ||
var resource = { | ||
uri: uri, | ||
data: data, | ||
dataText: dataText, | ||
dataXML: dataXML, | ||
headers: headers, | ||
headersAll: headersAll, | ||
createdAt: createdAt, | ||
accessedAt: accessedAt | ||
}; | ||
cache.set(uri, resource); | ||
if (typeof resource.data.byteLength === 'number') { | ||
bytesWritten += resource.data.byteLength; | ||
} | ||
var totalSize = instance.countBytes(); | ||
if (totalSize > MAX_CACHE_SIZE_BYTES) { | ||
if (instance.errorOnOverflow) throw new Error('Cache exceeds max size, has', totalSize, 'bytes'); | ||
instance.purgeOldest(); | ||
} | ||
return instance; | ||
}, | ||
purgeByUri: function purgeByUri(uri) { | ||
return cache.delete(uri); | ||
}, | ||
purgeAll: function purgeAll() { | ||
cache.clear(); | ||
}, | ||
purgeNotAccessedSince: function purgeNotAccessedSince(timeMillisSince) { | ||
var now = Date.now(); | ||
cache.forEach(function (resource, uri) { | ||
if (!resource.accessedAt // never accessed | ||
|| resource.accessedAt < now - timeMillisSince) cache.delete(uri); | ||
}); | ||
}, | ||
purgeCreatedBefore: function purgeCreatedBefore(timestamp) { | ||
cache.forEach(function (resource, uri) { | ||
if (createdAt < timestamp) cache.delete(uri); | ||
}); | ||
}, | ||
purgeOldest: function purgeOldest() { | ||
var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'accessed'; | ||
var count = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; | ||
var prop = type + 'At'; | ||
var _loop = function _loop(i) { | ||
var oldest = null; | ||
cache.forEach(function (resource) { | ||
if (!oldest || resource[prop] < oldest[prop]) { | ||
oldest = resource; | ||
} | ||
}); | ||
cache.delete(oldest.uri); | ||
}; | ||
for (var i = 0; i < count; i++) { | ||
_loop(i); | ||
} | ||
}, | ||
reduce: function reduce(reduceFn) { | ||
var accuInit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; | ||
var accu = accuInit; | ||
cache.forEach(function (resource, uri) { | ||
accu = reduceFn.bind(undefined)(accu, resource); | ||
}); | ||
return accu; | ||
}, | ||
sumDataProperty: function sumDataProperty(field) { | ||
return instance.reduce(function (accu, resource) { | ||
return accu + resource.data[field]; | ||
}); | ||
}, | ||
countBytes: function countBytes() { | ||
return instance.sumDataProperty('byteLength'); | ||
} | ||
}; | ||
var getInfo = function getInfo() { | ||
return { | ||
bytesRead: bytesRead, | ||
bytesWritten: bytesWritten, | ||
hits: hits, | ||
misses: misses | ||
}; | ||
}; | ||
exports.default = { | ||
getInfo: getInfo, | ||
instance: instance | ||
}; | ||
/***/ }), | ||
/* 4 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
function setupCachedXhr(xhr, xhrProxy, cacheInstance) { | ||
xhrProxy._cacheInstance = cacheInstance; | ||
} | ||
exports.default = setupCachedXhr; | ||
/***/ }), | ||
/* 5 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
var XHRReadyStates = { | ||
UNSENT: 0, | ||
OPENED: 1, | ||
HEADERS_RECEIVED: 2, | ||
LOADING: 3, | ||
DONE: 4 | ||
}; | ||
function setupThrottledXhr(xhr, xhrProxy) { | ||
@@ -511,10 +850,10 @@ var shaper = xhrProxy.shaper; | ||
doneTs = void 0; | ||
var loaded = 0, | ||
total = void 0; | ||
var loaded = 0; | ||
var total = 0; | ||
var currentBitrateKpbs = void 0; | ||
var progressEvents = []; | ||
var progressTimer = void 0; | ||
var progressTimer = null; | ||
var lastProgressEvent = false; | ||
var loadEndEvent = void 0; | ||
var loadEvent = void 0; | ||
var loadEndEvent = null; | ||
var loadEvent = null; | ||
var done = false; | ||
@@ -525,3 +864,2 @@ | ||
//console.log('native loadend'); | ||
@@ -541,3 +879,4 @@ loadEndEvent = event; | ||
loadEvent = event; | ||
if (done && xhr.readyState === 4) { | ||
if (done && xhr.readyState === XHRReadyStates.DONE) { | ||
xhrProxy._setupWrappedResponseData(); | ||
_onload && _onload(event); | ||
@@ -549,2 +888,3 @@ xhrProxy._dispatchWrappedEventType('load'); | ||
xhr.onreadystatechange = function (event) { | ||
var now = Date.now(); | ||
var _onreadystatechange = xhrProxy._onreadystatechange, | ||
@@ -555,34 +895,41 @@ _onprogress = xhrProxy._onprogress, | ||
var triggerStateChange = function triggerStateChange(e, readyState) { | ||
if (typeof readyState !== 'number') { | ||
throw new Error('readyState should be a number'); | ||
} | ||
function triggerStateChange(e) { | ||
xhrProxy._readyState = readyState; | ||
_onreadystatechange && _onreadystatechange(e); | ||
xhrProxy._dispatchWrappedEventType('readystatechange'); | ||
} | ||
}; | ||
var latency = void 0; | ||
var delay1 = 0; | ||
var delay2 = 0; | ||
switch (xhr.readyState) { | ||
case 0: | ||
// UNSENT | ||
triggerStateChange(event); | ||
triggerStateChange(event, XHRReadyStates.UNSENT); | ||
break; | ||
case 1: | ||
// OPENED | ||
openedTs = Date.now(); | ||
triggerStateChange(event); | ||
openedTs = now; | ||
triggerStateChange(event, XHRReadyStates.OPENED); | ||
break; | ||
case 2: | ||
// HEADERS_RECEIVE | ||
headersTs = Date.now(); | ||
triggerStateChange(event); | ||
// HEADERS_RECEIVED | ||
headersTs = now; | ||
xhrProxy._setupWrappedHeaders(); | ||
triggerStateChange(event, XHRReadyStates.HEADERS_RECEIVED); | ||
break; | ||
case 3: | ||
// LOADING | ||
loadingTs = Date.now(); | ||
triggerStateChange(event); | ||
loadingTs = now; | ||
triggerStateChange(event, XHRReadyStates.LOADING); | ||
break; | ||
case 4: | ||
// DONE | ||
var delay1 = 0, | ||
delay2 = 0; | ||
doneTs = Date.now(); | ||
var latency = doneTs - openedTs; | ||
doneTs = now; | ||
latency = doneTs - openedTs; | ||
if (latency < shaper.minLatency) { | ||
@@ -603,3 +950,3 @@ delay1 = shaper.minLatency - latency; | ||
triggerStateChange(event); | ||
triggerStateChange(event, XHRReadyStates.DONE); | ||
@@ -609,2 +956,3 @@ done = true; | ||
if (loadEvent) { | ||
xhrProxy._setupWrappedResponseData(); | ||
_onload && _onload(loadEvent); | ||
@@ -624,3 +972,4 @@ xhrProxy._dispatchWrappedEventType('load'); | ||
done = true; | ||
triggerStateChange(event); | ||
xhrProxy._setupWrappedResponseData(); | ||
triggerStateChange(event, XHRReadyStates.DONE); | ||
} | ||
@@ -632,7 +981,6 @@ break; | ||
xhr.onprogress = function (event) { | ||
var now = Date.now(); | ||
var _onprogress = xhrProxy._onprogress; | ||
function triggerProgress(e) { | ||
var triggerProgress = function triggerProgress(e) { | ||
if (loaded === total) { | ||
@@ -644,5 +992,4 @@ lastProgressEvent = true; | ||
xhrProxy._dispatchWrappedEventType('progress'); | ||
} | ||
}; | ||
var now = Date.now(); | ||
var duration = now - openedTs; | ||
@@ -655,8 +1002,5 @@ var delay = void 0; | ||
// console.log('current bitrate: ' + Math.round(currentBitrateKpbs) + ' kbps'); | ||
if (currentBitrateKpbs > shaper.maxBandwidth) { | ||
delay = currentBitrateKpbs / shaper.maxBandwidth * duration - duration; | ||
progressEvents.push(event); | ||
// console.log('delaying progress event by ' + Math.round(delay) + ' ms'); | ||
progressTimer = setTimeout(function () { | ||
@@ -670,7 +1014,2 @@ triggerProgress(event); | ||
}; | ||
var id = Math.round(Math.random() * 1e6); | ||
xhr.__throttledId = id; | ||
xhrProxy.__throttledId = id; | ||
} | ||
@@ -681,3 +1020,3 @@ | ||
/***/ }), | ||
/* 4 */ | ||
/* 6 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -684,0 +1023,0 @@ |
@@ -313,14 +313,22 @@ (function webpackUniversalModuleDefinition(root, factory) { | ||
var _xhr = __webpack_require__(0); | ||
var _xhrProxy = __webpack_require__(0); | ||
var _xhr2 = _interopRequireDefault(_xhr); | ||
var _xhrProxy2 = _interopRequireDefault(_xhrProxy); | ||
var _shaper = __webpack_require__(4); | ||
var _shaper = __webpack_require__(6); | ||
var _shaper2 = _interopRequireDefault(_shaper); | ||
var _setupThrottledXhr = __webpack_require__(3); | ||
var _setupThrottledXhr = __webpack_require__(5); | ||
var _setupThrottledXhr2 = _interopRequireDefault(_setupThrottledXhr); | ||
var _setupCachedXhr = __webpack_require__(4); | ||
var _setupCachedXhr2 = _interopRequireDefault(_setupCachedXhr); | ||
var _cache = __webpack_require__(3); | ||
var _cache2 = _interopRequireDefault(_cache); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -334,2 +342,4 @@ | ||
var DEBUG = false; | ||
var PASSTHROUGH_EVENTS = ['loadstart', 'timeout', 'abort', 'error']; | ||
@@ -343,6 +353,6 @@ | ||
var ThrottledXHR = function (_XHRProxy) { | ||
_inherits(ThrottledXHR, _XHRProxy); | ||
var XHR = function (_XHRProxy) { | ||
_inherits(XHR, _XHRProxy); | ||
_createClass(ThrottledXHR, null, [{ | ||
_createClass(XHR, null, [{ | ||
key: 'Shaper', | ||
@@ -352,13 +362,35 @@ get: function get() { | ||
} | ||
}, { | ||
key: 'cache', | ||
get: function get() { | ||
return _cache2.default; | ||
} | ||
}]); | ||
function ThrottledXHR() { | ||
_classCallCheck(this, ThrottledXHR); | ||
function XHR() { | ||
_classCallCheck(this, XHR); | ||
var _this = _possibleConstructorReturn(this, (ThrottledXHR.__proto__ || Object.getPrototypeOf(ThrottledXHR)).call(this)); | ||
var _this = _possibleConstructorReturn(this, (XHR.__proto__ || Object.getPrototypeOf(XHR)).call(this)); | ||
_this._cacheInstance = _cache2.default.instance; | ||
_this._shaper = new _shaper2.default(); | ||
_this._listenersMap = new Map(); | ||
_this._dispatchedEventsList = []; | ||
_this._response = null; | ||
_this._responseText = null; | ||
_this._responseXML = null; | ||
_this._headers = null; | ||
_this._readyState = 0; | ||
_this._caching = false; | ||
_this._cacheHit = false; | ||
_this._cacheWrite = true; | ||
_this._onSend = null; | ||
_this.addEventListener('loadend', function () { | ||
if (_this._caching && _this._cacheWrite) { | ||
DEBUG && console.log('CACHE WRITE:', _this._xhr.responseURL); | ||
_this._cacheInstance.put(_this._xhr.responseURL, _this._response, _this._responseText, _this._responseXML, null, _this._xhr.getAllResponseHeaders()); | ||
} | ||
}); | ||
(0, _setupThrottledXhr2.default)(_this._xhr, _this); | ||
@@ -368,3 +400,3 @@ return _this; | ||
_createClass(ThrottledXHR, [{ | ||
_createClass(XHR, [{ | ||
key: '_dispatchWrappedEventType', | ||
@@ -386,2 +418,63 @@ value: function _dispatchWrappedEventType(type) { | ||
}, { | ||
key: '_handleCacheHitOnSend', | ||
value: function _handleCacheHitOnSend(cachedResource) { | ||
var _this3 = this; | ||
this._readyState = 1; | ||
this._cacheHit = true; | ||
var onSend = function onSend() { | ||
setTimeout(function () { | ||
_this3._readyState = 2; | ||
_this3.onreadystatechange && _this3.onreadystatechange(); | ||
_this3._dispatchWrappedEventType('readystatechange'); | ||
_this3._readyState = 3; | ||
_this3.onreadystatechange && _this3.onreadystatechange(); | ||
_this3._dispatchWrappedEventType('readystatechange'); | ||
_this3.onloadstart && _this3.onloadstart(); | ||
_this3._dispatchWrappedEventType('loadstart'); | ||
_this3._response = cachedResource.data; | ||
_this3._responseText = cachedResource.dataText; | ||
_this3._responseXML = cachedResource.dataXML; | ||
_this3._headersAll = cachedResource.headers; | ||
_this3._readyState = 4; | ||
_this3.onreadystatechange && _this3.onreadystatechange(); | ||
_this3._dispatchWrappedEventType('readystatechange'); | ||
_this3.onreadystatechange && _this3.onreadystatechange(); | ||
_this3._dispatchWrappedEventType('readystatechange'); | ||
_this3.onload && _this3.onload(); | ||
_this3._dispatchWrappedEventType('load'); | ||
_this3.onloadend && _this3.onloadend(); | ||
_this3._dispatchWrappedEventType('loadend'); | ||
}, 0); | ||
}; | ||
this._onSend = onSend; | ||
} | ||
}, { | ||
key: '_setupWrappedResponseData', | ||
value: function _setupWrappedResponseData() { | ||
try { | ||
this._response = this._xhr.response; | ||
} catch (e) { | ||
DEBUG && console.warn(e); | ||
} | ||
try { | ||
this._responseText = this._xhr.responseText; | ||
} catch (e) { | ||
DEBUG && console.warn(e); | ||
} | ||
try { | ||
this._responseXML = this._xhr.responseXML; | ||
} catch (e) { | ||
DEBUG && console.warn(e); | ||
} | ||
} | ||
}, { | ||
key: '_setupWrappedHeaders', | ||
value: function _setupWrappedHeaders() { | ||
try { | ||
this._headersAll = this._xhr.getAllResponseHeaders(); | ||
} catch (e) {} | ||
} | ||
}, { | ||
key: 'addEventListener', | ||
@@ -391,3 +484,3 @@ value: function addEventListener(type, listener, optionsOrUseCapture, wantsUntrusted) { | ||
if (PASSTHROUGH_EVENTS.includes(type)) { | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'addEventListener', this).call(this, type, listener, optionsOrUseCapture, wantsUntrusted); | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'addEventListener', this).call(this, type, listener, optionsOrUseCapture, wantsUntrusted); | ||
} | ||
@@ -397,3 +490,3 @@ | ||
this._listenersMap.set(listener, listenerWrapper); | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'addEventListener', this).call(this, type, listenerWrapper, optionsOrUseCapture, wantsUntrusted); | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'addEventListener', this).call(this, type, listenerWrapper, optionsOrUseCapture, wantsUntrusted); | ||
} | ||
@@ -405,3 +498,3 @@ }, { | ||
if (PASSTHROUGH_EVENTS.includes(type)) { | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'removeEventListener', this).call(this, type, listener, optionsOrUseCapture); | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'removeEventListener', this).call(this, type, listener, optionsOrUseCapture); | ||
} | ||
@@ -414,10 +507,70 @@ | ||
this._listenersMap.delete(listener); | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'removeEventListener', this).call(this, type, listenerWrapper, optionsOrUseCapture); | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'removeEventListener', this).call(this, type, listenerWrapper, optionsOrUseCapture); | ||
} | ||
}, { | ||
key: 'dispatchEvent', | ||
value: function dispatchEvent(event) { | ||
return _get(ThrottledXHR.prototype.__proto__ || Object.getPrototypeOf(ThrottledXHR.prototype), 'dispatchEvent', this).call(this, event); | ||
key: 'open', | ||
value: function open(method, url, async, user, password) { | ||
if (this._caching) { | ||
if (!this._cacheInstance) { | ||
throw new Error('no cache setup'); | ||
} | ||
DEBUG && console.log('CACHE GET:', url); | ||
var cachedResource = this._cacheInstance.get(url, false); | ||
if (cachedResource) { | ||
DEBUG && console.log('CACHE HIT'); | ||
this._handleCacheHitOnSend(cachedResource); | ||
} | ||
} | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'open', this).call(this, method, url, async, user, password); | ||
} | ||
}, { | ||
key: 'send', | ||
value: function send(data) { | ||
if (this._caching && this._cacheHit && this._onSend) { | ||
return this._onSend(); | ||
} | ||
return _get(XHR.prototype.__proto__ || Object.getPrototypeOf(XHR.prototype), 'send', this).call(this, data); | ||
} | ||
}, { | ||
key: 'getAllResponseHeaders', | ||
value: function getAllResponseHeaders() { | ||
return this._headersAll; | ||
} | ||
}, { | ||
key: 'caching', | ||
get: function get() { | ||
return this._caching; | ||
}, | ||
set: function set(enabled) { | ||
this._caching = enabled; | ||
} | ||
}, { | ||
key: 'cacheInstance', | ||
set: function set(instance) { | ||
this._cacheInstance = instance; | ||
}, | ||
get: function get() { | ||
return this._cacheInstance; | ||
} | ||
}, { | ||
key: 'isCacheHit', | ||
get: function get() { | ||
return this._cacheHit; | ||
} | ||
}, { | ||
key: 'cacheWriteEnabled', | ||
set: function set(enable) { | ||
this._cacheWrite = enable; | ||
} | ||
}, { | ||
key: 'enableCacheWrite', | ||
get: function get() { | ||
return this._cacheWrite; | ||
} | ||
}, { | ||
key: 'shaper', | ||
@@ -428,2 +581,9 @@ get: function get() { | ||
}, { | ||
key: 'readyState', | ||
get: function get() { | ||
if (typeof this._readyState === 'number') { | ||
return this._readyState; | ||
} | ||
} | ||
}, { | ||
key: 'onloadend', | ||
@@ -460,8 +620,23 @@ set: function set(fn) { | ||
} | ||
}, { | ||
key: 'response', | ||
get: function get() { | ||
return this._response; | ||
} | ||
}, { | ||
key: 'responseText', | ||
get: function get() { | ||
return this._responseText; | ||
} | ||
}, { | ||
key: 'responseXML', | ||
get: function get() { | ||
return this._responseXML; | ||
} | ||
}]); | ||
return ThrottledXHR; | ||
}(_xhr2.default); | ||
return XHR; | ||
}(_xhrProxy2.default); | ||
exports.default = ThrottledXHR; | ||
exports.default = XHR; | ||
@@ -475,9 +650,9 @@ /***/ }), | ||
var _xhr = __webpack_require__(0); | ||
var _xhrProxy = __webpack_require__(0); | ||
var _xhr2 = _interopRequireDefault(_xhr); | ||
var _xhrProxy2 = _interopRequireDefault(_xhrProxy); | ||
var _throttledXhr = __webpack_require__(1); | ||
var _xhr = __webpack_require__(1); | ||
var _throttledXhr2 = _interopRequireDefault(_throttledXhr); | ||
var _xhr2 = _interopRequireDefault(_xhr); | ||
@@ -490,9 +665,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
// Overload native window constructor | ||
global.XMLHttpRequest = _throttledXhr2.default; | ||
global.XMLHttpRequest = _xhr2.default; | ||
} | ||
module.exports = { | ||
XMLHttpRequest: _throttledXhr2.default, | ||
ThrottledXHR: _throttledXhr2.default, | ||
XHRProxy: _xhr2.default, | ||
XMLHttpRequest: _xhr2.default, | ||
XHRProxy: _xhrProxy2.default, | ||
useGlobal: useGlobal | ||
@@ -509,4 +683,169 @@ }; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
var DEFAULT_ALLOW_UPDATES = false; | ||
var MAX_CACHE_SIZE_BYTES = 1024 * 1e6; // 1024 Mbytes | ||
var cache = new Map(); | ||
var bytesRead = 0; | ||
var bytesWritten = 0; | ||
var misses = 0; | ||
var hits = 0; | ||
var instance = { | ||
allowUpdates: DEFAULT_ALLOW_UPDATES, | ||
errorOnOverflow: false, | ||
get: function get(uri) { | ||
var onlyData = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; | ||
var resource = void 0; | ||
if (!cache.has(uri)) { | ||
misses++; | ||
return null; | ||
} | ||
hits++; | ||
resource = cache.get(uri); | ||
resource.accessedAt = Date.now(); | ||
if (typeof resource.data.byteLength === 'number') { | ||
bytesRead += resource.data.byteLength; | ||
} | ||
if (onlyData) { | ||
return resource.data; | ||
} else { | ||
return resource; | ||
} | ||
}, | ||
put: function put(uri, data, dataText, dataXML, headers, headersAll) { | ||
if (!instance.allowUpdates && cache.has(uri)) { | ||
throw new Error('Cache updates not allowed. Purge first! URI:', uri); | ||
} | ||
var createdAt = Date.now(); | ||
var accessedAt = null; | ||
var resource = { | ||
uri: uri, | ||
data: data, | ||
dataText: dataText, | ||
dataXML: dataXML, | ||
headers: headers, | ||
headersAll: headersAll, | ||
createdAt: createdAt, | ||
accessedAt: accessedAt | ||
}; | ||
cache.set(uri, resource); | ||
if (typeof resource.data.byteLength === 'number') { | ||
bytesWritten += resource.data.byteLength; | ||
} | ||
var totalSize = instance.countBytes(); | ||
if (totalSize > MAX_CACHE_SIZE_BYTES) { | ||
if (instance.errorOnOverflow) throw new Error('Cache exceeds max size, has', totalSize, 'bytes'); | ||
instance.purgeOldest(); | ||
} | ||
return instance; | ||
}, | ||
purgeByUri: function purgeByUri(uri) { | ||
return cache.delete(uri); | ||
}, | ||
purgeAll: function purgeAll() { | ||
cache.clear(); | ||
}, | ||
purgeNotAccessedSince: function purgeNotAccessedSince(timeMillisSince) { | ||
var now = Date.now(); | ||
cache.forEach(function (resource, uri) { | ||
if (!resource.accessedAt // never accessed | ||
|| resource.accessedAt < now - timeMillisSince) cache.delete(uri); | ||
}); | ||
}, | ||
purgeCreatedBefore: function purgeCreatedBefore(timestamp) { | ||
cache.forEach(function (resource, uri) { | ||
if (createdAt < timestamp) cache.delete(uri); | ||
}); | ||
}, | ||
purgeOldest: function purgeOldest() { | ||
var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'accessed'; | ||
var count = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; | ||
var prop = type + 'At'; | ||
var _loop = function _loop(i) { | ||
var oldest = null; | ||
cache.forEach(function (resource) { | ||
if (!oldest || resource[prop] < oldest[prop]) { | ||
oldest = resource; | ||
} | ||
}); | ||
cache.delete(oldest.uri); | ||
}; | ||
for (var i = 0; i < count; i++) { | ||
_loop(i); | ||
} | ||
}, | ||
reduce: function reduce(reduceFn) { | ||
var accuInit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; | ||
var accu = accuInit; | ||
cache.forEach(function (resource, uri) { | ||
accu = reduceFn.bind(undefined)(accu, resource); | ||
}); | ||
return accu; | ||
}, | ||
sumDataProperty: function sumDataProperty(field) { | ||
return instance.reduce(function (accu, resource) { | ||
return accu + resource.data[field]; | ||
}); | ||
}, | ||
countBytes: function countBytes() { | ||
return instance.sumDataProperty('byteLength'); | ||
} | ||
}; | ||
var getInfo = function getInfo() { | ||
return { | ||
bytesRead: bytesRead, | ||
bytesWritten: bytesWritten, | ||
hits: hits, | ||
misses: misses | ||
}; | ||
}; | ||
exports.default = { | ||
getInfo: getInfo, | ||
instance: instance | ||
}; | ||
/***/ }), | ||
/* 4 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
function setupCachedXhr(xhr, xhrProxy, cacheInstance) { | ||
xhrProxy._cacheInstance = cacheInstance; | ||
} | ||
exports.default = setupCachedXhr; | ||
/***/ }), | ||
/* 5 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
var XHRReadyStates = { | ||
UNSENT: 0, | ||
OPENED: 1, | ||
HEADERS_RECEIVED: 2, | ||
LOADING: 3, | ||
DONE: 4 | ||
}; | ||
function setupThrottledXhr(xhr, xhrProxy) { | ||
@@ -520,10 +859,10 @@ var shaper = xhrProxy.shaper; | ||
doneTs = void 0; | ||
var loaded = 0, | ||
total = void 0; | ||
var loaded = 0; | ||
var total = 0; | ||
var currentBitrateKpbs = void 0; | ||
var progressEvents = []; | ||
var progressTimer = void 0; | ||
var progressTimer = null; | ||
var lastProgressEvent = false; | ||
var loadEndEvent = void 0; | ||
var loadEvent = void 0; | ||
var loadEndEvent = null; | ||
var loadEvent = null; | ||
var done = false; | ||
@@ -534,3 +873,2 @@ | ||
//console.log('native loadend'); | ||
@@ -550,3 +888,4 @@ loadEndEvent = event; | ||
loadEvent = event; | ||
if (done && xhr.readyState === 4) { | ||
if (done && xhr.readyState === XHRReadyStates.DONE) { | ||
xhrProxy._setupWrappedResponseData(); | ||
_onload && _onload(event); | ||
@@ -558,2 +897,3 @@ xhrProxy._dispatchWrappedEventType('load'); | ||
xhr.onreadystatechange = function (event) { | ||
var now = Date.now(); | ||
var _onreadystatechange = xhrProxy._onreadystatechange, | ||
@@ -564,34 +904,41 @@ _onprogress = xhrProxy._onprogress, | ||
var triggerStateChange = function triggerStateChange(e, readyState) { | ||
if (typeof readyState !== 'number') { | ||
throw new Error('readyState should be a number'); | ||
} | ||
function triggerStateChange(e) { | ||
xhrProxy._readyState = readyState; | ||
_onreadystatechange && _onreadystatechange(e); | ||
xhrProxy._dispatchWrappedEventType('readystatechange'); | ||
} | ||
}; | ||
var latency = void 0; | ||
var delay1 = 0; | ||
var delay2 = 0; | ||
switch (xhr.readyState) { | ||
case 0: | ||
// UNSENT | ||
triggerStateChange(event); | ||
triggerStateChange(event, XHRReadyStates.UNSENT); | ||
break; | ||
case 1: | ||
// OPENED | ||
openedTs = Date.now(); | ||
triggerStateChange(event); | ||
openedTs = now; | ||
triggerStateChange(event, XHRReadyStates.OPENED); | ||
break; | ||
case 2: | ||
// HEADERS_RECEIVE | ||
headersTs = Date.now(); | ||
triggerStateChange(event); | ||
// HEADERS_RECEIVED | ||
headersTs = now; | ||
xhrProxy._setupWrappedHeaders(); | ||
triggerStateChange(event, XHRReadyStates.HEADERS_RECEIVED); | ||
break; | ||
case 3: | ||
// LOADING | ||
loadingTs = Date.now(); | ||
triggerStateChange(event); | ||
loadingTs = now; | ||
triggerStateChange(event, XHRReadyStates.LOADING); | ||
break; | ||
case 4: | ||
// DONE | ||
var delay1 = 0, | ||
delay2 = 0; | ||
doneTs = Date.now(); | ||
var latency = doneTs - openedTs; | ||
doneTs = now; | ||
latency = doneTs - openedTs; | ||
if (latency < shaper.minLatency) { | ||
@@ -612,3 +959,3 @@ delay1 = shaper.minLatency - latency; | ||
triggerStateChange(event); | ||
triggerStateChange(event, XHRReadyStates.DONE); | ||
@@ -618,2 +965,3 @@ done = true; | ||
if (loadEvent) { | ||
xhrProxy._setupWrappedResponseData(); | ||
_onload && _onload(loadEvent); | ||
@@ -633,3 +981,4 @@ xhrProxy._dispatchWrappedEventType('load'); | ||
done = true; | ||
triggerStateChange(event); | ||
xhrProxy._setupWrappedResponseData(); | ||
triggerStateChange(event, XHRReadyStates.DONE); | ||
} | ||
@@ -641,7 +990,6 @@ break; | ||
xhr.onprogress = function (event) { | ||
var now = Date.now(); | ||
var _onprogress = xhrProxy._onprogress; | ||
function triggerProgress(e) { | ||
var triggerProgress = function triggerProgress(e) { | ||
if (loaded === total) { | ||
@@ -653,5 +1001,4 @@ lastProgressEvent = true; | ||
xhrProxy._dispatchWrappedEventType('progress'); | ||
} | ||
}; | ||
var now = Date.now(); | ||
var duration = now - openedTs; | ||
@@ -664,8 +1011,5 @@ var delay = void 0; | ||
// console.log('current bitrate: ' + Math.round(currentBitrateKpbs) + ' kbps'); | ||
if (currentBitrateKpbs > shaper.maxBandwidth) { | ||
delay = currentBitrateKpbs / shaper.maxBandwidth * duration - duration; | ||
progressEvents.push(event); | ||
// console.log('delaying progress event by ' + Math.round(delay) + ' ms'); | ||
progressTimer = setTimeout(function () { | ||
@@ -679,7 +1023,2 @@ triggerProgress(event); | ||
}; | ||
var id = Math.round(Math.random() * 1e6); | ||
xhr.__throttledId = id; | ||
xhrProxy.__throttledId = id; | ||
} | ||
@@ -690,3 +1029,3 @@ | ||
/***/ }), | ||
/* 4 */ | ||
/* 6 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -693,0 +1032,0 @@ |
@@ -1,3 +0,3 @@ | ||
import XHRProxy from './src/xhr'; | ||
import ThrottledXHR from './src/throttled-xhr'; | ||
import XHRProxy from './src/xhr-proxy'; | ||
import XHR from './src/xhr'; | ||
@@ -8,10 +8,9 @@ function useGlobal() { | ||
// Overload native window constructor | ||
global.XMLHttpRequest = ThrottledXHR; | ||
global.XMLHttpRequest = XHR; | ||
} | ||
module.exports = { | ||
XMLHttpRequest: ThrottledXHR, | ||
ThrottledXHR, | ||
XMLHttpRequest: XHR, | ||
XHRProxy, | ||
useGlobal | ||
}; |
{ | ||
"name": "xhr-shaper", | ||
"version": "2.2.1", | ||
"version": "2.2.2", | ||
"description": "Shapes your XHR requests to a max emulated bandwidth and latency, randomizes frequency of progress events", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -0,1 +1,9 @@ | ||
var XHRReadyStates = { | ||
UNSENT: 0, | ||
OPENED: 1, | ||
HEADERS_RECEIVED: 2, | ||
LOADING: 3, | ||
DONE: 4 | ||
}; | ||
function setupThrottledXhr(xhr, xhrProxy) { | ||
@@ -8,9 +16,10 @@ | ||
let openedTs, headersTs, loadingTs, doneTs; | ||
let loaded = 0, total; | ||
let loaded = 0; | ||
let total = 0; | ||
let currentBitrateKpbs; | ||
let progressEvents = []; | ||
let progressTimer; | ||
let progressTimer = null; | ||
let lastProgressEvent = false; | ||
let loadEndEvent; | ||
let loadEvent; | ||
let loadEndEvent = null; | ||
let loadEvent = null; | ||
let done = false; | ||
@@ -24,3 +33,2 @@ | ||
//console.log('native loadend'); | ||
loadEndEvent = event; | ||
@@ -41,3 +49,4 @@ if (done) { | ||
loadEvent = event; | ||
if (done && xhr.readyState === 4) { | ||
if (done && xhr.readyState === XHRReadyStates.DONE) { | ||
xhrProxy._setupWrappedResponseData(); | ||
_onload && _onload(event); | ||
@@ -49,4 +58,4 @@ xhrProxy._dispatchWrappedEventType('load'); | ||
xhr.onreadystatechange = function(event) { | ||
let { | ||
const now = Date.now(); | ||
const { | ||
_onreadystatechange, | ||
@@ -57,4 +66,8 @@ _onprogress, | ||
} = xhrProxy; | ||
const triggerStateChange = function(e, readyState) { | ||
if (typeof readyState !== 'number') { | ||
throw new Error('readyState should be a number'); | ||
} | ||
function triggerStateChange(e) { | ||
xhrProxy._readyState = readyState; | ||
_onreadystatechange && _onreadystatechange(e); | ||
@@ -64,22 +77,26 @@ xhrProxy._dispatchWrappedEventType('readystatechange'); | ||
let latency; | ||
let delay1 = 0; | ||
let delay2 = 0; | ||
switch (xhr.readyState) { | ||
case 0: // UNSENT | ||
triggerStateChange(event); | ||
triggerStateChange(event, XHRReadyStates.UNSENT); | ||
break; | ||
case 1: // OPENED | ||
openedTs = Date.now(); | ||
triggerStateChange(event); | ||
openedTs = now; | ||
triggerStateChange(event, XHRReadyStates.OPENED); | ||
break; | ||
case 2: // HEADERS_RECEIVE | ||
headersTs = Date.now(); | ||
triggerStateChange(event); | ||
case 2: // HEADERS_RECEIVED | ||
headersTs = now; | ||
xhrProxy._setupWrappedHeaders(); | ||
triggerStateChange(event, XHRReadyStates.HEADERS_RECEIVED); | ||
break; | ||
case 3: // LOADING | ||
loadingTs = Date.now(); | ||
triggerStateChange(event); | ||
loadingTs = now; | ||
triggerStateChange(event, XHRReadyStates.LOADING); | ||
break; | ||
case 4: // DONE | ||
let delay1 = 0, delay2 = 0; | ||
doneTs = Date.now(); | ||
let latency = doneTs - openedTs; | ||
doneTs = now; | ||
latency = doneTs - openedTs; | ||
if (latency < shaper.minLatency) { | ||
@@ -100,3 +117,3 @@ delay1 = shaper.minLatency - latency; | ||
triggerStateChange(event); | ||
triggerStateChange(event, XHRReadyStates.DONE); | ||
@@ -106,2 +123,3 @@ done = true; | ||
if (loadEvent) { | ||
xhrProxy._setupWrappedResponseData(); | ||
_onload && _onload(loadEvent); | ||
@@ -122,3 +140,4 @@ xhrProxy._dispatchWrappedEventType('load'); | ||
done = true; | ||
triggerStateChange(event); | ||
xhrProxy._setupWrappedResponseData(); | ||
triggerStateChange(event, XHRReadyStates.DONE); | ||
} | ||
@@ -130,9 +149,7 @@ break; | ||
xhr.onprogress = function(event) { | ||
let { | ||
const now = Date.now(); | ||
const { | ||
_onprogress | ||
} = xhrProxy; | ||
function triggerProgress(e) { | ||
const triggerProgress = function(e) { | ||
if (loaded === total) { | ||
@@ -146,3 +163,2 @@ lastProgressEvent = true; | ||
let now = Date.now(); | ||
let duration = now - openedTs; | ||
@@ -155,8 +171,5 @@ let delay; | ||
// console.log('current bitrate: ' + Math.round(currentBitrateKpbs) + ' kbps'); | ||
if (currentBitrateKpbs > shaper.maxBandwidth) { | ||
delay = (currentBitrateKpbs / shaper.maxBandwidth) * duration - duration; | ||
progressEvents.push(event); | ||
// console.log('delaying progress event by ' + Math.round(delay) + ' ms'); | ||
progressTimer = setTimeout(function() { | ||
@@ -170,9 +183,4 @@ triggerProgress(event); | ||
}; | ||
let id = Math.round(Math.random() * 1e6); | ||
xhr.__throttledId = id; | ||
xhrProxy.__throttledId = id; | ||
} | ||
export default setupThrottledXhr; |
307
src/xhr.js
@@ -1,152 +0,265 @@ | ||
// See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest | ||
import XHRProxy from './xhr-proxy'; | ||
import Shaper from './shaper'; | ||
import setupThrottledXhr from './setup-throttled-xhr'; | ||
import setupCachedXhr from './setup-cached-xhr'; | ||
import cache from './cache'; | ||
const XMLHttpRequest = window.XMLHttpRequest; | ||
const DEBUG = false; | ||
class XHRProxy { | ||
const PASSTHROUGH_EVENTS = ['loadstart', 'timeout', 'abort', 'error']; | ||
constructor() { | ||
this._xhr = new XMLHttpRequest(); | ||
const createListenerWrapper = (type, listener, dispatchedEventsList) => { | ||
return (event) => { | ||
dispatchedEventsList.push({type, listener, event, propagated: false}); | ||
}; | ||
} | ||
class XHR extends XHRProxy { | ||
static get Shaper() { | ||
return Shaper; | ||
} | ||
// methods | ||
static get cache() { | ||
return cache; | ||
} | ||
abort() { | ||
return this._xhr.abort(); | ||
constructor() { | ||
super(); | ||
this._cacheInstance = cache.instance; | ||
this._shaper = new Shaper(); | ||
this._listenersMap = new Map(); | ||
this._dispatchedEventsList = []; | ||
this._response = null; | ||
this._responseText = null; | ||
this._responseXML = null; | ||
this._headers = null; | ||
this._readyState = 0; | ||
this._caching = false; | ||
this._cacheHit = false; | ||
this._cacheWrite = true; | ||
this._onSend = null; | ||
this.addEventListener('loadend', () => { | ||
if (this._caching && this._cacheWrite) { | ||
DEBUG && console.log('CACHE WRITE:', this._xhr.responseURL); | ||
this._cacheInstance.put( | ||
this._xhr.responseURL, | ||
this._response, | ||
this._responseText, | ||
this._responseXML, | ||
null, | ||
this._xhr.getAllResponseHeaders() | ||
); | ||
} | ||
}); | ||
setupThrottledXhr(this._xhr, this); | ||
} | ||
open(method, url, async, user, password) { | ||
return this._xhr.open(method, url, async, user, password); | ||
get caching() { | ||
return this._caching; | ||
} | ||
send(data) { | ||
return this._xhr.send(data); | ||
set caching(enabled) { | ||
this._caching = enabled; | ||
} | ||
setRequestHeader(header, value) { | ||
return this._xhr.setRequestHeader(header, value); | ||
set cacheInstance(instance) { | ||
this._cacheInstance = instance; | ||
} | ||
getResponseHeader(header) { | ||
return this._xhr.getResponseHeader(header); | ||
get cacheInstance() { | ||
return this._cacheInstance; | ||
} | ||
overrideMimeType(mimeType) { | ||
return this._xhr.overrideMimeType(mimeType); | ||
get isCacheHit() { | ||
return this._cacheHit; | ||
} | ||
getAllResponseHeaders() { | ||
return this._xhr.getAllResponseHeaders(); | ||
set cacheWriteEnabled(enable) { | ||
this._cacheWrite = enable; | ||
} | ||
addEventListener(type, listener, optionsOrUseCapture, wantsUntrusted) { | ||
return this._xhr.addEventListener(type, listener, optionsOrUseCapture, wantsUntrusted); | ||
get enableCacheWrite() { | ||
return this._cacheWrite; | ||
} | ||
removeEventListener(type, listener, optionsOrUseCapture) { | ||
return this._xhr.removeEventListener(type, listener, optionsOrUseCapture); | ||
get shaper() { | ||
return this._shaper; | ||
} | ||
dispatchEvent(event) { | ||
return this._xhr.dispatchEvent(event); | ||
_dispatchWrappedEventType(type) { | ||
// it needs to run on the next tick since this is actually | ||
// triggered from our throttler listeners on the proxy's inner XHR | ||
setTimeout(() => { | ||
this._dispatchedEventsList | ||
.filter((dispatchedEvent) => (dispatchedEvent.type === type && !dispatchedEvent.propagated)) | ||
.forEach((dispatchedEvent) => { | ||
dispatchedEvent.propagated = true; | ||
dispatchedEvent.listener(dispatchedEvent.event); | ||
}); | ||
}, 0); | ||
} | ||
// Read-only properties | ||
_handleCacheHitOnSend(cachedResource) { | ||
this._readyState = 1; | ||
this._cacheHit = true; | ||
get readyState() { | ||
return this._xhr.readyState; | ||
const onSend = () => { | ||
setTimeout(() => { | ||
this._readyState = 2; | ||
this.onreadystatechange && this.onreadystatechange(); | ||
this._dispatchWrappedEventType('readystatechange'); | ||
this._readyState = 3; | ||
this.onreadystatechange && this.onreadystatechange(); | ||
this._dispatchWrappedEventType('readystatechange'); | ||
this.onloadstart && this.onloadstart(); | ||
this._dispatchWrappedEventType('loadstart'); | ||
this._response = cachedResource.data; | ||
this._responseText = cachedResource.dataText; | ||
this._responseXML = cachedResource.dataXML; | ||
this._headersAll = cachedResource.headers; | ||
this._readyState = 4; | ||
this.onreadystatechange && this.onreadystatechange(); | ||
this._dispatchWrappedEventType('readystatechange'); | ||
this.onreadystatechange && this.onreadystatechange(); | ||
this._dispatchWrappedEventType('readystatechange'); | ||
this.onload && this.onload(); | ||
this._dispatchWrappedEventType('load'); | ||
this.onloadend && this.onloadend(); | ||
this._dispatchWrappedEventType('loadend'); | ||
}, 0); | ||
}; | ||
this._onSend = onSend; | ||
} | ||
get response() { | ||
return this._xhr.response; | ||
_setupWrappedResponseData() { | ||
try { | ||
this._response = this._xhr.response; | ||
} catch(e) { | ||
DEBUG && console.warn(e) | ||
} | ||
try { | ||
this._responseText = this._xhr.responseText; | ||
} catch(e) { | ||
DEBUG && console.warn(e) | ||
} | ||
try { | ||
this._responseXML = this._xhr.responseXML; | ||
} catch(e) { | ||
DEBUG && console.warn(e) | ||
} | ||
} | ||
get responseText() { | ||
return this._xhr.responseText; | ||
_setupWrappedHeaders() { | ||
try { this._headersAll = this._xhr.getAllResponseHeaders(); } catch(e) {} | ||
} | ||
get responseXML() { | ||
return this._xhr.responseXML; | ||
addEventListener(type, listener, optionsOrUseCapture, wantsUntrusted) { | ||
if (PASSTHROUGH_EVENTS.includes(type)) { | ||
return super.addEventListener(type, listener, optionsOrUseCapture, wantsUntrusted); | ||
} | ||
const listenerWrapper = createListenerWrapper(type, listener, this._dispatchedEventsList); | ||
this._listenersMap.set(listener, listenerWrapper); | ||
return super.addEventListener(type, listenerWrapper, optionsOrUseCapture, wantsUntrusted); | ||
} | ||
get responseURL() { | ||
return this._xhr.responseURL; | ||
} | ||
get status() { | ||
return this._xhr.status; | ||
} | ||
get statusText() { | ||
return this._xhr.statusText; | ||
} | ||
get upload() { | ||
return this._xhr.upload; | ||
} | ||
// R & W properties | ||
set withCredentials(enabled) { | ||
this._xhr.withCredentials = enabled; | ||
removeEventListener(type, listener, optionsOrUseCapture) { | ||
if (PASSTHROUGH_EVENTS.includes(type)) { | ||
return super.removeEventListener(type, listener, optionsOrUseCapture) | ||
} | ||
const listenerWrapper = this._listenersMap.get(listener); | ||
if (!listenerWrapper) { | ||
return; | ||
} | ||
this._listenersMap.delete(listener); | ||
return super.removeEventListener(type, listenerWrapper, optionsOrUseCapture); | ||
} | ||
get withCredentials() { | ||
return this._xhr.withCredentials; | ||
} | ||
set responseType(responseType) { | ||
this._xhr.responseType = responseType; | ||
open(method, url, async, user, password) { | ||
if (this._caching) { | ||
if (!this._cacheInstance) { | ||
throw new Error('no cache setup'); | ||
} | ||
DEBUG && console.log('CACHE GET:', url); | ||
const cachedResource = this._cacheInstance.get(url, false); | ||
if (cachedResource) { | ||
DEBUG && console.log('CACHE HIT'); | ||
this._handleCacheHitOnSend(cachedResource); | ||
} | ||
} | ||
return super.open(method, url, async, user, password); | ||
} | ||
get responseType() { | ||
return this._xhr.responseType; | ||
} | ||
set timeout(timeout) { | ||
this._xhr.timeout = timeout; | ||
send(data) { | ||
if (this._caching && this._cacheHit && this._onSend) { | ||
return this._onSend(); | ||
} | ||
return super.send(data); | ||
} | ||
get timeout() { | ||
return this._xhr.timeout; | ||
} | ||
set onload(fn) { | ||
this._xhr.onload = fn; | ||
getAllResponseHeaders() { | ||
return this._headersAll; | ||
} | ||
get onload() { | ||
return this._xhr.onload; | ||
} | ||
set onloadstart(fn) { | ||
this._xhr.onloadstart = fn; | ||
get readyState() { | ||
if (typeof this._readyState === 'number') { | ||
return this._readyState; | ||
} | ||
} | ||
get onloadstart() { | ||
return this._xhr.onloadstart; | ||
} | ||
set onloadend(fn) { | ||
this._xhr.onloadend = fn; | ||
this._onloadend = fn; | ||
} | ||
get onloadend() { | ||
return this._xhr.onloadend; | ||
return this._onloadend; | ||
} | ||
set onabort(fn) { | ||
this._xhr.onabort = fn; | ||
set onload(fn) { | ||
this._onload = fn; | ||
} | ||
get onabort() { | ||
return this._xhr.onabort; | ||
get onload() { | ||
return this._onload; | ||
} | ||
set onerror(fn) { | ||
this._xhr.onerror = fn; | ||
set onreadystatechange(fn) { | ||
this._onreadystatechange = fn; | ||
} | ||
get onerror() { | ||
return this._xhr.onerror; | ||
get onreadystatechange() { | ||
return this._onreadystatechange; | ||
} | ||
set onprogress(fn) { | ||
this._xhr.onprogress = fn; | ||
this._onprogress = fn; | ||
} | ||
get onprogress() { | ||
return this._xhr.onprogress; | ||
return this._onprogress; | ||
} | ||
set ontimeout(fn) { | ||
this._xhr.ontimeout = fn; | ||
get response() { | ||
return this._response; | ||
} | ||
get ontimeout() { | ||
return this._xhr.ontimeout; | ||
get responseText() { | ||
return this._responseText; | ||
} | ||
set onreadystatechange(fn) { | ||
this._xhr.onreadystatechange = fn; | ||
get responseXML() { | ||
return this._responseXML; | ||
} | ||
get onreadystatechange() { | ||
return this._xhr.onreadystatechange; | ||
} | ||
} | ||
export default XHRProxy; | ||
export default XHR; |
@@ -38,2 +38,6 @@ /////////////////////////////////// | ||
it('should make a large throttled request and accurately limit the bandwidth', function(done) { | ||
var xhr = makeRequest(bigChunkUrl, done, 0, 512); | ||
}); | ||
it('should make two consequent requests', function(done) { | ||
@@ -40,0 +44,0 @@ var xhr = makeRequest(fooUrl, function() { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
191565
25
2863