diff --git a/.jshintrc b/.jshintrc index 11ec1bc..36b6c1e 100644 --- a/.jshintrc +++ b/.jshintrc @@ -116,9 +116,10 @@ //"iLoveDouglas" ], "indent" : 4, // Specify indentation spacing - "mocha": true, "globals": { + //"console": true, "WebSocket": false, + "WorkerGlobalScope": false, "Buffer": false, "require": false, "exports": false, diff --git a/HISTORY.md b/HISTORY.md index b8ceb74..1353010 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,9 @@ Version history =============== +### 0.2.1x (2018-12-xx) +* Add Websocket connection support inside Worker thread to accelerate decompresion and free Window Rendering thread. + ### 0.2.15 (2018-12-06) * Fix possible cache hit on POST|PUT|DELETE (via #118) diff --git a/README.md b/README.md index 560d591..ef10e27 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,47 @@ In full The integration tests require Java JDK 8 be installed. ```bash -npm i +npm install npm run build ``` +### Browser Compatibility + +| Feature/Browser | Chrome | Firefox | Safari | Internet Explorer | +|-----------------|--------|---------|--------|-------------------| +| http2 over ws | x | x | x | x (IE 10+) | +| ws in worker | x | x | x | x (IE 11+) | +| arraybuffer | x | x | x | x (Edge) | + +### Native Browser Implementations + +The example directory contains a simple Web App which tests whether the browser +supports native HTTP2 push with SPEC compliant caching. + + +Start origin +``` +http-server -c-1 +``` + +Start data server +``` + node server.js +``` + +Visit page `https://localhost:8080/` (Note: need to trust TLS cert) + + ### Integration Tests -```bash -npm run test:browser + +Run integration tests on client side: ``` +npm run integration +``` + +Run client worker integration tests" +``` +npm run integration:worker +``` + diff --git a/index.js b/index.js index 50711bc..e3d11f9 100644 --- a/index.js +++ b/index.js @@ -1,2 +1 @@ - require('./lib/http2-cache'); \ No newline at end of file diff --git a/integration-test/.jshintrc b/integration-test/.jshintrc new file mode 100644 index 0000000..c9d3e68 --- /dev/null +++ b/integration-test/.jshintrc @@ -0,0 +1,3 @@ +{ + "mocha": true +} diff --git a/integration-test/http2-worker-itest.js b/integration-test/http2-worker-itest.js new file mode 100644 index 0000000..60fd21e --- /dev/null +++ b/integration-test/http2-worker-itest.js @@ -0,0 +1,445 @@ +var assert = chai.assert; + +var largeRequestCharSize = 1024 * 1024 * 1; // ~ 1MB +//var largeRequestCharSize = 1024 * 1024 * 1; // ~ 5MB +//var largeRequestCharSize = 1024 * 1000; // ~ 1000Kb +var hostname = window.location.hostname; +describe('http2-cache', function () { + + it('proxy() with empty params throws exception', function () { + assert.throws(function () { + XMLHttpRequest.proxy(); + }); + }); + + it('proxy() with no arrays throws exception', function () { + assert.throws(function () { + XMLHttpRequest.proxy("http://url"); + } + ); + }); + + it('proxy() with invalid params throws exception', function () { + assert.throws(function () { + XMLHttpRequest.proxy([1]); + }); + }); + + describe('http-cache regular (no worker)', function () { + + describe('Pure XHR', function () { + + it('should proxy GET request small', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(typeof JSON.parse(xhr.response), 'object'); + assert.equal(xhr.getResponseHeader('content-type'), 'application/json'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://' + hostname + ':7080/config', true); + xhr.send(null); + }); + + it('should proxy GET request large (string)', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + //assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://' + hostname + ':7080/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + + it('should proxy GET request large (gzip+string)', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + //assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://' + hostname + ':7080/gzip/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + + it('should proxy GET request large (arraybuffer)', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.responseType = 'arraybuffer'; + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'object'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://' + hostname + ':7080/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + }); + + describe('HTTP2.js XHR', function () { + + before('configure http2 proxy, and worker (wait 250)', function (done) { + XMLHttpRequest.configuration.useWorker = false; + XMLHttpRequest.configuration.terminateWorker(true); + XMLHttpRequest.proxy(["http://" + hostname + ":7080/config"]); + setTimeout(done, 250); + }); + + it('should proxy GET request small', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof10', true); + xhr.send(null); + }); + + it('should proxy GET request large (string)', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + + it('should proxy GET request large (gzip+string)', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/gzip/charof' + largeRequestCharSize, true); + + // not required to work, and cause + // http2-cache.js:2059 Refused to set unsafe header "accept-encoding" + //xhr.setRequestHeader('accept-encoding','gzip'); + xhr.send(null); + }); + + it('should proxy GET request large (arraybuffer)', function (done) { + + var xhr = new XMLHttpRequest(); + xhr.responseType = 'arraybuffer'; + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'object'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + }); + }); + + describe('HTTP2.js using Worker', function () { + + before('configure http2 proxy, and worker (wait 250)', function (done) { + XMLHttpRequest.configuration.useTransferable = false; + XMLHttpRequest.configuration.useWorker = true; + XMLHttpRequest.configuration.terminateWorker(true); + XMLHttpRequest.proxy(["http://" + hostname + ":7080/config"]); + setTimeout(done, 250); + }); + + it('should proxy GET request small', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof10', true); + xhr.send(null); + }); + + it('should proxy GET request large (string)', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + + it('should proxy GET request large (gzip+string)', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/gzip/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + + it('should proxy GET request large (arraybuffer)', function (done) { + + var xhr = new XMLHttpRequest(); + xhr.responseType = 'arraybuffer'; + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'object'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + }); + + describe('HTTP2.js using Worker (Transferable ArrayBuffer)', function () { + + before('configure http2 proxy, and worker (wait 250)', function (done) { + XMLHttpRequest.configuration.useTransferable = true; + XMLHttpRequest.configuration.useWorker = true; + XMLHttpRequest.configuration.terminateWorker(true); + XMLHttpRequest.proxy(["http://" + hostname + ":7080/config"]); + setTimeout(done, 250); + }); + + it('should proxy GET request small', function (done) { + + var xhr = new XMLHttpRequest(); + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof10', true); + xhr.send(null); + }); + + it('should proxy GET request large (string)', function (done) { + + var xhr = new XMLHttpRequest(); + XMLHttpRequest.configuration.useTransferable = true; + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + + it('should proxy GET request large (string+gzip)', function (done) { + + var xhr = new XMLHttpRequest(); + XMLHttpRequest.configuration.useTransferable = true; + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'string'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/gzip/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + + it('should proxy GET request large (arraybuffer)', function (done) { + + var xhr = new XMLHttpRequest(); + XMLHttpRequest.configuration.useTransferable = true; + xhr.responseType = 'arraybuffer'; + + xhr.onloadstart = function () { + xhr.onprogress = function () { + xhr.onload = function () { + assert.equal(xhr.status, 200); + assert.equal(typeof xhr.response, 'object'); + assert.notEqual(xhr.response.lentgh, 0); + assert.equal(xhr.getResponseHeader('content-type'), 'text/plain; charset=utf-8'); + done(); + }; + }; + }; + + xhr.onerror = function (err) { + throw new TypeError('Network request failed'); + }; + xhr.open('GET', 'http://cache-endpoint/charof' + largeRequestCharSize, true); + xhr.send(null); + }); + }); +}); diff --git a/integration-test/run-in-worker.html b/integration-test/run-in-worker.html new file mode 100644 index 0000000..5c2209b --- /dev/null +++ b/integration-test/run-in-worker.html @@ -0,0 +1,15 @@ + + + http2-cache worker tests + + + + + +
+ + diff --git a/integration-test/run-worker.html b/integration-test/run-worker.html index 5c2209b..1cca5a2 100644 --- a/integration-test/run-worker.html +++ b/integration-test/run-worker.html @@ -1,11 +1,48 @@ - http2-cache worker tests + http2-cache tests + + + + + + diff --git a/integration-test/run-worker.js b/integration-test/run-worker.js index da949ee..fc3670d 100644 --- a/integration-test/run-worker.js +++ b/integration-test/run-worker.js @@ -1,8 +1,7 @@ /* globals self, importScripts, console, mocha:true */ - self.global = self; -delete self['global']; +delete self.global; importScripts('../../../node_modules/mocha/mocha.js'); importScripts('../../../node_modules/chai/chai.js'); @@ -37,7 +36,7 @@ mocha.setup({ ignoreLeaks: false }); -importScripts('./http-cache-test.js'); +importScripts('./http2-worker-itest.js'); mocha.run(); diff --git a/integration-test/run.html b/integration-test/run.html index e1c5e3a..e2f901e 100644 --- a/integration-test/run.html +++ b/integration-test/run.html @@ -10,7 +10,7 @@ mocha.setup({ allowUncaught: true, ui: 'bdd', - slow: 150, + slow: 1, timeout: 15000, bail: false, ignoreLeaks: false diff --git a/integration-test/test-server.js b/integration-test/test-server.js index 5eec6a5..b775b61 100644 --- a/integration-test/test-server.js +++ b/integration-test/test-server.js @@ -1,33 +1,25 @@ -var http = require('http'), - getSocketServer = require('./../test/test-utils.js').getSocketServer, +var getSocketTestServer = require('./../test/test-utils.js').getSocketTestServer, getConfigServer = require('./../test/test-utils.js').getConfigServer; +var socketServerOps = { + //hostname: '192.168.6.143', + hostname: 'localhost', + port: 7081 +}; var configServerOps = { config: { - 'transport': 'ws://localhost:7081/path', - 'worker': true, + 'transport': 'ws://' + socketServerOps.hostname + ':' + socketServerOps.port + '/path', 'proxy': [ - 'http://cache-endpoint/', - 'http://localhost:7080/path/proxy', + 'http://cache-endpoint/' ] }, - port: 7080 -}; - -getConfigServer(configServerOps); - -var socketServerOps = { - port: 7081 + port: 7080 }; +// Start test websocket+http2 server +getSocketTestServer(socketServerOps); -var message = "Hello, Dave. You're looking well today."; -getSocketServer(socketServerOps, function (request, response) { - response.setHeader('Content-Type', 'text/html'); - response.setHeader('Content-Length', message.length); - response.setHeader('Cache-Control', 'private, max-age=0'); - response.write(message); - response.end(); -}); \ No newline at end of file +// Start config http +getConfigServer(configServerOps); diff --git a/lib/configuration.js b/lib/configuration.js index f3187a3..dc1078d 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -10,18 +10,35 @@ var websocket = require('websocket-stream'), Agent = require('./agent').Agent, logger = require('./logger'), parseUrl = require('./utils').parseUrl, + dataToType = require('./utils').dataToType, getOrigin = require('./utils.js').getOrigin, mergeTypedArrays = require('./utils.js').mergeTypedArrays, defaultPort = require('./utils.js').defaultPort, + runningInWorker = require('./utils.js').runningInWorker, CacheListener = require('./cache-listener').CacheListener; //////////////////////////////////////////// Configuration //////////////////////////////////////////// +// Save global self to worker +/* global self:true */ +var runInWorker = runningInWorker(), + worker = runInWorker ? self : null; +/* global self:false */ + +// Detect current script to be injected in worker, null in worker. +var currentScript = (function () { + if (typeof document !== 'undefined' && runInWorker !== true) { + return (document.currentScript || (function () { + var scripts = document.getElementsByTagName('script'); + return scripts[scripts.length - 1]; + }())); + } +}()); function Configuration(options) { - var that = this; + var self = this; // Init default options - that.options = assign({}, that.defaultOptions); + self.options = assign({}, self.defaultOptions); // Set options this.setOptions(options); @@ -29,35 +46,42 @@ function Configuration(options) { EventEmitter.call(this); // Map of Url to transport - that._proxyMap = {}; + self._proxyMap = {}; // Map of PushUrl ro PushRequest - that._pushRequests = {}; + self._pushRequests = {}; - that._activeConfigurationCnt = 0; - that._activeTransportConnections = {}; + self._activeConfigurationCnt = 0; + self._activeTransportConnections = {}; // Init debug/log - that._log = logger.consoleLogger; - that.setDebugLevel(that.options.clientLogLevel || that.options.debug); + self._log = logger.consoleLogger; + self.setDebugLevel(self.options.clientLogLevel || self.options.debug); // Init Cache - that.cache = options.cache || new Cache({ - debug: that.debug, - log: that._log + self.cache = options.cache || new Cache({ + debug: self.debug, + log: self._log }); - that.cacheListener = new CacheListener(that); + self.cacheListener = new CacheListener(self); - that.agent = new Agent({ - log: that._log + self.agent = new Agent({ + log: self._log }); + + // Init worker listener + if (self.runInWorker === true) { + self.initWorker(); + } } util.inherits(Configuration, EventEmitter); var confProto = Configuration.prototype; +confProto.runInWorker = runInWorker; + confProto.defaultOptions = { // Logger debugLevel true='info' or (info|debug|trace) debug: false, @@ -71,13 +95,13 @@ confProto.defaultOptions = { accelerationStrategy: 'always', }; +confProto.debug = false; + confProto.setOptions = function (options) { assign(this.options, options); return this; }; -confProto.debug = false; - confProto.setDebugLevel = function (level) { var that = this; @@ -411,6 +435,9 @@ confProto.parseConfig = function (config) { return config; }; +confProto.useWorker = typeof Worker !== 'undefined'; +confProto.useTransferable = typeof ArrayBuffer !== 'undefined'; + // add config by json /* { @@ -437,27 +464,320 @@ confProto.parseConfig = function (config) { */ confProto.addConfig = function (config) { - var that = this; + var self = this; // Decode config - config = that.parseConfig(config); + config = self.parseConfig(config); // Legacy with warning if (config.pushURL) { - that._log.warn('XMLHttpRequest Http2-Cache configuration "pushURL" is now "push"'); + self._log.warn('XMLHttpRequest Http2-Cache configuration "pushURL" is now "push"'); config.push = config.pushURL; delete config.pushURL; } // Update clientLogLevel if (config.hasOwnProperty('clientLogLevel')) { - that.setDebugLevel(config.clientLogLevel); + self.setDebugLevel(config.clientLogLevel); // Update option to reflect client state - that.options.clientLogLevel = config.clientLogLevel; + self.options.clientLogLevel = config.clientLogLevel; + } + + config.worker = typeof config.worker === 'undefined' ? + self.useWorker : config.worker; + + // Lookup for defaultOptions keys in config + keys(self.defaultOptions).forEach(function (configOption) { + if (config.hasOwnProperty(configOption)) { + // Create config.options if do not exit only if mapping match once at least + config.options = config.options || {}; + config.options[configOption] = config[configOption]; + } + }); + + // Merge config options + if (config.hasOwnProperty('options') && config.options) { + self.setOptions(config.options); } + // Install transport + if (config.hasOwnProperty('transport') && config.transport) { + + // Validate transport + try { + + var transportUri = parseUrl(config.transport); + + + if ( + config.worker !== false && + self.runInWorker === false + ) { + // Create worker if missing + self.registerWorker(config.worker); + + self.configuring(); + + // Call addConfig + self.channel.port1.postMessage({ + method: 'addConfig', + params: { + config: config + } + }); + + // Add transport proxyfied urls + if (config.hasOwnProperty('proxy')) { + self.addTransportUrls(config.proxy, transportUri); + } + } else { + + if (config.channel) { + self.channel = config.channel; + } + + // Add transport proxyfied urls + if (config.hasOwnProperty('proxy')) { + self.addTransportUrls(config.proxy, transportUri); + } + + // Connect transport to push url + if (config.hasOwnProperty('push') && config.push) { + self.openH2StreamForPush(parseUrl(config.push), transportUri); + } + } + + } catch (err) { + self._log.error('XMLHttpRequest Http2-Cache configuration "transport" error', err); + } + } +}; + + +confProto.terminateWorker = function () { + var self = this; + if (typeof self.worker !== 'undefined') { + self.worker.terminate(); + delete self.worker; + } + + if (typeof self.channel !== 'undefined') { + self.channel.port1.close(); + self.channel.port2.close(); + delete self.channel; + } +}; + +confProto.registerWorker = function (worker) { + var self = this; + + // Only one + this.terminateWorker(); + + // TODO detect location + if (typeof Worker === 'undefined') { + throw new Error('Worker not supported'); + } else if (worker instanceof Worker) { + self.worker = worker; + } else if (typeof worker === 'string') { + worker = self.worker = new Worker(worker); + } else if (typeof worker === 'boolean') { + if ( + currentScript !== null && + typeof currentScript.src !== 'undefined' + ) { + worker = self.worker = new Worker(currentScript.src); + } else { + throw new Error('Unable to detect http2-cache script location'); + } + } else { + throw new Error('Invalid worker options.'); + } + + if (typeof self.channel !== 'undefined') { + throw new Error('Channel already open.'); + } + + // TODO close MessageChannel + var channel = self.channel = new MessageChannel(); + + // TODO add push event support + + channel.port1.onmessage = function (event) { + //console.log('channel onmessage', event); + var data = event.data; + if ( + typeof data === 'object' && + typeof data.method === 'string' + ) { + if (data.method === 'configured') { + self.configured(); + } else { + throw new Error('Unknow method: ' + data.method); + } + } else { + throw new Error('Unknow event:' + event); + } + }; + + worker.postMessage({ + method: 'registerWorkerPort', + params: { + port: channel.port2 + } + }, [channel.port2]); +}; + +confProto.initWorker = function () { + //console.log('initWorker'); + var self = this; + worker.addEventListener('message', function (event) { + //console.log('initWorker message', event); + var data = event.data; + if ( + typeof data === 'object' && + typeof data.method === 'string' + ) { + if (data.method === 'registerWorkerPort') { + self.registerWorkerPort(data.params.port); + } else { + throw new Error('Unknow method: ' + data.method); + } + } else { + throw new Error('Unknow event:' + event); + } + }); +}; + +/** + * XmlHttpRequest's getAllResponseHeaders() method returns a string of response + * headers according to the format described here: + * http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method + * This method parses that string into a user-friendly key/value pair object. + */ +function parseResponseHeaders(headerStr) { + var headers = {}; + if (!headerStr) { + return headers; + } + var headerPairs = headerStr.split('\u000d\u000a'); + for (var i = 0, len = headerPairs.length; i < len; i++) { + var headerPair = headerPairs[i]; + var index = headerPair.indexOf('\u003a\u0020'); + if (index > 0) { + var key = headerPair.substring(0, index); + var val = headerPair.substring(index + 2); + headers[key] = val; + } + } + return headers; +} + +confProto.registerWorkerPort = function (port) { + // TODO check typeof port + //console.log('registerWorkerPort', port); + var self = this; // TODO avoid self due worker ? + port.onmessage = function (event) { + //console.log('registerWorkerPort onmessage', event); + var data = event.data; + if ( + typeof data === 'object' && + typeof data.method === 'string' + ) { + if (data.method === 'addConfig') { + var result = self.addConfig(data.params.config); + port.postMessage({ + method: 'configured', + params: data.params + }); + } else if (data.method === 'sendViaChannel') { + + // TODO map not only on _sendViaHttp2 but pure xhr in worker also + var xhr = new XMLHttpRequest(); + + xhr.responsetype = event.data.params.responseType; + + // Share worker client + xhr.addEventListener('readystatechange', function () { + var state = xhr.readyState, + options = { + response: {} + }; + + if (state === XMLHttpRequest.HEADERS_RECEIVED) { + options.response.statusCode = parseInt(xhr.status, 10); + } + + if (state === XMLHttpRequest.DONE) { + options.response.headers = (xhr.responseRaw ? + xhr.responseRaw.headers : + parseResponseHeaders(xhr.getAllResponseHeaders() || "")); + } + + if (state >= XMLHttpRequest.LOADING) { + options.response.data = (xhr.responseRaw ? + xhr.responseRaw.data : xhr.response); + } + + // Use Sharebuffer to prevent copy + var transferable = [], + // TODO Enable via config ? + useTransferable = !!self.useTransferable; // Disabled for now + + // DONE fix Uncaught DataCloneError: Failed to execute 'postMessage' on 'Worker': Value at index 0 does not have a transferable type. + // - https://chromium.googlesource.com/chromium/blink/+/72fef91ac1ef679207f51def8133b336a6f6588f/LayoutTests/fast/events/message-port-clone.html + // DONE fix Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'MessagePort': An ArrayBuffer is neutered and could not be cloned. + // - https://stackoverflow.com/questions/38169672/why-are-transfered-buffers-neutered-in-javascript/38283644 + // DONE fix Firefox InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable (http://localhost:8086/dist/http2-cache.js:942) + // - Parse header only when XMLHttpRequest.DONE + if ( + useTransferable && + options.response && + options.response.data + ) { + options.response.buffer = (options.response.data instanceof ArrayBuffer) ? + options.response.data : dataToType(options.response.data, 'arraybuffer'); + delete options.response.data; + + transferable.push(options.response.buffer); + } + + data.params.port.postMessage({ + method: '_changeState', + params: { + state: state, + options: options + } + }, transferable); + }); + + if (event.data.params.headers) { + event.data.params.headers.forEach(function (value, key) { + self.setRequestHeader(key, value); + }, self); + } + + xhr.open(event.data.params.method, event.data.params.url); + xhr.send(event.data.params.body); + + data.params.port.postMessage({ + method: 'willSendViaChannel', + params: event.data.params.url + }); + + } else { + throw new Error('Unknow method: ' + data.method); + } + } else { + throw new Error('Unknow event:' + event); + } + }; +}; + +confProto.addConfigTransportUrls = function (config) { + var self = this; // Lookup for defaultOptions keys in config - keys(that.defaultOptions).forEach(function (configOption) { + keys(self.defaultOptions).forEach(function (configOption) { if (config.hasOwnProperty(configOption)) { // Create config.options if do not exit only if mapping match once at least config.options = config.options || {}; @@ -467,7 +787,7 @@ confProto.addConfig = function (config) { // Merge config options if (config.hasOwnProperty('options') && config.options) { - that.setOptions(config.options); + self.setOptions(config.options); } // Install transport @@ -480,16 +800,16 @@ confProto.addConfig = function (config) { // Add transport proxyfied urls if (config.hasOwnProperty('proxy')) { - that.addTransportUrls(config.proxy, transportUri); + self.addTransportUrls(config.proxy, transportUri); } // Connect transport to push url if (config.hasOwnProperty('push') && config.push) { - that.openH2StreamForPush(parseUrl(config.push), transportUri); + self.openH2StreamForPush(parseUrl(config.push), transportUri); } } catch (err) { - that._log.error('XMLHttpRequest Http2-Cache configuration "transport" error', err); + self._log.error('XMLHttpRequest Http2-Cache configuration "transport" error', err); } } }; diff --git a/lib/utils.js b/lib/utils.js index 754e729..13a9298 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,7 +2,6 @@ var url = require('url'), util = require("util"), InvalidStateError = require('./errors').InvalidStateError; - var resolvePort = function (u) { u = (u instanceof url.constructor) ? u : url.parse(u); var port = u.port; @@ -62,11 +61,6 @@ var redefine = function (obj, prop, value) { var defineGetter = function (obj, prop, getter) { - if (obj[prop]) { - // TODO, consider erasing scope/hiding (enumerable: false) - obj["_" + prop] = obj[prop]; - } - Object.defineProperty(obj, prop, { enumerable: true, configurable: true, @@ -194,7 +188,31 @@ var mergeTypedArrays = function (a, b) { return c; }; +var addition = Math.pow(2, 16) -1; +function arrayBufferToString(buffer){ + + var bufView = new Uint8Array(buffer); + var length = bufView.length; + var result = ''; + + for(var i = 0;i length){ + addition = length - i; + } + result += String.fromCharCode.apply(null, bufView.subarray(i,i+addition)); + } + + return result; + +} + var toArrayBuffer = function (buf) { + if (buf instanceof Uint8Array) { + // Fix is neutered and could not be clone due parent reference. + return buf.buffer.slice(0); + } + var ab = new ArrayBuffer(buf.length), view = new Uint8Array(ab); @@ -212,8 +230,10 @@ var dataToType = function (data, type) { case "text": if (data instanceof Uint8Array) { return Utf8ArrayToStr(data); - } - return data; + } else if (data instanceof ArrayBuffer) { + return arrayBufferToString(data); + } + break; case "json": return JSON.parse(data); case "arraybuffer": @@ -309,7 +329,15 @@ var serializeXhrBody = function (headers, body) { return body; }; +var runningInWorker = function () { + /* global self:true */ + return typeof window === 'undefined' && + typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope; + /* global self:false */ +}; + module.exports = { + runningInWorker: runningInWorker, parseUrl: parseUrl, redefine: redefine, defineGetter: defineGetter, @@ -324,5 +352,3 @@ module.exports = { mergeTypedArrays: mergeTypedArrays, Utf8ArrayToStr: Utf8ArrayToStr }; - - diff --git a/lib/xhr.js b/lib/xhr.js index 4ff1587..591583d 100644 --- a/lib/xhr.js +++ b/lib/xhr.js @@ -65,6 +65,7 @@ function enableXHROverH2(XMLHttpRequest, configuration) { var xhrInfo = new XhrInfo(); definePublic(XMLHttpRequest, 'configuration', configuration); + definePublic(XMLHttpRequest, 'proxy', function (configs) { return configuration.configure(configs); }); @@ -83,6 +84,32 @@ function enableXHROverH2(XMLHttpRequest, configuration) { }); } + // Expose response and responseText with cached dataToType + function defineXhrResponse(xhr, response) { + + // TODO find a way to expose internaly + defineGetter(xhr, 'responseRaw', function () { + return response; + }); + + defineGetter(xhr, 'response', function () { + // Render as responseType + var responseType = xhrInfo.get(xhr, 'responseType') || ''; + return dataToType(response.data, responseType); + }); + + defineGetter(xhr, 'responseText', function () { + + var responseType = xhrInfo.get(xhr, 'responseType') || ''; + if (responseType !== '' && responseType !== 'text') { + throw new InvalidStateError("Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '" + responseType + "')"); + } + + // Force text rendering + return dataToType(response.data, 'text'); + }); + } + redefineProtoInfo(xhrProto, xhrInfo, "responseType", ''); redefineProtoInfo(xhrProto, xhrInfo, "readyState", 0); redefineProtoInfo(xhrProto, xhrInfo, "timeout"); @@ -212,28 +239,7 @@ function enableXHROverH2(XMLHttpRequest, configuration) { } this.__dispatchEvent(new ProgressEvent('readystatechange')); }); - - // Expose response and responseText with cached dataToType - function defineXhrResponse(xhr, response) { - - defineGetter(xhr, 'response', function () { - // Render as responseType - var responseType = xhrInfo.get(xhr, 'responseType') || ''; - return dataToType(response.data, responseType); - }); - - defineGetter(xhr, 'responseText', function () { - - var responseType = xhrInfo.get(xhr, 'responseType') || ''; - if (responseType !== '' && responseType !== 'text') { - throw new InvalidStateError("Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '" + responseType + "')"); - } - - // Force text rendering - return dataToType(response.data, 'text'); - }); - } - + definePrivate(xhrProto, '_sendViaHttp2', function (destination, body, proxyTransportUrl) { var self = this, @@ -341,7 +347,7 @@ function enableXHROverH2(XMLHttpRequest, configuration) { var request = http2.raw.request({ agent: configuration.agent, // protocol has already been matched by getting transport url - // protocol: destination.protocol, + //protocol: destination.protocol, hostname: destination.hostname, port: destination.port, path: destination.path, @@ -484,8 +490,7 @@ function enableXHROverH2(XMLHttpRequest, configuration) { self.send(); } }); - - // Update cache when receive pushRequest + request.on('push', function(respo) { configuration.onPush(respo); }); @@ -497,6 +502,73 @@ function enableXHROverH2(XMLHttpRequest, configuration) { }); }); + definePrivate(xhrProto, '_sendViaChannel', function (body) { + + var self = this, + cache = configuration.cache, + channel = configuration.channel, + requestUrl = xhrInfo.get(self, 'url'), + requestMethod = xhrInfo.get(self, 'method'), + responseType = xhrInfo.get(self, 'responseType'), + requestHeaders = xhrInfo.get(self, 'headers'), + requestInfo = new RequestInfo(requestMethod, requestUrl, requestHeaders); + + xhrInfo.put(self, 'channel'); + + // TODO close MessageChannel + var xhrChannel = new MessageChannel(), + xhrChannelResponse = null; + + xhrChannel.port1.onmessage = function (event) { + //console.log('sendViaHttp2 channel onmessage', event); + var data = event.data; + if ( + typeof data === 'object' && + typeof data.method === 'string' + ) { + if (data.method === 'willSendViaChannel') { + self.__dispatchEvent(new ProgressEvent('loadstart')); + } else if (data.method === '_changeState') { + + xhrChannelResponse = data.params.options.response; + if (xhrChannelResponse) { + + // Use buffer has data + if (xhrChannelResponse.buffer) { + xhrChannelResponse.data = xhrChannelResponse.buffer; + } + + defineXhrResponse(self, xhrChannelResponse); + } + + self._changeState( + data.params.state, + data.params.options + ); + } else { + throw new Error('Unknow method: ' + data.method); + } + } else { + throw new Error('Unknow event: ' + event); + } + }; + + channel.port1.postMessage({ + method: 'sendViaChannel', + params: { + port: xhrChannel.port2, + url: requestUrl, + responseType: responseType, + method: requestMethod, + headers: requestHeaders + } + }, [xhrChannel.port2]); + + // Set requestInfo revalidate state + cache.revalidate(requestInfo); + + }); + redefine(xhrProto, 'send', function (body) { var self = this, @@ -532,7 +604,12 @@ function enableXHROverH2(XMLHttpRequest, configuration) { body = serializeXhrBody(headers, body); } - self._sendViaHttp2(destination, body, proxyTransportUrl); + // Use channel + if (configuration.channel) { + self._sendViaChannel(destination, body, proxyTransportUrl); + } else { + self._sendViaHttp2(destination, body, proxyTransportUrl); + } } else { @@ -550,16 +627,18 @@ function enableXHROverH2(XMLHttpRequest, configuration) { // Restore responseType self._responseType = xhrInfo.get(self, 'responseType') || ''; - // Fix support for http2.js only if FormData is not defined and not null if (body && typeof FormData === "undefined") { body = serializeXhrBody(headers, body); } - + // Reset Headers - headers.forEach(function (value, key) { - self._setRequestHeader(key, value); - }, self); + var requestHeaders = xhrInfo.get(self, 'headers'); + if (requestHeaders) { + requestHeaders.forEach(function (value, key) { + self._setRequestHeader(key, value); + }, self); + } self._send(body); } @@ -641,4 +720,4 @@ function enableXHROverH2(XMLHttpRequest, configuration) { module.exports = { enableXHROverH2: enableXHROverH2 -}; +}; \ No newline at end of file diff --git a/package.json b/package.json index f9c26a0..40ec3c8 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,10 @@ "test:karma-travis": "npm run build:integration && karma start --no-auto-watch --single-run --browsers=Chrome_travis_ci", "test:karma-dev": "karma start --auto-watch --no-single-run --capture", "test:browser": "npm run build:integration && concurrently \"http-server -p 8086\" \"open http://localhost:8086/integration-test/run.html\"", + "test:browser-worker": "concurrently \"node integration-test/test-server\" \"http-server -p 8086\" \"open http://localhost:8086/integration-test/run-worker.html\"", "test:jasmine": "npm run test:browser", "integration": "npm run test:browser", + "integration:worker": "npm run test:browser-worker", "build": "npm run build:browserify && npm run build:uglify", "build:browserify": "browserify -p [ browserify-banner --template '<%= pkg.name %> v<%= pkg.version %>' ] -r ./lib/http2-cache.js -s http2-cache > ./dist/http2-cache.js", "build:uglify": "uglifyjs --comments '/^/*!/' dist/http2-cache.js -c > ./dist/http2-cache.min.js", diff --git a/test/.jshintrc b/test/.jshintrc new file mode 100644 index 0000000..5164d79 --- /dev/null +++ b/test/.jshintrc @@ -0,0 +1,3 @@ +{ + "mocha": true +} \ No newline at end of file diff --git a/test/test-utils.js b/test/test-utils.js index 3e68e6e..1f661e0 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -109,6 +109,61 @@ var defaultResponseHeaders = { }; +function generateRandAlphaNumStr(len) { + var rdmString = ""; + while (rdmString.length < len) { + rdmString += Math.random().toString(36).substr(2); + } + return rdmString; +} + +var UTF8_BYTES_REG = /%[89ABab]/g; +function lengthInUtf8Bytes(str) { + // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence. + var m = encodeURIComponent(str).match(UTF8_BYTES_REG); + return str.length + (m ? m.length : 0); +} + + +function sendResponse(request, response, body) { + response.writeHead(200, Object.assign({ + "Content-Type": 'text/plain; charset=utf-8', + // TODO 'Content-Length' via lengthInUtf8Bytes ? + }, defaultResponseHeaders)); + var buf = Buffer.from(body, 'utf8'); + response.write(buf); + response.end(); +} + +var zlib = require('zlib'); +function sendGzipResponse(request, response, body) { + + var acceptEncoding = request.headers['accept-encoding']; + if (!acceptEncoding) { + acceptEncoding = ''; + } + + // Note: this is not a conformant accept-encoding parser. + // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 + if (acceptEncoding.match(/\bdeflate\b/)) { + response.writeHead(200, Object.assign({ + "Content-Type": 'text/plain; charset=utf-8', + "content-encoding": 'deflate' + }, defaultResponseHeaders)); + response.write(Buffer.from(zlib.deflateSync(body))); + response.end(); + } else if (acceptEncoding.match(/\bgzip\b/)) { + response.writeHead(200, Object.assign({ + "Content-Type": 'text/plain; charset=utf-8', + "content-encoding": 'gzip' + }, defaultResponseHeaders)); + response.write(Buffer.from(zlib.gzipSync(body))); + response.end(); + } else { + send(request, response, body); + } +} + function _getConfigServer(options, onStart) { return http.createServer(function (request, response) { @@ -152,13 +207,23 @@ function _getConfigServer(options, onStart) { data: Date.now() })); } + } else if (path.startsWith("/charof")) { + var charSize = parseInt(request.url.replace("/charof", ""), 10) || 8192; + var charBody = generateRandAlphaNumStr(charSize); + var charLength = lengthInUtf8Bytes(charBody); + sendResponse(request, response, charBody); + + } else if (path.startsWith("/gzip/charof")) { + var charSize = parseInt(request.url.replace("/gzip/charof", ""), 10) || 8192; + var charBody = generateRandAlphaNumStr(charSize); + var charLength = lengthInUtf8Bytes(charBody); + sendGzipResponse(request, response, charBody); } else { - console.warn("Request for unknown path: " + path); response.writeHead(404); response.end("Not Found"); } - }).listen(options.port, function (){ + }).listen(options.port, function () { console.log("Listening on " + options.port); if (typeof onStart === 'function') { onStart(); @@ -180,21 +245,6 @@ function unicodeStringToTypedArray(s) { return ua; } -function generateRandAlphaNumStr(len) { - var rdmString = ""; - while (rdmString.length < len) { - rdmString += Math.random().toString(36).substr(2); - } - return rdmString; -} - -var UTF8_BYTES_REG = /%[89ABab]/g; -function lengthInUtf8Bytes(str) { - // Matches only the 10.. bytes that are non-initial characters in a multi-byte sequence. - var m = encodeURIComponent(str).match(UTF8_BYTES_REG); - return str.length + (m ? m.length : 0); -} - function getConfigServer(options, onStart) { if ( typeof window !== 'undefined' || @@ -206,6 +256,7 @@ function getConfigServer(options, onStart) { } } + function getSocketServer(options, onRequest, onStart) { if ( typeof window !== 'undefined' || @@ -217,10 +268,33 @@ function getSocketServer(options, onRequest, onStart) { } } +function getSocketTestServer(options, onStart) { + return getSocketServer(options, function (request, response) { + + if (request.url.startsWith("/charof")) { + var charSize = parseInt(request.url.replace("/charof", ""), 10) || 8192, + charBody = generateRandAlphaNumStr(charSize), + charLength = lengthInUtf8Bytes(charBody); + sendResponse(request, response, charBody); + } else if (request.url.startsWith("/gzip/charof")) { + var charGzipSize = parseInt(request.url.replace("/charof", ""), 10) || 8192, + charGzipBody = generateRandAlphaNumStr(charGzipSize); + //send(request, response, charBody); + sendGzipResponse(request, response, charGzipBody); + } else { + response.writeHead(404); + response.end("Not Found"); + } + }, onStart); +} + module.exports = { + sendResponse: sendResponse, + sendGzipResponse: sendGzipResponse, generateRandAlphaNumStr: generateRandAlphaNumStr, lengthInUtf8Bytes: lengthInUtf8Bytes, unicodeStringToTypedArray: unicodeStringToTypedArray, getConfigServer: getConfigServer, + getSocketTestServer: getSocketTestServer, getSocketServer: getSocketServer }; \ No newline at end of file diff --git a/test/utils-test.js b/test/utils-test.js index abaa4a4..234a811 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -1,11 +1,13 @@ /* global console, global */ var mergeTypedArrays = require('../lib/utils').mergeTypedArrays, - Utf8ArrayToStr = require('../lib/utils').Utf8ArrayToStr, - parseUrl = require('../lib/utils').parseUrl, - FormData = require('../lib/form-data').FormData, - serializeXhrBody = require('../lib/utils').serializeXhrBody, + Utf8ArrayToStr = require('../lib/utils').Utf8ArrayToStr, + parseUrl = require('../lib/utils').parseUrl, + runningInWorker = require('../lib/utils').runningInWorker, + FormData = require('../lib/form-data').FormData, + serializeXhrBody = require('../lib/utils').serializeXhrBody, unicodeStringToTypedArray = require('./test-utils').unicodeStringToTypedArray, generateRandAlphaNumStr = require('./test-utils').generateRandAlphaNumStr; + var assert = require('assert'); @@ -18,82 +20,86 @@ require("../lib/http2-cache"); describe('utils', function () { - describe('parseUrl', function () { - it('should parse url with custom port', function () { - var url = "https://example.com:8080/path?query=1", - uri = parseUrl(url); - assert.equal(uri.port, 8080); - assert.equal(uri.host, uri.hostname + ":" + uri.port); - assert.equal(uri.href, url); - }); - - it('should parse url with default https port', function () { - var url = "https://example.com/path?query=1", - uri = parseUrl(url); - assert.equal(uri.port, 443); - assert.equal(uri.host, uri.hostname + ":" + uri.port); - assert.equal(uri.href, url.replace(uri.hostname, uri.host)); - }); - - it('should parse url with default http port', function () { - var url = "http://example.com/path?query=1", - uri = parseUrl(url); - - assert.equal(uri.port, 80); - assert.equal(uri.host, uri.hostname + ":" + uri.port); - assert.equal(uri.href, url.replace(uri.hostname, uri.host)); - }); - - - it('should add defult protocol, hostname, and port', function () { - - global.window = { - location: { - hostname: 'example.com', - protocol: 'https:', - port: '8080' - } - }; - - var url = "/path?query=1", - uri = parseUrl(url); - assert.equal(uri.port, 8080); - assert.equal(uri.host, 'example.com:8080'); - assert.equal(uri.href, "https://example.com:8080/path?query=1"); - }); - }); - - describe('Utf8ArrayToStr', function () { - it('should convert Utf8Array to string', function () { - var aStr = generateRandAlphaNumStr(2500), - a = unicodeStringToTypedArray(aStr); - assert.equal(Utf8ArrayToStr(a), aStr); - }); - it('should handle 4+ byte sequences', function () { - assert.equal(Utf8ArrayToStr([240,159,154,133]), '🚅'); - assert.equal(Utf8ArrayToStr([226,152,131]), '☃'); - }); - }); - - describe('mergeTypedArrays', function () { - it('should merge Utf8Array', function () { - var aStr = generateRandAlphaNumStr(2500), - bStr = generateRandAlphaNumStr(2500), - a = unicodeStringToTypedArray(aStr), - b = unicodeStringToTypedArray(bStr), - c = unicodeStringToTypedArray(aStr + bStr); - assert.equal(Utf8ArrayToStr(mergeTypedArrays(a, b)), Utf8ArrayToStr(c)); - }); - }); + describe('runningInWorker', function () { + assert.equal(runningInWorker(), false); + }); + + describe('parseUrl', function () { + it('should parse url with custom port', function () { + var url = "https://example.com:8080/path?query=1", + uri = parseUrl(url); + assert.equal(uri.port, 8080); + assert.equal(uri.host, uri.hostname + ":" + uri.port); + assert.equal(uri.href, url); + }); + + it('should parse url with default https port', function () { + var url = "https://example.com/path?query=1", + uri = parseUrl(url); + assert.equal(uri.port, 443); + assert.equal(uri.host, uri.hostname + ":" + uri.port); + assert.equal(uri.href, url.replace(uri.hostname, uri.host)); + }); + + it('should parse url with default http port', function () { + var url = "http://example.com/path?query=1", + uri = parseUrl(url); + + assert.equal(uri.port, 80); + assert.equal(uri.host, uri.hostname + ":" + uri.port); + assert.equal(uri.href, url.replace(uri.hostname, uri.host)); + }); + + + it('should add defult protocol, hostname, and port', function () { + + global.window = { + location: { + hostname: 'example.com', + protocol: 'https:', + port: '8080' + } + }; + + var url = "/path?query=1", + uri = parseUrl(url); + assert.equal(uri.port, 8080); + assert.equal(uri.host, 'example.com:8080'); + assert.equal(uri.href, "https://example.com:8080/path?query=1"); + }); + }); + + describe('Utf8ArrayToStr', function () { + it('should convert Utf8Array to string', function () { + var aStr = generateRandAlphaNumStr(2500), + a = unicodeStringToTypedArray(aStr); + assert.equal(Utf8ArrayToStr(a), aStr); + }); + it('should handle 4+ byte sequences', function () { + assert.equal(Utf8ArrayToStr([240,159,154,133]), '🚅'); + assert.equal(Utf8ArrayToStr([226,152,131]), '☃'); + }); + }); + + describe('mergeTypedArrays', function () { + it('should merge Utf8Array', function () { + var aStr = generateRandAlphaNumStr(2500), + bStr = generateRandAlphaNumStr(2500), + a = unicodeStringToTypedArray(aStr), + b = unicodeStringToTypedArray(bStr), + c = unicodeStringToTypedArray(aStr + bStr); + assert.equal(Utf8ArrayToStr(mergeTypedArrays(a, b)), Utf8ArrayToStr(c)); + }); + }); describe('serializeXhrBody', function () { it('should merge serialize Xhr Body', function () { + var headers = new Map(); var formData = new FormData(); formData.append('username', 'Chris'); formData.append('username', 'Bob'); formData.append('gender', 'male'); - var headers = new Map(); var seed = formData._TestBoundary = (+(new Date())).toString(16); assert.equal(serializeXhrBody(headers, formData),