├── tests ├── data │ └── a.js ├── tests.js ├── puppeteer-chrome.js ├── puppeteer-firefox.js ├── test-load.js ├── tests.html ├── test-track.js ├── test-jsonp.js ├── phantom.js ├── test-bust.js ├── test-retry.js ├── test-cache.js ├── server.js ├── test-io-as-bundle.js ├── test-fetch.js ├── test-promise.js ├── test-mock.js └── test-io.js ├── .gitignore ├── dist ├── main.js ├── url.js ├── bust.js ├── load.js ├── jsonp.js ├── scaffold.js ├── track.js ├── retry.js ├── FauxXHR.js ├── fetch.js ├── mock.js ├── cache.js ├── bundle.js └── io.js ├── .travis.yml ├── .editorconfig ├── main.js ├── url.js ├── bower.json ├── load.js ├── bust.js ├── jsonp.js ├── scaffold.js ├── package.json ├── track.js ├── retry.js ├── FauxXHR.js ├── fetch.js ├── mock.js ├── cache.js ├── bundle.js ├── README.md ├── LICENSE └── io.js /tests/data/a.js: -------------------------------------------------------------------------------- 1 | window.__a = 'a'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | (function(_,f){f(window.heya.io,window.heya.io,window.heya.io);}) 2 | (['./io', './cache', './bundle'], function (io) { 3 | io.cache.attach(); 4 | io.bundle.attach(); 5 | 6 | return io; 7 | }); 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | addon: 4 | chrome: stable 5 | 6 | language: node_js 7 | 8 | node_js: 9 | - "12" 10 | 11 | before_script: 12 | - node tests/server.js & 13 | - sleep 5 14 | 15 | script: npm test 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = tab 9 | indent_size = 4 10 | 11 | [*.{json,yml,md}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | (['./io', './cache', './bundle'], function (io) { 3 | io.cache.attach(); 4 | io.bundle.attach(); 5 | 6 | return io; 7 | }); 8 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | define(['heya-unit', 2 | './test-promise', './test-io', './test-jsonp', './test-load', 3 | './test-mock', './test-track', './test-cache', './test-bust', 4 | './test-io-as-bundle', './test-fetch', './test-retry.js'], 5 | function(unit){ 6 | 'use strict'; 7 | 8 | unit.run(); 9 | 10 | return {}; 11 | }); 12 | -------------------------------------------------------------------------------- /dist/url.js: -------------------------------------------------------------------------------- 1 | (function(_,f,g){g=window;g=g.heya||(g.heya={});g=g.io||(g.io={});g.url=f();}) 2 | ([], function () { 3 | 'use strict'; 4 | 5 | return function url (parts) { 6 | var result = parts[0] || ''; 7 | for (var i = 1; i < parts.length; ++i) { 8 | result += encodeURIComponent(arguments[i]) + parts[i]; 9 | } 10 | return result; 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /url.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | ([], function () { 3 | 'use strict'; 4 | 5 | return function url (parts) { 6 | var result = parts[0] || ''; 7 | for (var i = 1; i < parts.length; ++i) { 8 | result += encodeURIComponent(arguments[i]) + parts[i]; 9 | } 10 | return result; 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heya-io", 3 | "description": "Intelligent I/O for browsers.", 4 | "main": "main.js", 5 | "authors": [ 6 | "Eugene Lazutkin (http://www.lazutkin.com/)" 7 | ], 8 | "license": "BSD-3-Clause", 9 | "keywords": [ 10 | "I/O", 11 | "XHR" 12 | ], 13 | "homepage": "https://github.com/heya/io", 14 | "moduleType": [ 15 | "amd", 16 | "globals", 17 | "node" 18 | ], 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "tests" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /dist/bust.js: -------------------------------------------------------------------------------- 1 | (function(_,f){f(window.heya.io);}) 2 | (['./io'], function (io) { 3 | 'use strict'; 4 | 5 | io.bustKey = 'io-bust'; 6 | 7 | io.generateTimestamp = function (options) { 8 | return (new Date().getTime()) + '-' + Math.floor(Math.random() * 1000000); 9 | }; 10 | 11 | var oldBuildUrl = io.buildUrl; 12 | io.buildUrl = function (options) { 13 | var url = oldBuildUrl(options); 14 | if (options.bust) { 15 | var key = options.bust === true ? io.bustKey : options.bust; 16 | return url + (url.indexOf('?') < 0 ? '?' : '&') + 17 | key + '=' + io.generateTimestamp(options); 18 | } 19 | return url; 20 | }; 21 | 22 | return io; 23 | }); 24 | -------------------------------------------------------------------------------- /load.js: -------------------------------------------------------------------------------- 1 | define(['./io'], function (io) { 2 | 'use strict'; 3 | 4 | // script handler 5 | // This is a browser-only module. 6 | 7 | function loadTransport (options, prep) { 8 | var script = document.createElement('script'), 9 | deferred = new io.Deferred(); 10 | script.onload = function () { 11 | deferred.resolve(); 12 | }; 13 | script.onerror = function (e) { 14 | deferred.reject(new io.FailedIO(null, options, e)); 15 | }; 16 | script.src = prep.url; 17 | document.documentElement.appendChild(script); 18 | return deferred.promise || deferred; 19 | } 20 | 21 | io.transports.load = loadTransport; 22 | 23 | return io.makeVerb('load', 'transport'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/puppeteer-chrome.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | 4 | (async () => { 5 | const browser = await puppeteer.launch(); 6 | const page = await browser.newPage(); 7 | 8 | page.on('console', msg => console[typeof console[msg.type()] == 'function' ? msg.type() : 'log'](msg.text())); 9 | page.on('error', e => console.error(e)); 10 | 11 | await page.exposeFunction('callPhantom', async text => { 12 | await browser.close(); 13 | switch (text) { 14 | case "success": 15 | process.exit(0); 16 | break; 17 | case "failure": 18 | process.exit(1); 19 | break; 20 | } 21 | }); 22 | 23 | await page.goto('http://localhost:3000/tests/tests.html'); 24 | })(); 25 | -------------------------------------------------------------------------------- /tests/puppeteer-firefox.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer-firefox'); 2 | 3 | 4 | (async () => { 5 | const browser = await puppeteer.launch(); 6 | const page = await browser.newPage(); 7 | 8 | page.on('console', msg => console[typeof console[msg.type()] == 'function' ? msg.type() : 'log'](msg.text())); 9 | page.on('error', e => console.error(e)); 10 | 11 | await page.exposeFunction('callPhantom', async text => { 12 | await browser.close(); 13 | switch (text) { 14 | case "success": 15 | process.exit(0); 16 | break; 17 | case "failure": 18 | process.exit(1); 19 | break; 20 | } 21 | }); 22 | 23 | await page.goto('http://localhost:3000/tests/tests.html'); 24 | })(); 25 | -------------------------------------------------------------------------------- /dist/load.js: -------------------------------------------------------------------------------- 1 | (function(_,f){window.heya.io.load=f(window.heya.io);}) 2 | (['./io'], function (io) { 3 | 'use strict'; 4 | 5 | // script handler 6 | // This is a browser-only module. 7 | 8 | function loadTransport (options, prep) { 9 | var script = document.createElement('script'), 10 | deferred = new io.Deferred(); 11 | script.onload = function () { 12 | deferred.resolve(); 13 | }; 14 | script.onerror = function (e) { 15 | deferred.reject(new io.FailedIO(null, options, e)); 16 | }; 17 | script.src = prep.url; 18 | document.documentElement.appendChild(script); 19 | return deferred.promise || deferred; 20 | } 21 | 22 | io.transports.load = loadTransport; 23 | 24 | return io.makeVerb('load', 'transport'); 25 | }); 26 | -------------------------------------------------------------------------------- /bust.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | (['./io'], function (io) { 3 | 'use strict'; 4 | 5 | io.bustKey = 'io-bust'; 6 | 7 | io.generateTimestamp = function (options) { 8 | return (new Date().getTime()) + '-' + Math.floor(Math.random() * 1000000); 9 | }; 10 | 11 | var oldBuildUrl = io.buildUrl; 12 | io.buildUrl = function (options) { 13 | var url = oldBuildUrl(options); 14 | if (options.bust) { 15 | var key = options.bust === true ? io.bustKey : options.bust; 16 | return url + (url.indexOf('?') < 0 ? '?' : '&') + 17 | key + '=' + io.generateTimestamp(options); 18 | } 19 | return url; 20 | }; 21 | 22 | return io; 23 | }); 24 | -------------------------------------------------------------------------------- /tests/test-load.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/io', 'heya-io/load', 'heya-async/Deferred'], function (module, unit, io, load, Deferred) { 2 | 'use strict'; 3 | 4 | unit.add(module, [ 5 | function test_setup () { 6 | io.Deferred = Deferred; 7 | }, 8 | function test_exist (t) { 9 | eval(t.TEST('typeof load == "function"')); 10 | }, 11 | function test_simple_io (t) { 12 | var x = t.startAsync(); 13 | load('http://localhost:3000/tests/data/a.js').then(function () { 14 | eval(t.TEST('window.__a = "a"')); 15 | delete window.__a; 16 | x.done(); 17 | }); 18 | }, 19 | function test_io_get_error (t) { 20 | var x = t.startAsync(); 21 | load('http://localhost:3000/tests/data/b.js').then(function () { 22 | t.test(false); // we should not be here 23 | x.done(); 24 | }).catch(function () { 25 | t.test(true); 26 | x.done(); 27 | }); 28 | }, 29 | function test_teardown () { 30 | io.Deferred = io.FauxDeferred; 31 | } 32 | ]); 33 | 34 | return {}; 35 | }); 36 | -------------------------------------------------------------------------------- /jsonp.js: -------------------------------------------------------------------------------- 1 | define(['./io'], function (io) { 2 | 'use strict'; 3 | 4 | // JSONP handler 5 | // This is a browser-only module. 6 | 7 | var counter = 0; 8 | 9 | function jsonpTransport (options, prep) { 10 | var callback = options.callback || 'callback', 11 | name = '__io_jsonp_callback_' + (counter++), 12 | script = document.createElement('script'), 13 | deferred = new io.Deferred(); 14 | window[name] = function (value) { 15 | delete window[name]; 16 | script.parentNode.removeChild(script); 17 | deferred.resolve(value); 18 | }; 19 | script.onerror = function (e) { 20 | delete window[name]; 21 | script.parentNode.removeChild(script); 22 | deferred.reject(new io.FailedIO(null, options, e)); 23 | }; 24 | script.src = prep.url + (prep.url.indexOf('?') >= 0 ? '&' : '?') + 25 | 'callback=' + encodeURIComponent(name); 26 | document.documentElement.appendChild(script); 27 | return deferred.promise || deferred; 28 | } 29 | 30 | io.transports.jsonp = jsonpTransport; 31 | 32 | return io.makeVerb('jsonp', 'transport'); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | heya-io test runner 5 | 6 | 7 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /dist/jsonp.js: -------------------------------------------------------------------------------- 1 | (function(_,f){window.heya.io.jsonp=f(window.heya.io);}) 2 | (['./io'], function (io) { 3 | 'use strict'; 4 | 5 | // JSONP handler 6 | // This is a browser-only module. 7 | 8 | var counter = 0; 9 | 10 | function jsonpTransport (options, prep) { 11 | var callback = options.callback || 'callback', 12 | name = '__io_jsonp_callback_' + (counter++), 13 | script = document.createElement('script'), 14 | deferred = new io.Deferred(); 15 | window[name] = function (value) { 16 | delete window[name]; 17 | script.parentNode.removeChild(script); 18 | deferred.resolve(value); 19 | }; 20 | script.onerror = function (e) { 21 | delete window[name]; 22 | script.parentNode.removeChild(script); 23 | deferred.reject(new io.FailedIO(null, options, e)); 24 | }; 25 | script.src = prep.url + (prep.url.indexOf('?') >= 0 ? '&' : '?') + 26 | 'callback=' + encodeURIComponent(name); 27 | document.documentElement.appendChild(script); 28 | return deferred.promise || deferred; 29 | } 30 | 31 | io.transports.jsonp = jsonpTransport; 32 | 33 | return io.makeVerb('jsonp', 'transport'); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/test-track.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/track', 'heya-async/Deferred-ext'], function (module, unit, io, Deferred) { 2 | 'use strict'; 3 | 4 | unit.add(module, [ 5 | function test_setup () { 6 | io.Deferred = Deferred; 7 | io.track.attach(); 8 | }, 9 | function test_exist (t) { 10 | eval(t.TEST('typeof io.track == "object"')); 11 | }, 12 | function test_dedupe (t) { 13 | var x = t.startAsync(); 14 | Deferred.par( 15 | io.get('http://localhost:3000/api'), 16 | io.get('http://localhost:3000/api') 17 | ).then(function (results) { 18 | eval(t.TEST('results.length === 2')); 19 | eval(t.TEST('results[0].counter === results[1].counter')); 20 | x.done(); 21 | }); 22 | }, 23 | function test_no_dedupe (t) { 24 | var x = t.startAsync(), counter; 25 | io.get('http://localhost:3000/api').then(function (value) { 26 | counter = value.counter; 27 | return io.get('http://localhost:3000/api'); 28 | }).then(function (value) { 29 | eval(t.TEST('counter !== value.counter')); 30 | x.done(); 31 | }); 32 | }, 33 | function test_teardown () { 34 | io.Deferred = io.FauxDeferred; 35 | io.track.detach(); 36 | } 37 | ]); 38 | 39 | return {}; 40 | }); 41 | -------------------------------------------------------------------------------- /dist/scaffold.js: -------------------------------------------------------------------------------- 1 | (function(_,f,g){g=window;g=g.heya||(g.heya={});g=g.io||(g.io={});g.scaffold=f();}) 2 | ([], function () { 3 | 'use strict'; 4 | 5 | // service scaffolding 6 | 7 | function defaultOptIn (options) { 8 | return !options.transport && (!options.method || options.method.toUpperCase() == 'GET'); 9 | } 10 | 11 | var names = ['theDefault', 'attach', 'detach', 'optIn']; 12 | 13 | return function (io, name, priority, callback) { 14 | var service = io[name] = io[name] || {}; 15 | service.isActive = false; 16 | 17 | var methods = [defaultOptIn, attach, detach, optIn]; 18 | 19 | names.forEach(function (name, index) { 20 | if (!(name in service)) { 21 | service[name] = methods[index]; 22 | } 23 | }); 24 | 25 | return io; 26 | 27 | function attach () { 28 | io.attach({ 29 | name: name, 30 | priority: priority, 31 | callback: callback 32 | }); 33 | io[name].isActive = true; 34 | } 35 | 36 | function detach () { 37 | io.detach(name); 38 | io[name].isActive = false; 39 | } 40 | 41 | function optIn (options) { 42 | if (name in options) { 43 | return options[name]; 44 | } 45 | var optIn = service.theDefault; 46 | return typeof optIn == 'function' ? optIn(options) : optIn; 47 | } 48 | }; 49 | }); 50 | -------------------------------------------------------------------------------- /tests/test-jsonp.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/io', 'heya-io/jsonp', 'heya-async/Deferred'], function (module, unit, io, jsonp, Deferred) { 2 | 'use strict'; 3 | 4 | unit.add(module, [ 5 | function test_setup () { 6 | io.Deferred = Deferred; 7 | }, 8 | function test_exist (t) { 9 | eval(t.TEST('typeof jsonp == "function"')); 10 | }, 11 | function test_simple_io (t) { 12 | var x = t.startAsync(); 13 | jsonp('http://localhost:3000/api').then(function (data) { 14 | eval(t.TEST('data.method === "GET"')); 15 | eval(t.TEST('data.body === null')); 16 | x.done(); 17 | }); 18 | }, 19 | function test_io_get_query (t) { 20 | var x = t.startAsync(); 21 | jsonp('http://localhost:3000/api', {a: 1}).then(function (data) { 22 | eval(t.TEST('data.method === "GET"')); 23 | eval(t.TEST('data.query.a === "1"')); 24 | x.done(); 25 | }); 26 | }, 27 | function test_io_get_error (t) { 28 | var x = t.startAsync(); 29 | jsonp('http://localhost:3000/api', {status: 500}).then(function (data) { 30 | t.test(false); // we should not be here 31 | x.done(); 32 | }).catch(function (data) { 33 | eval(t.TEST('data.xhr === null')); 34 | x.done(); 35 | }); 36 | }, 37 | function test_teardown () { 38 | io.Deferred = io.FauxDeferred; 39 | } 40 | ]); 41 | 42 | return {}; 43 | }); 44 | -------------------------------------------------------------------------------- /scaffold.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | ([], function () { 3 | 'use strict'; 4 | 5 | // service scaffolding 6 | 7 | function defaultOptIn (options) { 8 | return !options.transport && (!options.method || options.method.toUpperCase() == 'GET'); 9 | } 10 | 11 | var names = ['theDefault', 'attach', 'detach', 'optIn']; 12 | 13 | return function (io, name, priority, callback) { 14 | var service = io[name] = io[name] || {}; 15 | service.isActive = false; 16 | 17 | var methods = [defaultOptIn, attach, detach, optIn]; 18 | 19 | names.forEach(function (name, index) { 20 | if (!(name in service)) { 21 | service[name] = methods[index]; 22 | } 23 | }); 24 | 25 | return io; 26 | 27 | function attach () { 28 | io.attach({ 29 | name: name, 30 | priority: priority, 31 | callback: callback 32 | }); 33 | io[name].isActive = true; 34 | } 35 | 36 | function detach () { 37 | io.detach(name); 38 | io[name].isActive = false; 39 | } 40 | 41 | function optIn (options) { 42 | if (name in options) { 43 | return options[name]; 44 | } 45 | var optIn = service.theDefault; 46 | return typeof optIn == 'function' ? optIn(options) : optIn; 47 | } 48 | }; 49 | }); 50 | -------------------------------------------------------------------------------- /tests/phantom.js: -------------------------------------------------------------------------------- 1 | phantom.onError = function(msg, trace){ 2 | var msgStack = ["PHANTOM ERROR: " + msg]; 3 | if(trace){ 4 | msgStack.push("TRACE:"); 5 | trace.forEach(function(t){ 6 | msgStack.push(" -> " + (t.file || t.sourceURL) + ": " + t.line + 7 | (t.function ? " (in function " + t.function + ")" : "")); 8 | }); 9 | } 10 | console.error(msgStack.join('\n')); 11 | phantom.exit(1); 12 | }; 13 | 14 | var page = require("webpage").create(); 15 | 16 | page.onError = function(msg){ 17 | console.error("ERROR: " + msg); 18 | phantom.exit(1); 19 | }; 20 | 21 | page.onAlert = function(msg){ 22 | console.log("ALERT: " + msg); 23 | }; 24 | page.onConsoleMessage = function(msg){ 25 | console.log(msg); 26 | }; 27 | page.onCallback = function(msg){ 28 | switch(msg){ 29 | case "success": 30 | phantom.exit(0); 31 | break; 32 | case "failure": 33 | phantom.exit(1); 34 | break; 35 | } 36 | }; 37 | 38 | //var scriptPath = require("system").args[0], 39 | // path = require("fs").absolute( 40 | // (scriptPath.length && scriptPath.charAt(0) == "/" ? "" : "./") + scriptPath).split("/"); 41 | // 42 | //path.pop(); 43 | //path.push("tests.html"); 44 | 45 | page.open(/*path.join("/")*/ "http://localhost:3000/tests/tests.html", function(status){ 46 | if(status !== "success"){ 47 | console.error("ERROR: Can't load a web page."); 48 | phantom.exit(1); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /tests/test-bust.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/io', 'heya-async/Deferred-ext', 'heya-io/bust'], function (module, unit, io, Deferred) { 2 | 'use strict'; 3 | 4 | unit.add(module, [ 5 | function test_setup () { 6 | io.Deferred = Deferred; 7 | }, 8 | function test_exist (t) { 9 | eval(t.TEST('typeof io.bustKey == "string"')); 10 | eval(t.TEST('typeof io.generateTimestamp == "function"')); 11 | }, 12 | function test_bust_query (t) { 13 | var x = t.startAsync(); 14 | io.get({url: 'http://localhost:3000/api', bust: true}).then(function (data) { 15 | eval(t.TEST('data.query["io-bust"]')); 16 | x.done(); 17 | }); 18 | }, 19 | function test_custom_bust (t) { 20 | var x = t.startAsync(); 21 | io.get({url: 'http://localhost:3000/api', bust: 'buster'}).then(function (data) { 22 | eval(t.TEST('data.query.buster')); 23 | x.done(); 24 | }); 25 | }, 26 | function test_two_bust_values (t) { 27 | var x = t.startAsync(); 28 | Deferred.par( 29 | io.get({url: 'http://localhost:3000/api', bust: true}), 30 | io.get({url: 'http://localhost:3000/api', bust: true}) 31 | ).then(function (results) { 32 | eval(t.TEST('results[0].query["io-bust"]')); 33 | eval(t.TEST('results[1].query["io-bust"]')); 34 | eval(t.TEST('results[0].query["io-bust"] !== results[1].query["io-bust"]')); 35 | x.done(); 36 | }); 37 | }, 38 | function test_teardown () { 39 | io.Deferred = io.FauxDeferred; 40 | } 41 | ]); 42 | 43 | return {}; 44 | }); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heya-io", 3 | "version": "1.9.3", 4 | "description": "Intelligent I/O for browsers and Node.", 5 | "main": "main.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "node tests/server.js", 11 | "test-chrome": "node tests/puppeteer-chrome.js", 12 | "test-firefox": "node tests/puppeteer-firefox.js", 13 | "test": "npm run test-chrome && npm run test-firefox", 14 | "dist": "node node_modules/heya-globalize/index.js", 15 | "build": "npm run dist", 16 | "prepublishOnly": "npm run dist" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/heya/io.git" 21 | }, 22 | "keywords": [ 23 | "I/O", 24 | "XHR", 25 | "IO", 26 | "fetch" 27 | ], 28 | "author": "Eugene Lazutkin (http://www.lazutkin.com/)", 29 | "license": "BSD-3-Clause", 30 | "bugs": { 31 | "url": "https://github.com/heya/io/issues" 32 | }, 33 | "homepage": "https://github.com/heya/io#readme", 34 | "devDependencies": { 35 | "body-parser": "^1.19.0", 36 | "express": "^4.17.1", 37 | "heya-async": "^1.0.1", 38 | "heya-bundler": "^1.1.2", 39 | "heya-globalize": "^1.2.1", 40 | "heya-unit": "^0.3.0", 41 | "puppeteer": "^3.0.1", 42 | "puppeteer-firefox": "^0.5.1" 43 | }, 44 | "browserGlobals": { 45 | "!root": "heya.io", 46 | "./io": "heya.io", 47 | "./main": "!heya.io", 48 | "./mock": "!heya.io", 49 | "./track": "!heya.io", 50 | "./cache": "!heya.io", 51 | "./bundle": "!heya.io", 52 | "./bust": "!heya.io", 53 | "./fetch": "!heya.io", 54 | "./retry": "!heya.io" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /dist/track.js: -------------------------------------------------------------------------------- 1 | (function(_,f){f(window.heya.io,window.heya.io.scaffold);}) 2 | (['./io', './scaffold'], function (io, scaffold) { 3 | 'use strict'; 4 | 5 | // keep track of I/O requests 6 | 7 | function track (options, prep, level) { 8 | if (!io.track.optIn(options)) { 9 | return null; 10 | } 11 | 12 | var key = prep.key, deferred = io.track.deferred[key]; 13 | 14 | // check if in flight 15 | if (deferred) { 16 | return deferred.promise || deferred; 17 | } 18 | 19 | // check if required to wait 20 | if (options.wait) { 21 | return flyByKey(key); 22 | } 23 | 24 | // register a request 25 | var promise = flyByKey(key); 26 | deferred = io.track.deferred[key]; 27 | var newPromise = io.request(options, prep, level - 1); 28 | if (promise !== newPromise) { 29 | newPromise.then( 30 | function (value) { deferred.resolve(value, true); }, 31 | function (value) { deferred.reject (value, true); } 32 | ); 33 | } 34 | return promise; 35 | } 36 | 37 | function flyByKey(key) { 38 | var deferred = io.track.deferred[key], needsCleanUp = false; 39 | if (!deferred) { 40 | deferred = io.track.deferred[key] = new io.Deferred(); 41 | needsCleanUp = true; 42 | } 43 | var promise = deferred.promise || deferred; 44 | if (needsCleanUp) { 45 | promise.then(cleanUp, cleanUp); 46 | } 47 | return promise; 48 | function cleanUp () { 49 | delete io.track.deferred[key]; 50 | } 51 | } 52 | 53 | function fly (options) { 54 | options = io.processOptions(typeof options == 'string' ? 55 | {url: options, method: 'GET'} : options); 56 | return io.track.flyByKey(io.makeKey(options)); 57 | } 58 | 59 | function isFlying (options) { 60 | options = io.processOptions(typeof options == 'string' ? 61 | {url: options, method: 'GET'} : options); 62 | return io.track.deferred[io.makeKey(options)]; 63 | } 64 | 65 | 66 | // export 67 | 68 | io.track = { 69 | flyByKey: flyByKey, 70 | fly: fly, 71 | isFlying: isFlying, 72 | 73 | deferred: {} 74 | }; 75 | return scaffold(io, 'track', 40, track); 76 | }); 77 | -------------------------------------------------------------------------------- /track.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | (['./io', './scaffold'], function (io, scaffold) { 3 | 'use strict'; 4 | 5 | // keep track of I/O requests 6 | 7 | function track (options, prep, level) { 8 | if (!io.track.optIn(options)) { 9 | return null; 10 | } 11 | 12 | var key = prep.key, deferred = io.track.deferred[key]; 13 | 14 | // check if in flight 15 | if (deferred) { 16 | return deferred.promise || deferred; 17 | } 18 | 19 | // check if required to wait 20 | if (options.wait) { 21 | return flyByKey(key); 22 | } 23 | 24 | // register a request 25 | var promise = flyByKey(key); 26 | deferred = io.track.deferred[key]; 27 | var newPromise = io.request(options, prep, level - 1); 28 | if (promise !== newPromise) { 29 | newPromise.then( 30 | function (value) { deferred.resolve(value, true); }, 31 | function (value) { deferred.reject (value, true); } 32 | ); 33 | } 34 | return promise; 35 | } 36 | 37 | function flyByKey(key) { 38 | var deferred = io.track.deferred[key], needsCleanUp = false; 39 | if (!deferred) { 40 | deferred = io.track.deferred[key] = new io.Deferred(); 41 | needsCleanUp = true; 42 | } 43 | var promise = deferred.promise || deferred; 44 | if (needsCleanUp) { 45 | promise.then(cleanUp, cleanUp); 46 | } 47 | return promise; 48 | function cleanUp () { 49 | delete io.track.deferred[key]; 50 | } 51 | } 52 | 53 | function fly (options) { 54 | options = io.processOptions(typeof options == 'string' ? 55 | {url: options, method: 'GET'} : options); 56 | return io.track.flyByKey(io.makeKey(options)); 57 | } 58 | 59 | function isFlying (options) { 60 | options = io.processOptions(typeof options == 'string' ? 61 | {url: options, method: 'GET'} : options); 62 | return io.track.deferred[io.makeKey(options)]; 63 | } 64 | 65 | 66 | // export 67 | 68 | io.track = { 69 | flyByKey: flyByKey, 70 | fly: fly, 71 | isFlying: isFlying, 72 | 73 | deferred: {} 74 | }; 75 | return scaffold(io, 'track', 40, track); 76 | }); 77 | -------------------------------------------------------------------------------- /dist/retry.js: -------------------------------------------------------------------------------- 1 | (function(_,f){f(window.heya.io,window.heya.io.scaffold);}) 2 | (['./io', './scaffold'], function (io, scaffold) { 3 | 'use strict'; 4 | 5 | // implement retries for unreliable I/O requests 6 | 7 | function retry (options, prep, level) { 8 | if (typeof options.retries != 'number' || !io.retry.optIn(options)) { 9 | return null; 10 | } 11 | 12 | // pass the request, and retry conditionally 13 | var retries = options.retries, 14 | continueRetries = typeof options.continueRetries == 'function' ? options.continueRetries : continueRetriesIfNot2XX, 15 | nextDelay = typeof options.nextDelay == 'function' ? options.nextDelay : io.retry.nextDelay, 16 | delayMs = typeof options.initDelay == 'number' ? options.initDelay : io.retry.initDelay, 17 | currentRetry = 0; 18 | 19 | return io.request(options, prep, level - 1).then(retries > 0 ? loop : condLoop); 20 | 21 | function loop(result) { 22 | ++currentRetry; 23 | if (--retries >= 0 && continueRetries(result, currentRetry)) { 24 | delayMs = nextDelay(delayMs, currentRetry, options); 25 | return io.retry.delay(delayMs).then(function() { return io.request(options, prep, level - 1); }).then(loop); 26 | } 27 | return result; 28 | } 29 | 30 | function condLoop(result) { 31 | ++currentRetry; 32 | if (continueRetries(result, currentRetry)) { 33 | delayMs = nextDelay(delayMs, currentRetry, options); 34 | return io.retry.delay(delayMs).then(function() { return io.request(options, prep, level - 1); }).then(condLoop); 35 | } 36 | return result; 37 | } 38 | } 39 | 40 | function delay (ms) { 41 | var d = new io.Deferred(); 42 | setTimeout(function () { d.resolve(ms); }, ms); 43 | return d.promise || d; 44 | } 45 | 46 | function continueRetriesIfNot2XX (result) { return result.xhr && (result.xhr.status < 200 || result.xhr.status >= 300); } 47 | 48 | function defaultOptIn (options) { return !options.transport; } 49 | 50 | // export 51 | 52 | io.retry = { 53 | delay: delay, 54 | initDelay: 50, //ms 55 | nextDelay: function (delay, retry, options) { return delay; }, 56 | defaultOptIn: defaultOptIn 57 | }; 58 | return scaffold(io, 'retry', 30, retry); 59 | }); 60 | -------------------------------------------------------------------------------- /retry.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | (['./io', './scaffold'], function (io, scaffold) { 3 | 'use strict'; 4 | 5 | // implement retries for unreliable I/O requests 6 | 7 | function retry (options, prep, level) { 8 | if (typeof options.retries != 'number' || !io.retry.optIn(options)) { 9 | return null; 10 | } 11 | 12 | // pass the request, and retry conditionally 13 | var retries = options.retries, 14 | continueRetries = typeof options.continueRetries == 'function' ? options.continueRetries : continueRetriesIfNot2XX, 15 | nextDelay = typeof options.nextDelay == 'function' ? options.nextDelay : io.retry.nextDelay, 16 | delayMs = typeof options.initDelay == 'number' ? options.initDelay : io.retry.initDelay, 17 | currentRetry = 0; 18 | 19 | return io.request(options, prep, level - 1).then(retries > 0 ? loop : condLoop); 20 | 21 | function loop(result) { 22 | ++currentRetry; 23 | if (--retries >= 0 && continueRetries(result, currentRetry)) { 24 | delayMs = nextDelay(delayMs, currentRetry, options); 25 | return io.retry.delay(delayMs).then(function() { return io.request(options, prep, level - 1); }).then(loop); 26 | } 27 | return result; 28 | } 29 | 30 | function condLoop(result) { 31 | ++currentRetry; 32 | if (continueRetries(result, currentRetry)) { 33 | delayMs = nextDelay(delayMs, currentRetry, options); 34 | return io.retry.delay(delayMs).then(function() { return io.request(options, prep, level - 1); }).then(condLoop); 35 | } 36 | return result; 37 | } 38 | } 39 | 40 | function delay (ms) { 41 | var d = new io.Deferred(); 42 | setTimeout(function () { d.resolve(ms); }, ms); 43 | return d.promise || d; 44 | } 45 | 46 | function continueRetriesIfNot2XX (result) { return result.xhr && (result.xhr.status < 200 || result.xhr.status >= 300); } 47 | 48 | function defaultOptIn (options) { return !options.transport; } 49 | 50 | // export 51 | 52 | io.retry = { 53 | delay: delay, 54 | initDelay: 50, //ms 55 | nextDelay: function (delay, retry, options) { return delay; }, 56 | defaultOptIn: defaultOptIn 57 | }; 58 | return scaffold(io, 'retry', 30, retry); 59 | }); 60 | -------------------------------------------------------------------------------- /dist/FauxXHR.js: -------------------------------------------------------------------------------- 1 | (function(_,f,g){g=window;g=g.heya||(g.heya={});g=g.io||(g.io={});g.FauxXHR=f();}) 2 | ([], function () { 3 | 'use strict'; 4 | 5 | // Faux XHR stand-in to provide a placeholder for cached data 6 | 7 | var isXml = /^(?:application|text)\/(?:x|ht)ml\b/; 8 | // isJson = /^application\/json\b/; 9 | 10 | function FauxXHR (cached) { 11 | // initialize 12 | ['status', 'statusText', 'responseType', 'responseText', 'headers'].forEach(function (key) { 13 | this[key] = cached[key]; 14 | }.bind(this)); 15 | // create response, if required 16 | var mime = this.getResponseHeader('Content-Type'); 17 | switch (true) { 18 | case typeof ArrayBuffer != 'undefined' && this.responseType === 'arraybuffer': 19 | this.response = new ArrayBuffer(2 * this.responseText.length); 20 | for (var view = new Uint16Array(this.response), i = 0, n = this.responseText.length; i < n; ++i) { 21 | view[i] = this.responseText.charCodeAt(i); 22 | } 23 | break; 24 | case typeof Blob != 'undefined' && this.responseType === 'blob': 25 | this.response = new Blob([this.responseText], {type: mime}); 26 | break; 27 | case typeof DOMParser != 'undefined' && this.responseType === 'document': 28 | this.response = new DOMParser().parseFromString(this.responseText, mime); 29 | break; 30 | case this.responseType === 'json': 31 | if ('response' in cached) { 32 | this.response = cached.response; 33 | this.responseText = JSON.stringify(this.response); 34 | } else { 35 | this.response = JSON.parse(this.responseText); 36 | } 37 | break; 38 | default: 39 | this.response = this.responseText; 40 | break; 41 | } 42 | this.responseXML = null; 43 | if (this.responseType == 'document') { 44 | this.responseXML = this.response; 45 | } else if (typeof DOMParser != 'undefined') { 46 | var xmlMime = isXml.exec(mime); 47 | if (xmlMime) { 48 | this.responseXML = new DOMParser().parseFromString(this.responseText, xmlMime[0]); 49 | } 50 | } 51 | } 52 | FauxXHR.prototype = { 53 | readyState: 4, // DONE 54 | timeout: 0, // no timeout 55 | getAllResponseHeaders: function () { 56 | return this.headers; 57 | }, 58 | getResponseHeader: function (header) { 59 | var values = this.headers.match(new RegExp('^' + header + ': .*$', 'gmi')); 60 | return values ? values.map(function (s) { return s.slice(header.length + 2); }).join(', ') : null; 61 | } 62 | }; 63 | 64 | return FauxXHR; 65 | }); 66 | -------------------------------------------------------------------------------- /FauxXHR.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | ([], function () { 3 | 'use strict'; 4 | 5 | // Faux XHR stand-in to provide a placeholder for cached data 6 | 7 | var isXml = /^(?:application|text)\/(?:x|ht)ml\b/; 8 | // isJson = /^application\/json\b/; 9 | 10 | function FauxXHR (cached) { 11 | // initialize 12 | ['status', 'statusText', 'responseType', 'responseText', 'headers'].forEach(function (key) { 13 | this[key] = cached[key]; 14 | }.bind(this)); 15 | // create response, if required 16 | var mime = this.getResponseHeader('Content-Type'); 17 | switch (true) { 18 | case typeof ArrayBuffer != 'undefined' && this.responseType === 'arraybuffer': 19 | this.response = new ArrayBuffer(2 * this.responseText.length); 20 | for (var view = new Uint16Array(this.response), i = 0, n = this.responseText.length; i < n; ++i) { 21 | view[i] = this.responseText.charCodeAt(i); 22 | } 23 | break; 24 | case typeof Blob != 'undefined' && this.responseType === 'blob': 25 | this.response = new Blob([this.responseText], {type: mime}); 26 | break; 27 | case typeof DOMParser != 'undefined' && this.responseType === 'document': 28 | this.response = new DOMParser().parseFromString(this.responseText, mime); 29 | break; 30 | case this.responseType === 'json': 31 | if ('response' in cached) { 32 | this.response = cached.response; 33 | this.responseText = JSON.stringify(this.response); 34 | } else { 35 | this.response = JSON.parse(this.responseText); 36 | } 37 | break; 38 | default: 39 | this.response = this.responseText; 40 | break; 41 | } 42 | this.responseXML = null; 43 | if (this.responseType == 'document') { 44 | this.responseXML = this.response; 45 | } else if (typeof DOMParser != 'undefined') { 46 | var xmlMime = isXml.exec(mime); 47 | if (xmlMime) { 48 | this.responseXML = new DOMParser().parseFromString(this.responseText, xmlMime[0]); 49 | } 50 | } 51 | } 52 | FauxXHR.prototype = { 53 | readyState: 4, // DONE 54 | timeout: 0, // no timeout 55 | getAllResponseHeaders: function () { 56 | return this.headers; 57 | }, 58 | getResponseHeader: function (header) { 59 | var values = this.headers.match(new RegExp('^' + header + ': .*$', 'gmi')); 60 | return values ? values.map(function (s) { return s.slice(header.length + 2); }).join(', ') : null; 61 | } 62 | }; 63 | 64 | return FauxXHR; 65 | }); 66 | -------------------------------------------------------------------------------- /tests/test-retry.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/io', 'heya-io/retry'], function (module, unit, io) { 2 | 'use strict'; 3 | 4 | 5 | var isXml = /^application\/xml\b/, 6 | isOctetStream = /^application\/octet-stream\b/, 7 | isMultiPart = /^multipart\/form-data\b/; 8 | 9 | unit.add(module, [ 10 | function test_setup () { 11 | io.retry.attach(); 12 | }, 13 | function test_exist (t) { 14 | eval(t.TEST('typeof io.retry == "object"')); 15 | }, 16 | function test_no_retry (t) { 17 | var x = t.startAsync(); 18 | io('http://localhost:3000/api').then(function (data) { 19 | eval(t.TEST('data.method === "GET"')); 20 | x.done(); 21 | }); 22 | }, 23 | function test_retry_success (t) { 24 | var x = t.startAsync(); 25 | io({ 26 | url: 'http://localhost:3000/api', 27 | retries: 3 28 | }).then(function (data) { 29 | eval(t.TEST('data.method === "GET"')); 30 | x.done(); 31 | }); 32 | }, 33 | function test_retry_failure (t) { 34 | var x = t.startAsync(); 35 | io({ 36 | url: 'http://localhost:3000/xxx', // doesn't exist 37 | retries: 3 38 | }).catch(function (error) { 39 | eval(t.TEST('error.xhr.status === 404')); 40 | x.done(); 41 | }); 42 | }, 43 | function test_cond_retry_counter (t) { 44 | var x = t.startAsync(), counter = 0; 45 | io({ 46 | url: 'http://localhost:3000/xxx', // doesn't exist 47 | retries: 3, 48 | continueRetries: function () { ++counter; return true; } 49 | }).catch(function (error) { 50 | eval(t.TEST('error.xhr.status === 404')); 51 | eval(t.TEST('counter === 3')); 52 | x.done(); 53 | }); 54 | }, 55 | function test_cond_retry_counter_term_by_func (t) { 56 | var x = t.startAsync(), counter = 0; 57 | io({ 58 | url: 'http://localhost:3000/xxx', // doesn't exist 59 | retries: 5, 60 | continueRetries: function (result, retries) { ++counter; return retries < 2; } 61 | }).catch(function (error) { 62 | eval(t.TEST('error.xhr.status === 404')); 63 | eval(t.TEST('counter === 2')); 64 | x.done(); 65 | }); 66 | }, 67 | function test_cond_retry_failure (t) { 68 | var x = t.startAsync(), counter = 0; 69 | io({ 70 | url: 'http://localhost:3000/xxx', // doesn't exist 71 | retries: 0, 72 | continueRetries: function (result, retries) { ++counter; return retries < 2; } 73 | }).catch(function (error) { 74 | eval(t.TEST('error.xhr.status === 404')); 75 | eval(t.TEST('counter === 2')); 76 | x.done(); 77 | }); 78 | }, 79 | function test_retry_local (t) { 80 | var x = t.startAsync(), counter = 0; 81 | io({ 82 | url: 'http://localhost:3000/xxx', // doesn't exist 83 | retries: 3, 84 | initDelay: 20, 85 | nextDelay: function (delay) { ++counter; return 2 * delay; } 86 | }).catch(function (error) { 87 | eval(t.TEST('error.xhr.status === 404')); 88 | eval(t.TEST('counter === 3')); 89 | x.done(); 90 | }); 91 | }, 92 | function test_teardown () { 93 | io.retry.detach(); 94 | } 95 | ]); 96 | 97 | return {}; 98 | }); 99 | -------------------------------------------------------------------------------- /fetch.js: -------------------------------------------------------------------------------- 1 | define(['./io', './FauxXHR'], function (io, FauxXHR) { 2 | 'use strict'; 3 | 4 | // fetch() handler 5 | // This is a browser-only module. 6 | 7 | var isJson = /^application\/json\b/; 8 | 9 | function fetchTransport (options, prep) { 10 | var headers = new Headers(options.headers || {}), 11 | req = { 12 | method: options.method, 13 | mode: typeof options.fetchMode == 'string' || io.fetch.defaultMode, 14 | cache: typeof options.fetchCache == 'string' || io.fetch.defaultCache, 15 | redirect: typeof options.fetchRedirect == 'string' || io.fetch.defaultRedirect, 16 | referrer: typeof options.fetchReferrer == 'string' || io.fetch.defaultReferrer, 17 | referrerPolicy: typeof options.fetchReferrerPolicy == 'string' || io.fetch.defaultReferrerPolicy, 18 | credentials: typeof options.fetchCredentials == 'string' || (('withCredentials' in options) && 19 | (options.withCredentials ? 'include' : 'same-origin')) || io.fetch.defaultCredentials 20 | }, response; 21 | if (options.fetchIntegrity) req.integrity = options.fetchIntegrity; 22 | req.body = io.processData({setRequestHeader: function (key, value) { 23 | headers.append(key, value); 24 | }}, options, prep.data); 25 | if (typeof Document !== 'undefined' && typeof XMLSerializer !== 'undefined' && req.body instanceof Document) { 26 | if (!headers.has('Content-Type')) { 27 | headers.append('Content-Type', 'application/xml'); 28 | } 29 | req.body = new XMLSerializer().serializeToString(req.body); 30 | } 31 | req.headers = headers; 32 | return fetch(prep.url, req).catch(function (err) { 33 | return Promise.reject(new io.FailedIO(null, options, err)); 34 | }).then(function (res) { 35 | response = res; 36 | return res.text(); 37 | }).then(function (body) { 38 | return new io.Result(new FauxXHR({ 39 | status: response.status, 40 | statusText: response.statusText, 41 | headers: getAllResponseHeaders(response.headers, options.mime), 42 | responseType: options.responseType, 43 | responseText: body 44 | }), options); 45 | }); 46 | } 47 | 48 | io.fetch = { 49 | attach: attach, 50 | detach: detach, 51 | // defaults 52 | defaultMode: 'cors', 53 | defaultCache: 'default', 54 | defaultRedirect: 'follow', 55 | defaultReferrer: 'client', 56 | defaultReferrerPolicy: 'no-referrer-when-downgrade', 57 | defaultCredentials: 'same-origin' 58 | }; 59 | 60 | return io; 61 | 62 | var oldTransport; 63 | 64 | function attach () { 65 | if (io.defaultTransport !== fetchTransport) { 66 | oldTransport = io.defaultTransport; 67 | io.defaultTransport = fetchTransport; 68 | return true; 69 | } 70 | return false; 71 | } 72 | 73 | function detach () { 74 | if (oldTransport && io.defaultTransport === fetchTransport) { 75 | io.defaultTransport = oldTransport; 76 | oldTransport = null; 77 | return true; 78 | } 79 | return false; 80 | } 81 | 82 | function getAllResponseHeaders (headers, mime) { 83 | try { 84 | var h = []; 85 | if (mime) { 86 | for (var pair of headers) { 87 | if (pair[0].toLowerCase() !== 'content-type') { 88 | h.push(pair[0] + ': ' + pair[1]); 89 | } 90 | } 91 | h.push('Content-Type: ' + mime); 92 | } else { 93 | for (var pair of headers) { 94 | h.push(pair[0] + ': ' + pair[1]); 95 | } 96 | } 97 | return h.join('\n'); 98 | } catch (e) { 99 | // suppress 100 | } 101 | return headers.has('Content-Type') ? 'Content-Type: ' + headers.get('Content-Type') : ''; 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /dist/fetch.js: -------------------------------------------------------------------------------- 1 | (function(_,f){f(window.heya.io,window.heya.io.FauxXHR);}) 2 | (['./io', './FauxXHR'], function (io, FauxXHR) { 3 | 'use strict'; 4 | 5 | // fetch() handler 6 | // This is a browser-only module. 7 | 8 | var isJson = /^application\/json\b/; 9 | 10 | function fetchTransport (options, prep) { 11 | var headers = new Headers(options.headers || {}), 12 | req = { 13 | method: options.method, 14 | mode: typeof options.fetchMode == 'string' || io.fetch.defaultMode, 15 | cache: typeof options.fetchCache == 'string' || io.fetch.defaultCache, 16 | redirect: typeof options.fetchRedirect == 'string' || io.fetch.defaultRedirect, 17 | referrer: typeof options.fetchReferrer == 'string' || io.fetch.defaultReferrer, 18 | referrerPolicy: typeof options.fetchReferrerPolicy == 'string' || io.fetch.defaultReferrerPolicy, 19 | credentials: typeof options.fetchCredentials == 'string' || (('withCredentials' in options) && 20 | (options.withCredentials ? 'include' : 'same-origin')) || io.fetch.defaultCredentials 21 | }, response; 22 | if (options.fetchIntegrity) req.integrity = options.fetchIntegrity; 23 | req.body = io.processData({setRequestHeader: function (key, value) { 24 | headers.append(key, value); 25 | }}, options, prep.data); 26 | if (typeof Document !== 'undefined' && typeof XMLSerializer !== 'undefined' && req.body instanceof Document) { 27 | if (!headers.has('Content-Type')) { 28 | headers.append('Content-Type', 'application/xml'); 29 | } 30 | req.body = new XMLSerializer().serializeToString(req.body); 31 | } 32 | req.headers = headers; 33 | return fetch(prep.url, req).catch(function (err) { 34 | return Promise.reject(new io.FailedIO(null, options, err)); 35 | }).then(function (res) { 36 | response = res; 37 | return res.text(); 38 | }).then(function (body) { 39 | return new io.Result(new FauxXHR({ 40 | status: response.status, 41 | statusText: response.statusText, 42 | headers: getAllResponseHeaders(response.headers, options.mime), 43 | responseType: options.responseType, 44 | responseText: body 45 | }), options); 46 | }); 47 | } 48 | 49 | io.fetch = { 50 | attach: attach, 51 | detach: detach, 52 | // defaults 53 | defaultMode: 'cors', 54 | defaultCache: 'default', 55 | defaultRedirect: 'follow', 56 | defaultReferrer: 'client', 57 | defaultReferrerPolicy: 'no-referrer-when-downgrade', 58 | defaultCredentials: 'same-origin' 59 | }; 60 | 61 | return io; 62 | 63 | var oldTransport; 64 | 65 | function attach () { 66 | if (io.defaultTransport !== fetchTransport) { 67 | oldTransport = io.defaultTransport; 68 | io.defaultTransport = fetchTransport; 69 | return true; 70 | } 71 | return false; 72 | } 73 | 74 | function detach () { 75 | if (oldTransport && io.defaultTransport === fetchTransport) { 76 | io.defaultTransport = oldTransport; 77 | oldTransport = null; 78 | return true; 79 | } 80 | return false; 81 | } 82 | 83 | function getAllResponseHeaders (headers, mime) { 84 | try { 85 | var h = []; 86 | if (mime) { 87 | for (var pair of headers) { 88 | if (pair[0].toLowerCase() !== 'content-type') { 89 | h.push(pair[0] + ': ' + pair[1]); 90 | } 91 | } 92 | h.push('Content-Type: ' + mime); 93 | } else { 94 | for (var pair of headers) { 95 | h.push(pair[0] + ': ' + pair[1]); 96 | } 97 | } 98 | return h.join('\n'); 99 | } catch (e) { 100 | // suppress 101 | } 102 | return headers.has('Content-Type') ? 'Content-Type: ' + headers.get('Content-Type') : ''; 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /tests/test-cache.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/cache', 'heya-async/Deferred-ext'], function (module, unit, io, Deferred) { 2 | 'use strict'; 3 | 4 | unit.add(module, [ 5 | function test_setup () { 6 | io.Deferred = Deferred; 7 | io.cache.attach(); 8 | }, 9 | function test_exist (t) { 10 | eval(t.TEST('typeof io.cache == "object"')); 11 | }, 12 | function test_cache (t) { 13 | var x = t.startAsync(), counter; 14 | io.cache.clear(); 15 | // the next one should be from a server 16 | io.get('http://localhost:3000/api').then(function (value) { 17 | counter = value.counter; 18 | // the next one should be from cache 19 | return io.get('http://localhost:3000/api'); 20 | }).then(function (value) { 21 | eval(t.TEST('counter === value.counter')); 22 | io.cache.clear(); 23 | // the next one should be from a server 24 | return io.get('http://localhost:3000/api'); 25 | }).then(function (value) { 26 | eval(t.TEST('counter !== value.counter')); 27 | x.done(); 28 | }); 29 | }, 30 | function test_cache_remove_item (t) { 31 | var x = t.startAsync(), counter; 32 | io.cache.clear(); 33 | // the next one should be from a server 34 | io.get('http://localhost:3000/api').then(function (value) { 35 | counter = value.counter; 36 | // the next one should be from cache 37 | return io.get('http://localhost:3000/api'); 38 | }).then(function (value) { 39 | eval(t.TEST('counter === value.counter')); 40 | io.cache.remove('http://localhost:3000/api'); 41 | // the next one should be from a server 42 | return io.get('http://localhost:3000/api'); 43 | }).then(function (value) { 44 | eval(t.TEST('counter !== value.counter')); 45 | x.done(); 46 | }); 47 | }, 48 | function test_cache_remove_wildcard (t) { 49 | var x = t.startAsync(), counter; 50 | io.cache.clear(); 51 | // the next one should be from a server 52 | io.get('http://localhost:3000/api/xxx').then(function (value) { 53 | counter = value.counter; 54 | // the next one should be from cache 55 | return io.get('http://localhost:3000/api/xxx'); 56 | }).then(function (value) { 57 | eval(t.TEST('counter === value.counter')); 58 | io.cache.remove('http://localhost:3000/api*'); 59 | // the next one should be from a server 60 | return io.get('http://localhost:3000/api/xxx'); 61 | }).then(function (value) { 62 | eval(t.TEST('counter !== value.counter')); 63 | x.done(); 64 | }); 65 | }, 66 | function test_cache_remove_regexp (t) { 67 | var x = t.startAsync(), counter1, counter2; 68 | io.cache.clear(); 69 | // the next one should be from a server 70 | io.get('http://localhost:3000/api/xxx').then(function (value) { 71 | counter1 = value.counter; 72 | // the next one should be from cache 73 | return io.get('http://localhost:3000/api/xxx'); 74 | }).then(function (value) { 75 | eval(t.TEST('counter1 === value.counter')); 76 | // the next one should be from a server 77 | return io.get('http://localhost:3000/api/yyy'); 78 | }).then(function (value) { 79 | counter2 = value.counter; 80 | // the next one should be from cache 81 | return io.get('http://localhost:3000/api/yyy'); 82 | }).then(function (value) { 83 | eval(t.TEST('counter2 === value.counter')); 84 | io.cache.remove(/\bxxx\b/); 85 | // the next one should be from a server 86 | return io.get('http://localhost:3000/api/xxx'); 87 | }).then(function (value) { 88 | eval(t.TEST('counter1 !== value.counter')); 89 | // the next one should be from cache 90 | return io.get('http://localhost:3000/api/yyy'); 91 | }).then(function (value) { 92 | eval(t.TEST('counter2 === value.counter')); 93 | x.done(); 94 | }); 95 | }, 96 | function test_teardown () { 97 | io.Deferred = io.FauxDeferred; 98 | io.cache.detach(); 99 | io.cache.clear(); 100 | } 101 | ]); 102 | 103 | return {}; 104 | }); 105 | -------------------------------------------------------------------------------- /dist/mock.js: -------------------------------------------------------------------------------- 1 | (function(_,f){f(window.heya.io,window.heya.io.FauxXHR,window.heya.io.scaffold);}) 2 | (['./io', './FauxXHR', './scaffold'], function (io, FauxXHR, scaffold) { 3 | 'use strict'; 4 | 5 | // mock I/O requests 6 | 7 | function mock (options, prep, level) { 8 | if (!io.mock.optIn(options)) { 9 | return null; 10 | } 11 | 12 | var url = options.url, callback = io.mock.exact[url]; 13 | if (!callback) { 14 | var index = find(url), prefix = io.mock.prefix; 15 | if (index < prefix.length && url === prefix[index].url) { 16 | callback = prefix[index].callback; 17 | } else { 18 | for (var i = index - 1; i >= 0; --i) { 19 | var pattern = prefix[i].url; 20 | if (pattern.length <= url.length && pattern === url.substring(0, pattern.length)) { 21 | callback = prefix[i].callback; 22 | break; 23 | } 24 | } 25 | } 26 | } 27 | if (!callback) { 28 | const regexps = io.mock.regexp; 29 | for (let i = 0; i < regexps.length; ++i) { 30 | const value = regexps[i]; 31 | if (value.url.test(url)) { 32 | callback = value.callback; 33 | break; 34 | } 35 | } 36 | } 37 | if (!callback) { 38 | const match = io.mock.match; 39 | for (let i = 0; i < match.length; ++i) { 40 | const value = match[i]; 41 | if (value.url(options, prep, level)) { 42 | callback = value.callback; 43 | break; 44 | } 45 | } 46 | } 47 | 48 | return callback ? wrap(options, callback(options, prep, level)) : null; 49 | } 50 | 51 | function find (url) { 52 | // binary search, see https://github.com/heya/ctr/blob/master/algos/binarySearch.js 53 | var prefix = io.mock.prefix, l = 0, r = prefix.length; 54 | while (l < r) { 55 | var m = ((r - l) >> 1) + l, x = prefix[m].url; 56 | if (x < url) { 57 | l = m + 1; 58 | } else { 59 | r = m; 60 | } 61 | } 62 | return l; 63 | } 64 | 65 | function wrap (options, value) { 66 | if (value instanceof FauxXHR) { 67 | value = new io.Result(value, options, null); 68 | } 69 | return value && typeof value.then == 'function' ? value : io.Deferred.resolve(value); 70 | } 71 | 72 | function makeXHR (xhr) { 73 | return new FauxXHR({ 74 | status: xhr.status || 200, 75 | statusText: xhr.statusText || 'OK', 76 | responseType: xhr.responseType || '', 77 | responseText: xhr.responseText || '', 78 | headers: xhr.headers || '' 79 | }); 80 | } 81 | 82 | 83 | // export 84 | 85 | io.mock = function (url, callback) { 86 | if (url && typeof url == 'string') { 87 | if (url.charAt(url.length - 1) === '*') { 88 | // prefix 89 | url = url.substring(0, url.length - 1); 90 | var index = find(url), prefix = io.mock.prefix; 91 | if (index < prefix.length && url === prefix[index].url) { 92 | if (callback) { 93 | prefix[index].callback = callback; 94 | } else { 95 | prefix.splice(index, 1); 96 | } 97 | return; 98 | } 99 | prefix.splice(index, 0, {url: url, callback: callback}); 100 | } else { 101 | // exact 102 | if (callback) { 103 | io.mock.exact[url] = callback; 104 | } else { 105 | delete io.mock.exact[url]; 106 | } 107 | } 108 | } else if (url instanceof RegExp) { 109 | const regexps = io.mock.regexp; 110 | for (let i = 0; i < regexps.length; ++i) { 111 | const value = regexps[i]; 112 | if (value.url.source == url.source && value.url.flags == url.flags) { 113 | if (callback) { 114 | value.callback = callback; 115 | } else { 116 | regexps.splice(i, 1); 117 | } 118 | return; 119 | } 120 | } 121 | regexps.splice(regexps.length, 0, {url: url, callback: callback}); 122 | } else if (typeof url == 'function') { 123 | const match = io.mock.match; 124 | for (let i = 0; i < match.length; ++i) { 125 | const value = match[i]; 126 | if (value.url === url) { 127 | if (callback) { 128 | value.callback = callback; 129 | } else { 130 | match.splice(i, 1); 131 | } 132 | return; 133 | } 134 | } 135 | match.splice(match.length, 0, {url: url, callback: callback}); 136 | } 137 | }; 138 | 139 | io.mock.theDefault = true; 140 | 141 | io.mock.exact = {}; 142 | io.mock.prefix = []; 143 | io.mock.regexp = []; 144 | io.mock.match = []; 145 | 146 | io.mock.makeXHR = makeXHR; 147 | 148 | return scaffold(io, 'mock', 20, mock); 149 | }); 150 | -------------------------------------------------------------------------------- /mock.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | (['./io', './FauxXHR', './scaffold'], function (io, FauxXHR, scaffold) { 3 | 'use strict'; 4 | 5 | // mock I/O requests 6 | 7 | function mock (options, prep, level) { 8 | if (!io.mock.optIn(options)) { 9 | return null; 10 | } 11 | 12 | var url = options.url, callback = io.mock.exact[url]; 13 | if (!callback) { 14 | var index = find(url), prefix = io.mock.prefix; 15 | if (index < prefix.length && url === prefix[index].url) { 16 | callback = prefix[index].callback; 17 | } else { 18 | for (var i = index - 1; i >= 0; --i) { 19 | var pattern = prefix[i].url; 20 | if (pattern.length <= url.length && pattern === url.substring(0, pattern.length)) { 21 | callback = prefix[i].callback; 22 | break; 23 | } 24 | } 25 | } 26 | } 27 | if (!callback) { 28 | const regexps = io.mock.regexp; 29 | for (let i = 0; i < regexps.length; ++i) { 30 | const value = regexps[i]; 31 | if (value.url.test(url)) { 32 | callback = value.callback; 33 | break; 34 | } 35 | } 36 | } 37 | if (!callback) { 38 | const match = io.mock.match; 39 | for (let i = 0; i < match.length; ++i) { 40 | const value = match[i]; 41 | if (value.url(options, prep, level)) { 42 | callback = value.callback; 43 | break; 44 | } 45 | } 46 | } 47 | 48 | return callback ? wrap(options, callback(options, prep, level)) : null; 49 | } 50 | 51 | function find (url) { 52 | // binary search, see https://github.com/heya/ctr/blob/master/algos/binarySearch.js 53 | var prefix = io.mock.prefix, l = 0, r = prefix.length; 54 | while (l < r) { 55 | var m = ((r - l) >> 1) + l, x = prefix[m].url; 56 | if (x < url) { 57 | l = m + 1; 58 | } else { 59 | r = m; 60 | } 61 | } 62 | return l; 63 | } 64 | 65 | function wrap (options, value) { 66 | if (value instanceof FauxXHR) { 67 | value = new io.Result(value, options, null); 68 | } 69 | return value && typeof value.then == 'function' ? value : io.Deferred.resolve(value); 70 | } 71 | 72 | function makeXHR (xhr) { 73 | return new FauxXHR({ 74 | status: xhr.status || 200, 75 | statusText: xhr.statusText || 'OK', 76 | responseType: xhr.responseType || '', 77 | responseText: xhr.responseText || '', 78 | headers: xhr.headers || '' 79 | }); 80 | } 81 | 82 | 83 | // export 84 | 85 | io.mock = function (url, callback) { 86 | if (url && typeof url == 'string') { 87 | if (url.charAt(url.length - 1) === '*') { 88 | // prefix 89 | url = url.substring(0, url.length - 1); 90 | var index = find(url), prefix = io.mock.prefix; 91 | if (index < prefix.length && url === prefix[index].url) { 92 | if (callback) { 93 | prefix[index].callback = callback; 94 | } else { 95 | prefix.splice(index, 1); 96 | } 97 | return; 98 | } 99 | prefix.splice(index, 0, {url: url, callback: callback}); 100 | } else { 101 | // exact 102 | if (callback) { 103 | io.mock.exact[url] = callback; 104 | } else { 105 | delete io.mock.exact[url]; 106 | } 107 | } 108 | } else if (url instanceof RegExp) { 109 | const regexps = io.mock.regexp; 110 | for (let i = 0; i < regexps.length; ++i) { 111 | const value = regexps[i]; 112 | if (value.url.source == url.source && value.url.flags == url.flags) { 113 | if (callback) { 114 | value.callback = callback; 115 | } else { 116 | regexps.splice(i, 1); 117 | } 118 | return; 119 | } 120 | } 121 | regexps.splice(regexps.length, 0, {url: url, callback: callback}); 122 | } else if (typeof url == 'function') { 123 | const match = io.mock.match; 124 | for (let i = 0; i < match.length; ++i) { 125 | const value = match[i]; 126 | if (value.url === url) { 127 | if (callback) { 128 | value.callback = callback; 129 | } else { 130 | match.splice(i, 1); 131 | } 132 | return; 133 | } 134 | } 135 | match.splice(match.length, 0, {url: url, callback: callback}); 136 | } 137 | }; 138 | 139 | io.mock.theDefault = true; 140 | 141 | io.mock.exact = {}; 142 | io.mock.prefix = []; 143 | io.mock.regexp = []; 144 | io.mock.match = []; 145 | 146 | io.mock.makeXHR = makeXHR; 147 | 148 | return scaffold(io, 'mock', 20, mock); 149 | }); 150 | -------------------------------------------------------------------------------- /cache.js: -------------------------------------------------------------------------------- 1 | define(['./io', './FauxXHR', './scaffold'], function (io, FauxXHR, scaffold) { 2 | 'use strict'; 3 | 4 | // cache I/O requests 5 | // this is a browser-only module. 6 | 7 | function cache (options, prep, level) { 8 | var key = prep.key; 9 | 10 | if (options.wait || options.bust || 11 | io.track && io.track.deferred[key] || 12 | !io.cache.optIn(options)) { 13 | return null; 14 | } 15 | 16 | // retrieve data, if available 17 | var data = io.cache.storage.retrieve(key); 18 | if (typeof data !== 'undefined') { 19 | return io.Deferred.resolve(new io.Result(new FauxXHR(data), options, null)); 20 | } 21 | 22 | // pass the request, and cache the result 23 | var promise = io.request(options, prep, level - 1); 24 | promise.then(function (result) { saveByKey(key, result); }); 25 | return promise; 26 | } 27 | 28 | function saveByKey (key, result) { 29 | var xhr; 30 | if (result instanceof XMLHttpRequest || result instanceof FauxXHR) { 31 | xhr = result; 32 | } else if (result && (result.xhr instanceof XMLHttpRequest || result.xhr instanceof FauxXHR)) { 33 | xhr = result.xhr; 34 | } 35 | if (xhr) { 36 | if (xhr.status >= 200 && xhr.status < 300) { 37 | io.cache.storage.store(key, { 38 | status: xhr.status, 39 | statusText: xhr.statusText, 40 | responseType: xhr.responseType, 41 | responseText: xhr.responseText, 42 | headers: xhr.getAllResponseHeaders() 43 | }); 44 | } 45 | } else { 46 | io.cache.storage.store(key, { 47 | status: 200, 48 | statusText: 'OK', 49 | responseType: 'json', 50 | response: result, 51 | headers: 'Content-Type: application/json' 52 | }); 53 | } 54 | } 55 | 56 | function save (options, result) { 57 | options = io.processOptions(typeof options == 'string' ? {url: options, method: 'GET'} : options); 58 | io.cache.saveByKey(io.makeKey(options), result); 59 | } 60 | 61 | function remove(options) { 62 | if (typeof options == 'function') { 63 | var keys = io.cache.storage.getKeys(), 64 | regexp = new RegExp('^.{' + io.prefix.length + '}(.*)$'); 65 | for (var i = 0; i < keys.length; ++i) { 66 | var key = keys[i], m = regexp.exec(key); 67 | if (m && options(m[1])) { 68 | io.cache.storage.remove(key); 69 | } 70 | } 71 | return; 72 | } 73 | if (options instanceof RegExp) { 74 | var keys = io.cache.storage.getKeys(), 75 | regexp = new RegExp('^.{' + io.prefix.length + '}(.*)$'); 76 | for (var i = 0; i < keys.length; ++i) { 77 | var key = keys[i], m = regexp.exec(key); 78 | if (m && options.test(m[1])) { 79 | io.cache.storage.remove(key); 80 | } 81 | } 82 | return; 83 | } 84 | options = io.processOptions(typeof options == "string" ? {url: options, method: "GET"} : options); 85 | var url = options.url; 86 | if (url && url.charAt(url.length - 1) == "*") { 87 | var prefix = url.slice(0, url.length - 1), pl = prefix.length, 88 | keys = io.cache.storage.getKeys(), 89 | regexp = new RegExp('^.{' + (io.prefix.length + 1) + '}\\w+\\-(.*)$'); 90 | for (var i = 0; i < keys.length; ++i) { 91 | var key = keys[i], m = regexp.exec(key); 92 | if (m && m[1].slice(0, pl) == prefix) { 93 | io.cache.storage.remove(key); 94 | } 95 | } 96 | return; 97 | } 98 | io.cache.storage.remove(io.makeKey(options)); 99 | } 100 | 101 | function clear() { io.cache.storage.clear(); } 102 | 103 | function makeStorage (type) { 104 | return { 105 | retrieve: function (key) { 106 | var data = window[type].getItem(key); 107 | return data === null ? void 0 : JSON.parse(data); 108 | }, 109 | store: function (key, data) { 110 | window[type].setItem(key, JSON.stringify(data)); 111 | }, 112 | remove: function (key) { 113 | window[type].removeItem(key); 114 | }, 115 | clear: function () { 116 | window[type].clear(); 117 | }, 118 | getKeys: function() { 119 | var storage = window[type], keys = [], prefix = io.prefix, pl = prefix.length; 120 | for (var i = 0, n = storage.length; i < n; ++i) { 121 | var key = storage.key(i); 122 | if (prefix == key.slice(0, pl)) { 123 | keys.push(key); 124 | } 125 | } 126 | return keys; 127 | } 128 | }; 129 | } 130 | 131 | 132 | // export 133 | 134 | io.cache = { 135 | saveByKey: saveByKey, 136 | save: save, 137 | remove: remove, 138 | clear: clear, 139 | makeStorage: makeStorage, 140 | storage: makeStorage('sessionStorage') 141 | }; 142 | return scaffold(io, 'cache', 50, cache); 143 | }); 144 | -------------------------------------------------------------------------------- /dist/cache.js: -------------------------------------------------------------------------------- 1 | (function(_,f){f(window.heya.io,window.heya.io.FauxXHR,window.heya.io.scaffold);}) 2 | (['./io', './FauxXHR', './scaffold'], function (io, FauxXHR, scaffold) { 3 | 'use strict'; 4 | 5 | // cache I/O requests 6 | // this is a browser-only module. 7 | 8 | function cache (options, prep, level) { 9 | var key = prep.key; 10 | 11 | if (options.wait || options.bust || 12 | io.track && io.track.deferred[key] || 13 | !io.cache.optIn(options)) { 14 | return null; 15 | } 16 | 17 | // retrieve data, if available 18 | var data = io.cache.storage.retrieve(key); 19 | if (typeof data !== 'undefined') { 20 | return io.Deferred.resolve(new io.Result(new FauxXHR(data), options, null)); 21 | } 22 | 23 | // pass the request, and cache the result 24 | var promise = io.request(options, prep, level - 1); 25 | promise.then(function (result) { saveByKey(key, result); }); 26 | return promise; 27 | } 28 | 29 | function saveByKey (key, result) { 30 | var xhr; 31 | if (result instanceof XMLHttpRequest || result instanceof FauxXHR) { 32 | xhr = result; 33 | } else if (result && (result.xhr instanceof XMLHttpRequest || result.xhr instanceof FauxXHR)) { 34 | xhr = result.xhr; 35 | } 36 | if (xhr) { 37 | if (xhr.status >= 200 && xhr.status < 300) { 38 | io.cache.storage.store(key, { 39 | status: xhr.status, 40 | statusText: xhr.statusText, 41 | responseType: xhr.responseType, 42 | responseText: xhr.responseText, 43 | headers: xhr.getAllResponseHeaders() 44 | }); 45 | } 46 | } else { 47 | io.cache.storage.store(key, { 48 | status: 200, 49 | statusText: 'OK', 50 | responseType: 'json', 51 | response: result, 52 | headers: 'Content-Type: application/json' 53 | }); 54 | } 55 | } 56 | 57 | function save (options, result) { 58 | options = io.processOptions(typeof options == 'string' ? {url: options, method: 'GET'} : options); 59 | io.cache.saveByKey(io.makeKey(options), result); 60 | } 61 | 62 | function remove(options) { 63 | if (typeof options == 'function') { 64 | var keys = io.cache.storage.getKeys(), 65 | regexp = new RegExp('^.{' + io.prefix.length + '}(.*)$'); 66 | for (var i = 0; i < keys.length; ++i) { 67 | var key = keys[i], m = regexp.exec(key); 68 | if (m && options(m[1])) { 69 | io.cache.storage.remove(key); 70 | } 71 | } 72 | return; 73 | } 74 | if (options instanceof RegExp) { 75 | var keys = io.cache.storage.getKeys(), 76 | regexp = new RegExp('^.{' + io.prefix.length + '}(.*)$'); 77 | for (var i = 0; i < keys.length; ++i) { 78 | var key = keys[i], m = regexp.exec(key); 79 | if (m && options.test(m[1])) { 80 | io.cache.storage.remove(key); 81 | } 82 | } 83 | return; 84 | } 85 | options = io.processOptions(typeof options == "string" ? {url: options, method: "GET"} : options); 86 | var url = options.url; 87 | if (url && url.charAt(url.length - 1) == "*") { 88 | var prefix = url.slice(0, url.length - 1), pl = prefix.length, 89 | keys = io.cache.storage.getKeys(), 90 | regexp = new RegExp('^.{' + (io.prefix.length + 1) + '}\\w+\\-(.*)$'); 91 | for (var i = 0; i < keys.length; ++i) { 92 | var key = keys[i], m = regexp.exec(key); 93 | if (m && m[1].slice(0, pl) == prefix) { 94 | io.cache.storage.remove(key); 95 | } 96 | } 97 | return; 98 | } 99 | io.cache.storage.remove(io.makeKey(options)); 100 | } 101 | 102 | function clear() { io.cache.storage.clear(); } 103 | 104 | function makeStorage (type) { 105 | return { 106 | retrieve: function (key) { 107 | var data = window[type].getItem(key); 108 | return data === null ? void 0 : JSON.parse(data); 109 | }, 110 | store: function (key, data) { 111 | window[type].setItem(key, JSON.stringify(data)); 112 | }, 113 | remove: function (key) { 114 | window[type].removeItem(key); 115 | }, 116 | clear: function () { 117 | window[type].clear(); 118 | }, 119 | getKeys: function() { 120 | var storage = window[type], keys = [], prefix = io.prefix, pl = prefix.length; 121 | for (var i = 0, n = storage.length; i < n; ++i) { 122 | var key = storage.key(i); 123 | if (prefix == key.slice(0, pl)) { 124 | keys.push(key); 125 | } 126 | } 127 | return keys; 128 | } 129 | }; 130 | } 131 | 132 | 133 | // export 134 | 135 | io.cache = { 136 | saveByKey: saveByKey, 137 | save: save, 138 | remove: remove, 139 | clear: clear, 140 | makeStorage: makeStorage, 141 | storage: makeStorage('sessionStorage') 142 | }; 143 | return scaffold(io, 'cache', 50, cache); 144 | }); 145 | -------------------------------------------------------------------------------- /tests/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var path = require('path'); 5 | var debug = require('debug')('heya-io:server'); 6 | var express = require('express'); 7 | var bodyParser = require('body-parser'); 8 | 9 | var bundler = require('heya-bundler'); 10 | 11 | // The APP 12 | 13 | var app = express(); 14 | 15 | app.use(bodyParser.raw({type: '*/*'})); 16 | 17 | var counter = 0; 18 | 19 | app.all('/api*', function (req, res) { 20 | if (req.query.status) { 21 | var status = parseInt(req.query.status, 10); 22 | if (isNaN(status) || status < 100 || status >= 600) { 23 | status = 200; 24 | } 25 | res.status(status); 26 | } 27 | switch (req.query.payloadType) { 28 | case 'txt': 29 | res.set('Content-Type', 'text/plain'); 30 | res.send('Hello, world!'); 31 | return; 32 | case 'xml': 33 | res.set('Content-Type', 'application/xml'); 34 | res.send('
Hello, world!
'); 35 | return; 36 | } 37 | var data = { 38 | method: req.method, 39 | protocol: req.protocol, 40 | hostname: req.hostname, 41 | url: req.url, 42 | originalUrl: req.originalUrl, 43 | headers: req.headers, 44 | body: req.body && req.body.length && req.body.toString() || null, 45 | query: req.query, 46 | now: Date.now(), 47 | counter: counter++ 48 | }; 49 | var timeout = 0; 50 | if (req.query.timeout) { 51 | var timeout = parseInt(req.query.timeout, 10); 52 | if (isNaN(timeout) || timeout < 0 || timeout > 60000) { 53 | timeout = 0; 54 | } 55 | } 56 | if (timeout) { 57 | setTimeout(function () { 58 | res.jsonp(data); 59 | }, timeout); 60 | } else { 61 | res.jsonp(data); 62 | } 63 | }); 64 | 65 | app.put('/bundle', bundler({ 66 | isUrlAcceptable: isUrlAcceptable, 67 | resolveUrl: resolveUrl 68 | })); 69 | 70 | function isUrlAcceptable (uri) { 71 | return typeof uri == 'string' && !/^\/\//.test(uri) && 72 | (uri.charAt(0) === '/' || /^http:\/\/localhost:3000\//.test(uri)); 73 | } 74 | 75 | function resolveUrl (uri) { 76 | return uri.charAt(0) === '/' ? 'http://localhost:3000' + uri : uri; 77 | } 78 | 79 | app.use(express.static(path.join(__dirname, '..'))); 80 | 81 | // catch 404 and forward to error handler 82 | app.use(function(req, res, next) { 83 | var err = new Error('Not Found'); 84 | err.status = 404; 85 | next(err); 86 | }); 87 | 88 | // error handlers 89 | 90 | app.use(function(err, req, res, next) { 91 | // for simplicity we don't use fancy HTML formatting opting for a plain text 92 | res.status(err.status || 500); 93 | res.set('Content-Type', 'text/plain'); 94 | res.send('Error (' + err.status + '): ' + err.message + '\n' + err.stack); 95 | debug('Error: ' + err.message + ' (' + err.status + ')'); 96 | }); 97 | 98 | // The SERVER 99 | 100 | /** 101 | * Get port from environment and store in Express. 102 | */ 103 | 104 | var host = process.env.HOST || 'localhost', 105 | port = normalizePort(process.env.PORT || '3000'); 106 | app.set('port', port); 107 | 108 | /** 109 | * Create HTTP server. 110 | */ 111 | 112 | var server = http.createServer(app); 113 | 114 | /** 115 | * Listen on provided port, on provided host, or all network interfaces. 116 | */ 117 | 118 | server.listen(port, host); 119 | server.on('error', onError); 120 | server.on('listening', onListening); 121 | 122 | /** 123 | * Normalize a port into a number, string, or false. 124 | */ 125 | 126 | function normalizePort(val) { 127 | var port = parseInt(val, 10); 128 | 129 | if (isNaN(port)) { 130 | // named pipe 131 | return val; 132 | } 133 | 134 | if (port >= 0) { 135 | // port number 136 | return port; 137 | } 138 | 139 | return false; 140 | } 141 | 142 | /** 143 | * Human-readable port description. 144 | */ 145 | 146 | function portToString (port) { 147 | return typeof port === 'string' ? 'pipe ' + port : 'port ' + port; 148 | } 149 | 150 | 151 | /** 152 | * Event listener for HTTP server "error" event. 153 | */ 154 | 155 | function onError(error) { 156 | if (error.syscall !== 'listen') { 157 | throw error; 158 | } 159 | 160 | var bind = portToString(port); 161 | 162 | // handle specific listen errors with friendly messages 163 | switch (error.code) { 164 | case 'EACCES': 165 | console.error('Error: ' + bind + ' requires elevated privileges'); 166 | process.exit(1); 167 | break; 168 | case 'EADDRINUSE': 169 | console.error('Error: ' + bind + ' is already in use'); 170 | process.exit(1); 171 | break; 172 | default: 173 | throw error; 174 | } 175 | } 176 | 177 | /** 178 | * Event listener for HTTP server "listening" event. 179 | */ 180 | 181 | function onListening() { 182 | //var addr = server.address(); 183 | var bind = portToString(port); 184 | debug('Listening on ' + (host || 'all network interfaces') + ' ' + bind); 185 | } 186 | -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | (function(_,f){f(window.heya.io,window.heya.io.FauxXHR,window.heya.io.scaffold);}) 2 | (['./track', './FauxXHR', './scaffold'], function (io, FauxXHR, scaffold) { 3 | 'use strict'; 4 | 5 | // bundle I/O requests 6 | 7 | function bundle (options, prep, level) { 8 | if (options.wait || !io.bundle.optIn(options)) { 9 | return null; 10 | } 11 | 12 | var waitTime = io.bundle.waitTime, 13 | isBundling = io.bundle.isStarted(); 14 | 15 | if (isBundling || waitTime > 0) { 16 | if (!isBundling && waitTime > 0) { 17 | setTimeout(io.bundle.commit, waitTime); 18 | io.bundle.start(); 19 | } 20 | io.bundle.pending[prep.key] = {options: options, prep: prep, level: level}; 21 | var deferred = io.track.deferred[prep.key]; 22 | return deferred.promise || deferred; 23 | } 24 | 25 | return null; 26 | } 27 | 28 | var delay = false; 29 | 30 | function start () { 31 | delay = true; 32 | } 33 | 34 | function isStarted () { 35 | return delay; 36 | } 37 | 38 | function commit () { 39 | var bundle = Object.keys(io.bundle.pending).map(function (key) { 40 | return io.bundle.pending[key]; 41 | }), 42 | bundleSize = Math.max(io.bundle.maxSize, 2); 43 | io.bundle.pending = {}; 44 | delay = false; 45 | if (bundle.length <= bundleSize) { 46 | // send a single bundle 47 | sendBundle(bundle); 48 | } else { 49 | // send several bundles 50 | for (var i = 0; i < bundle.length; i += bundleSize) { 51 | sendBundle(bundle.slice(i, i + bundleSize)); 52 | } 53 | } 54 | } 55 | 56 | function sendBundle (bundle) { 57 | if (bundle.length < Math.max(Math.min(io.bundle.minSize, io.bundle.maxSize), 1)) { 58 | // small bundle => send each item separately 59 | return bundle.forEach(sendRequest); 60 | } 61 | // send a bundle 62 | io({ 63 | url: io.bundle.url, 64 | method: 'PUT', 65 | bundle: false, 66 | data: bundle.map(function (item) { 67 | return flattenOptions(item.options); 68 | }) 69 | }); 70 | } 71 | 72 | function sendRequest (item) { 73 | var key = item.prep.key, deferred = io.track.deferred[key]; 74 | io.request(item.options, item.prep, item.level - 1).then( 75 | function (value) { deferred.resolve(value, true); }, 76 | function (value) { deferred.reject (value, true); } 77 | ); 78 | } 79 | 80 | function flattenOptions (options) { 81 | var newOptions = {}; 82 | for (var key in options) { 83 | newOptions[key] = options[key]; 84 | } 85 | return newOptions; 86 | } 87 | 88 | 89 | // processing bundles 90 | 91 | function unbundle (data) { 92 | var bundle = io.bundle.detect(data); 93 | if (bundle) { 94 | bundle.forEach(function (result) { 95 | var key = io.makeKey(result.options), 96 | xhr = new FauxXHR(result.response), 97 | deferred = io.track.deferred[key]; 98 | if (deferred) { 99 | deferred.resolve(new io.Result(xhr, result.options, null), true); 100 | } else { 101 | io.cache && io.cache.saveByKey(key, xhr); 102 | } 103 | }); 104 | } 105 | } 106 | 107 | function detect (data) { 108 | return data && typeof data == 'object' && data.bundle === 'bundle' && 109 | data.results instanceof Array ? data.results : null; 110 | } 111 | 112 | function attachProcessSuccess (previousProcessSuccess) { 113 | return function (result) { 114 | var data = previousProcessSuccess(result); 115 | if (io.bundle.isActive) { 116 | io.bundle.unbundle(data); 117 | } 118 | return data; 119 | }; 120 | } 121 | 122 | 123 | // convenience functions 124 | 125 | function fly (bundle) { 126 | bundle.forEach(io.track.fly); 127 | } 128 | 129 | function submit (bundle) { 130 | if (io.bundle.isStarted()) { 131 | bundle.forEach(io); 132 | } else { 133 | io.bundle.start(); 134 | bundle.forEach(io); 135 | io.bundle.commit(); 136 | } 137 | } 138 | 139 | function submitWithRelated (options, bundle) { 140 | var promise; 141 | if (io.bundle.isStarted()) { 142 | bundle.forEach(io); 143 | promise = io(options); 144 | } else { 145 | io.bundle.start(); 146 | bundle.forEach(io); 147 | promise = io(options); 148 | io.bundle.commit(); 149 | } 150 | return promise; 151 | } 152 | 153 | 154 | // export 155 | 156 | function attach () { 157 | io.track.attach(); 158 | io.processSuccess = attachProcessSuccess(io.processSuccess); 159 | io.attach({ 160 | name: 'bundle', 161 | priority: 10, 162 | callback: bundle 163 | }); 164 | io.bundle.isActive = true; 165 | } 166 | 167 | io.bundle = { 168 | attach: attach, 169 | 170 | // start/commit bundles 171 | start: start, 172 | commit: commit, 173 | isStarted: isStarted, 174 | waitTime: 20, // in ms 175 | 176 | // server-side bundle settings 177 | url: '/bundle', 178 | minSize: 2, 179 | maxSize: 20, 180 | detect: detect, 181 | 182 | // advanced utilities 183 | unbundle: unbundle, 184 | submit: submit, 185 | submitWithRelated: submitWithRelated, 186 | fly: fly, 187 | 188 | pending: {} 189 | }; 190 | return scaffold(io, 'bundle', 10, bundle); 191 | }); 192 | -------------------------------------------------------------------------------- /tests/test-io-as-bundle.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/bundle', 'heya-async/Deferred', 'heya-io/track', 'heya-io/cache'], function (module, unit, io, Deferred) { 2 | 'use strict'; 3 | 4 | unit.add(module, [ 5 | function test_setup () { 6 | io.Deferred = Deferred; 7 | io.bundle.minSize = io.bundle.maxSize = 1; 8 | io.cache && io.cache.attach(); 9 | io.bundle.attach(); 10 | }, 11 | function test_simple_io (t) { 12 | var x = t.startAsync(); 13 | io('http://localhost:3000/api').then(function (data) { 14 | eval(t.TEST('data.method === "GET"')); 15 | eval(t.TEST('data.body === null')); 16 | x.done(); 17 | }); 18 | }, 19 | function test_io_get (t) { 20 | var x = t.startAsync(); 21 | io.get('http://localhost:3000/api').then(function (data) { 22 | eval(t.TEST('data.method === "GET"')); 23 | eval(t.TEST('data.body === null')); 24 | x.done(); 25 | }); 26 | }, 27 | function test_io_put (t) { 28 | var x = t.startAsync(); 29 | io.put('http://localhost:3000/api', {a: 1}).then(function (data) { 30 | eval(t.TEST('data.method === "PUT"')); 31 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 32 | x.done(); 33 | }); 34 | }, 35 | function test_io_post (t) { 36 | var x = t.startAsync(); 37 | io.post('http://localhost:3000/api', {a: 1}).then(function (data) { 38 | eval(t.TEST('data.method === "POST"')); 39 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 40 | x.done(); 41 | }); 42 | }, 43 | function test_io_patch (t) { 44 | var x = t.startAsync(); 45 | io.patch('http://localhost:3000/api', {a: 1}).then(function (data) { 46 | eval(t.TEST('data.method === "PATCH"')); 47 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 48 | x.done(); 49 | }); 50 | }, 51 | function test_io_remove (t) { 52 | var x = t.startAsync(); 53 | io.remove('http://localhost:3000/api').then(function (data) { 54 | eval(t.TEST('data.method === "DELETE"')); 55 | eval(t.TEST('data.body === null')); 56 | x.done(); 57 | }); 58 | }, 59 | function test_io_get_query (t) { 60 | var x = t.startAsync(); 61 | io.get('http://localhost:3000/api', {a: 1}).then(function (data) { 62 | eval(t.TEST('data.method === "GET"')); 63 | eval(t.TEST('t.unify(data.query, {a: "1"})')); 64 | x.done(); 65 | }); 66 | }, 67 | function test_io_get_error (t) { 68 | var x = t.startAsync(); 69 | io.get('http://localhost:3000/api', {status: 500}).then(function (data) { 70 | t.test(false); // we should not be here 71 | x.done(); 72 | }).catch(function (data) { 73 | eval(t.TEST('data.xhr.status === 500')); 74 | x.done(); 75 | }); 76 | }, 77 | function test_io_get_txt (t) { 78 | var x = t.startAsync(); 79 | io.get('http://localhost:3000/api', {payloadType: 'txt'}).then(function (data) { 80 | eval(t.TEST('typeof data == "string"')); 81 | eval(t.TEST('data == "Hello, world!"')); 82 | x.done(); 83 | }); 84 | }, 85 | function test_io_get_xml (t) { 86 | var x = t.startAsync(); 87 | io.get('http://localhost:3000/api', {payloadType: 'xml'}).then(function (data) { 88 | eval(t.TEST('typeof data == "object"')); 89 | eval(t.TEST('data.nodeName == "#document"')); 90 | eval(t.TEST('data.nodeType == 9')); 91 | x.done(); 92 | }); 93 | }, 94 | function test_io_get_xml_as_text_mime (t) { 95 | var x = t.startAsync(); 96 | io.get({ 97 | url: 'http://localhost:3000/api', 98 | mime: 'text/plain', 99 | cache: false 100 | }, {payloadType: 'xml'}).then(function (data) { 101 | eval(t.TEST('typeof data == "string"')); 102 | eval(t.TEST('data == "
Hello, world!
"')); 103 | x.done(); 104 | }); 105 | }, 106 | function test_io_get_xml_as_text (t) { 107 | var x = t.startAsync(); 108 | io.get({ 109 | url: 'http://localhost:3000/api', 110 | responseType: 'text', 111 | cache: false 112 | }, {payloadType: 'xml'}).then(function (data) { 113 | eval(t.TEST('typeof data == "string"')); 114 | eval(t.TEST('data == "
Hello, world!
"')); 115 | x.done(); 116 | }); 117 | }, 118 | function test_io_get_xml_as_blob (t) { 119 | if (typeof Blob == 'undefined') return; 120 | var x = t.startAsync(); 121 | io.get({ 122 | url: 'http://localhost:3000/api', 123 | responseType: 'blob', 124 | cache: false 125 | }, {payloadType: 'xml'}).then(function (data) { 126 | eval(t.TEST('data instanceof Blob')); 127 | x.done(); 128 | }); 129 | }, 130 | function test_io_get_xml_as_array_buffer (t) { 131 | if (typeof ArrayBuffer == 'undefined') return; 132 | var x = t.startAsync(); 133 | io.get({ 134 | url: 'http://localhost:3000/api', 135 | responseType: 'arraybuffer', 136 | cache: false 137 | }, {payloadType: 'xml'}).then(function (data) { 138 | eval(t.TEST('data instanceof ArrayBuffer')); 139 | x.done(); 140 | }); 141 | }, 142 | function test_teardown () { 143 | io.bundle.detach(); 144 | io.cache.detach(); 145 | io.track.detach(); 146 | io.cache.storage.clear(); 147 | io.Deferred = io.FauxDeferred; 148 | } 149 | ]); 150 | 151 | return {}; 152 | }); 153 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | (['./track', './FauxXHR', './scaffold'], function (io, FauxXHR, scaffold) { 3 | 'use strict'; 4 | 5 | // bundle I/O requests 6 | 7 | function bundle (options, prep, level) { 8 | if (options.wait || !io.bundle.optIn(options)) { 9 | return null; 10 | } 11 | 12 | var waitTime = io.bundle.waitTime, 13 | isBundling = io.bundle.isStarted(); 14 | 15 | if (isBundling || waitTime > 0) { 16 | if (!isBundling && waitTime > 0) { 17 | setTimeout(io.bundle.commit, waitTime); 18 | io.bundle.start(); 19 | } 20 | io.bundle.pending[prep.key] = {options: options, prep: prep, level: level}; 21 | var deferred = io.track.deferred[prep.key]; 22 | return deferred.promise || deferred; 23 | } 24 | 25 | return null; 26 | } 27 | 28 | var delay = false; 29 | 30 | function start () { 31 | delay = true; 32 | } 33 | 34 | function isStarted () { 35 | return delay; 36 | } 37 | 38 | function commit () { 39 | var bundle = Object.keys(io.bundle.pending).map(function (key) { 40 | return io.bundle.pending[key]; 41 | }), 42 | bundleSize = Math.max(io.bundle.maxSize, 2); 43 | io.bundle.pending = {}; 44 | delay = false; 45 | if (bundle.length <= bundleSize) { 46 | // send a single bundle 47 | sendBundle(bundle); 48 | } else { 49 | // send several bundles 50 | for (var i = 0; i < bundle.length; i += bundleSize) { 51 | sendBundle(bundle.slice(i, i + bundleSize)); 52 | } 53 | } 54 | } 55 | 56 | function sendBundle (bundle) { 57 | if (bundle.length < Math.max(Math.min(io.bundle.minSize, io.bundle.maxSize), 1)) { 58 | // small bundle => send each item separately 59 | return bundle.forEach(sendRequest); 60 | } 61 | // send a bundle 62 | io({ 63 | url: io.bundle.url, 64 | method: 'PUT', 65 | bundle: false, 66 | data: bundle.map(function (item) { 67 | return flattenOptions(item.options); 68 | }) 69 | }); 70 | } 71 | 72 | function sendRequest (item) { 73 | var key = item.prep.key, deferred = io.track.deferred[key]; 74 | io.request(item.options, item.prep, item.level - 1).then( 75 | function (value) { deferred.resolve(value, true); }, 76 | function (value) { deferred.reject (value, true); } 77 | ); 78 | } 79 | 80 | function flattenOptions (options) { 81 | var newOptions = {}; 82 | for (var key in options) { 83 | newOptions[key] = options[key]; 84 | } 85 | return newOptions; 86 | } 87 | 88 | 89 | // processing bundles 90 | 91 | function unbundle (data) { 92 | var bundle = io.bundle.detect(data); 93 | if (bundle) { 94 | bundle.forEach(function (result) { 95 | var key = io.makeKey(result.options), 96 | xhr = new FauxXHR(result.response), 97 | deferred = io.track.deferred[key]; 98 | if (deferred) { 99 | deferred.resolve(new io.Result(xhr, result.options, null), true); 100 | } else { 101 | io.cache && io.cache.saveByKey(key, xhr); 102 | } 103 | }); 104 | } 105 | } 106 | 107 | function detect (data) { 108 | return data && typeof data == 'object' && data.bundle === 'bundle' && 109 | data.results instanceof Array ? data.results : null; 110 | } 111 | 112 | function attachProcessSuccess (previousProcessSuccess) { 113 | return function (result) { 114 | var data = previousProcessSuccess(result); 115 | if (io.bundle.isActive) { 116 | io.bundle.unbundle(data); 117 | } 118 | return data; 119 | }; 120 | } 121 | 122 | 123 | // convenience functions 124 | 125 | function fly (bundle) { 126 | bundle.forEach(io.track.fly); 127 | } 128 | 129 | function submit (bundle) { 130 | if (io.bundle.isStarted()) { 131 | bundle.forEach(io); 132 | } else { 133 | io.bundle.start(); 134 | bundle.forEach(io); 135 | io.bundle.commit(); 136 | } 137 | } 138 | 139 | function submitWithRelated (options, bundle) { 140 | var promise; 141 | if (io.bundle.isStarted()) { 142 | bundle.forEach(io); 143 | promise = io(options); 144 | } else { 145 | io.bundle.start(); 146 | bundle.forEach(io); 147 | promise = io(options); 148 | io.bundle.commit(); 149 | } 150 | return promise; 151 | } 152 | 153 | 154 | // export 155 | 156 | function attach () { 157 | io.track.attach(); 158 | io.processSuccess = attachProcessSuccess(io.processSuccess); 159 | io.attach({ 160 | name: 'bundle', 161 | priority: 10, 162 | callback: bundle 163 | }); 164 | io.bundle.isActive = true; 165 | } 166 | 167 | io.bundle = { 168 | attach: attach, 169 | 170 | // start/commit bundles 171 | start: start, 172 | commit: commit, 173 | isStarted: isStarted, 174 | waitTime: 20, // in ms 175 | 176 | // server-side bundle settings 177 | url: '/bundle', 178 | minSize: 2, 179 | maxSize: 20, 180 | detect: detect, 181 | 182 | // advanced utilities 183 | unbundle: unbundle, 184 | submit: submit, 185 | submitWithRelated: submitWithRelated, 186 | fly: fly, 187 | 188 | pending: {} 189 | }; 190 | return scaffold(io, 'bundle', 10, bundle); 191 | }); 192 | -------------------------------------------------------------------------------- /tests/test-fetch.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/io', 'heya-io/fetch'], function (module, unit, io) { 2 | 'use strict'; 3 | 4 | 5 | if (typeof fetch !== 'function') return {}; 6 | 7 | var isXml = /^application\/xml\b/, 8 | isOctetStream = /^application\/octet-stream\b/, 9 | isMultiPart = /^multipart\/form-data\b/; 10 | 11 | unit.add(module, [ 12 | function test_setup () { 13 | io.fetch.attach(); 14 | }, 15 | function test_exist (t) { 16 | eval(t.TEST('typeof io == "function"')); 17 | eval(t.TEST('typeof io.get == "function"')); 18 | eval(t.TEST('typeof io.put == "function"')); 19 | eval(t.TEST('typeof io.post == "function"')); 20 | eval(t.TEST('typeof io.patch == "function"')); 21 | eval(t.TEST('typeof io.remove == "function"')); 22 | eval(t.TEST('typeof io["delete"] == "function"')); 23 | }, 24 | function test_simple_io (t) { 25 | var x = t.startAsync(); 26 | io('http://localhost:3000/api').then(function (data) { 27 | eval(t.TEST('data.method === "GET"')); 28 | eval(t.TEST('data.body === null')); 29 | x.done(); 30 | }); 31 | }, 32 | function test_io_get (t) { 33 | var x = t.startAsync(); 34 | io.get('http://localhost:3000/api').then(function (data) { 35 | eval(t.TEST('data.method === "GET"')); 36 | eval(t.TEST('data.body === null')); 37 | x.done(); 38 | }); 39 | }, 40 | function test_io_put (t) { 41 | var x = t.startAsync(); 42 | io.put('http://localhost:3000/api', {a: 1}).then(function (data) { 43 | eval(t.TEST('data.method === "PUT"')); 44 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 45 | x.done(); 46 | }); 47 | }, 48 | function test_io_post (t) { 49 | var x = t.startAsync(); 50 | io.post('http://localhost:3000/api', {a: 1}).then(function (data) { 51 | eval(t.TEST('data.method === "POST"')); 52 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 53 | x.done(); 54 | }); 55 | }, 56 | function test_io_patch (t) { 57 | var x = t.startAsync(); 58 | io.patch('http://localhost:3000/api', {a: 1}).then(function (data) { 59 | eval(t.TEST('data.method === "PATCH"')); 60 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 61 | x.done(); 62 | }); 63 | }, 64 | function test_io_remove (t) { 65 | var x = t.startAsync(); 66 | io.remove('http://localhost:3000/api').then(function (data) { 67 | eval(t.TEST('data.method === "DELETE"')); 68 | eval(t.TEST('data.body === null')); 69 | x.done(); 70 | }); 71 | }, 72 | function test_io_get_query (t) { 73 | var x = t.startAsync(); 74 | io.get('http://localhost:3000/api', {a: 1}).then(function (data) { 75 | eval(t.TEST('data.method === "GET"')); 76 | eval(t.TEST('t.unify(data.query, {a: "1"})')); 77 | x.done(); 78 | }); 79 | }, 80 | function test_io_get_error (t) { 81 | var x = t.startAsync(); 82 | io.get('http://localhost:3000/api', {status: 500}).then(function (data) { 83 | t.test(false); // we should not be here 84 | x.done(); 85 | }).catch(function (data) { 86 | eval(t.TEST('data.xhr.status === 500')); 87 | x.done(); 88 | }); 89 | }, 90 | function test_io_get_txt (t) { 91 | var x = t.startAsync(); 92 | io.get('http://localhost:3000/api', {payloadType: 'txt'}).then(function (data) { 93 | eval(t.TEST('typeof data == "string"')); 94 | eval(t.TEST('data == "Hello, world!"')); 95 | x.done(); 96 | }); 97 | }, 98 | function test_io_get_xml (t) { 99 | var x = t.startAsync(); 100 | io.get('http://localhost:3000/api', {payloadType: 'xml'}).then(function (data) { 101 | eval(t.TEST('typeof data == "object"')); 102 | eval(t.TEST('data.nodeName == "#document"')); 103 | eval(t.TEST('data.nodeType == 9')); 104 | return io.post('http://localhost:3000/api', data); 105 | }).then(function (data) { 106 | eval(t.TEST('isXml.test(data.headers["content-type"])')); 107 | x.done(); 108 | }); 109 | }, 110 | function test_io_get_xml_as_text_mime (t) { 111 | var x = t.startAsync(); 112 | io.get({ 113 | url: 'http://localhost:3000/api', 114 | mime: 'text/plain' 115 | }, {payloadType: 'xml'}).then(function (data) { 116 | eval(t.TEST('typeof data == "string"')); 117 | eval(t.TEST('data == "
Hello, world!
"')); 118 | x.done(); 119 | }); 120 | }, 121 | function test_io_get_xml_as_text (t) { 122 | var x = t.startAsync(); 123 | io.get({ 124 | url: 'http://localhost:3000/api', 125 | responseType: 'text' 126 | }, {payloadType: 'xml'}).then(function (data) { 127 | eval(t.TEST('typeof data == "string"')); 128 | eval(t.TEST('data == "
Hello, world!
"')); 129 | x.done(); 130 | }); 131 | }, 132 | function test_io_get_xml_as_blob (t) { 133 | if (typeof Blob == 'undefined') return; 134 | var x = t.startAsync(); 135 | io.get({ 136 | url: 'http://localhost:3000/api', 137 | responseType: 'blob' 138 | }, {payloadType: 'xml'}).then(function (data) { 139 | eval(t.TEST('data instanceof Blob')); 140 | return io.post('http://localhost:3000/api', data); 141 | }).then(function (data) { 142 | eval(t.TEST('isXml.test(data.headers["content-type"])')); 143 | x.done(); 144 | }); 145 | }, 146 | function test_io_get_xml_as_array_buffer (t) { 147 | if (typeof ArrayBuffer == 'undefined') return; 148 | var x = t.startAsync(); 149 | io.get({ 150 | url: 'http://localhost:3000/api', 151 | responseType: 'arraybuffer' 152 | }, {payloadType: 'xml'}).then(function (data) { 153 | eval(t.TEST('data instanceof ArrayBuffer')); 154 | return io.post('http://localhost:3000/api', data); 155 | }).then(function (data) { 156 | eval(t.TEST('isOctetStream.test(data.headers["content-type"])')); 157 | x.done(); 158 | }); 159 | }, 160 | function test_io_post_formdata (t) { 161 | if (typeof FormData == 'undefined') return; 162 | var x = t.startAsync(); 163 | var div = document.createElement('div'); 164 | div.innerHTML = '
'; 165 | var data = new FormData(div.firstChild); 166 | data.append('user', 'heh!'); 167 | io.post('http://localhost:3000/api', data).then(function (data) { 168 | eval(t.TEST('isMultiPart.test(data.headers["content-type"])')); 169 | x.done(); 170 | }); 171 | }, 172 | function test_teardown () { 173 | io.fetch.detach(); 174 | } 175 | ]); 176 | 177 | return {}; 178 | }); 179 | -------------------------------------------------------------------------------- /tests/test-promise.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/io'], function (module, unit, io) { 2 | 'use strict'; 3 | 4 | if (typeof Promise == 'undefined') return; 5 | 6 | var isXml = /^application\/xml\b/, 7 | isJson = /^application\/json\b/, 8 | isOctetStream = /^application\/octet-stream\b/, 9 | isMultiPart = /^multipart\/form-data\b/; 10 | 11 | unit.add(module, [ 12 | function test_exist (t) { 13 | eval(t.TEST('typeof io == "function"')); 14 | eval(t.TEST('typeof io.get == "function"')); 15 | eval(t.TEST('typeof io.put == "function"')); 16 | eval(t.TEST('typeof io.post == "function"')); 17 | eval(t.TEST('typeof io.patch == "function"')); 18 | eval(t.TEST('typeof io.remove == "function"')); 19 | eval(t.TEST('typeof io["delete"] == "function"')); 20 | }, 21 | function test_simple_io (t) { 22 | var x = t.startAsync(); 23 | io('http://localhost:3000/api').then(function (data) { 24 | eval(t.TEST('data.method === "GET"')); 25 | eval(t.TEST('data.body === null')); 26 | x.done(); 27 | }); 28 | }, 29 | function test_io_get (t) { 30 | var x = t.startAsync(); 31 | io.get('http://localhost:3000/api').then(function (data) { 32 | eval(t.TEST('data.method === "GET"')); 33 | eval(t.TEST('data.body === null')); 34 | x.done(); 35 | }); 36 | }, 37 | function test_io_put (t) { 38 | var x = t.startAsync(); 39 | io.put('http://localhost:3000/api', {a: 1}).then(function (data) { 40 | eval(t.TEST('data.method === "PUT"')); 41 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 42 | x.done(); 43 | }); 44 | }, 45 | function test_io_post (t) { 46 | var x = t.startAsync(); 47 | io.post('http://localhost:3000/api', {a: 1}).then(function (data) { 48 | eval(t.TEST('data.method === "POST"')); 49 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 50 | x.done(); 51 | }); 52 | }, 53 | function test_io_patch (t) { 54 | var x = t.startAsync(); 55 | io.patch('http://localhost:3000/api', {a: 1}).then(function (data) { 56 | eval(t.TEST('data.method === "PATCH"')); 57 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 58 | x.done(); 59 | }); 60 | }, 61 | function test_io_remove (t) { 62 | var x = t.startAsync(); 63 | io.remove('http://localhost:3000/api').then(function (data) { 64 | eval(t.TEST('data.method === "DELETE"')); 65 | eval(t.TEST('data.body === null')); 66 | x.done(); 67 | }); 68 | }, 69 | function test_io_get_query (t) { 70 | var x = t.startAsync(); 71 | io.get('http://localhost:3000/api', {a: 1}).then(function (data) { 72 | eval(t.TEST('data.method === "GET"')); 73 | eval(t.TEST('t.unify(data.query, {a: "1"})')); 74 | x.done(); 75 | }); 76 | }, 77 | function test_io_get_error (t) { 78 | var x = t.startAsync(); 79 | io.get('http://localhost:3000/api', {status: 500}).then(function (data) { 80 | t.test(false); // we should not be here 81 | x.done(); 82 | }).catch(function (data) { 83 | eval(t.TEST('data.xhr.status === 500')); 84 | x.done(); 85 | }); 86 | }, 87 | function test_io_get_txt (t) { 88 | var x = t.startAsync(); 89 | io.get('http://localhost:3000/api', {payloadType: 'txt'}).then(function (data) { 90 | eval(t.TEST('typeof data == "string"')); 91 | eval(t.TEST('data == "Hello, world!"')); 92 | x.done(); 93 | }); 94 | }, 95 | function test_io_get_xml (t) { 96 | var x = t.startAsync(); 97 | io.get('http://localhost:3000/api', {payloadType: 'xml'}).then(function (data) { 98 | eval(t.TEST('typeof data == "object"')); 99 | eval(t.TEST('data.nodeName == "#document"')); 100 | eval(t.TEST('data.nodeType == 9')); 101 | return io.post('http://localhost:3000/api', data); 102 | }).then(function (data) { 103 | eval(t.TEST('isXml.test(data.headers["content-type"])')); 104 | x.done(); 105 | }); 106 | }, 107 | function test_io_get_xml_as_text_mime (t) { 108 | var x = t.startAsync(); 109 | io.get({ 110 | url: 'http://localhost:3000/api', 111 | mime: 'text/plain' 112 | }, {payloadType: 'xml'}).then(function (data) { 113 | eval(t.TEST('typeof data == "string"')); 114 | eval(t.TEST('data == "
Hello, world!
"')); 115 | x.done(); 116 | }); 117 | }, 118 | function test_io_get_xml_as_text (t) { 119 | var x = t.startAsync(); 120 | io.get({ 121 | url: 'http://localhost:3000/api', 122 | responseType: 'text' 123 | }, {payloadType: 'xml'}).then(function (data) { 124 | eval(t.TEST('typeof data == "string"')); 125 | eval(t.TEST('data == "
Hello, world!
"')); 126 | x.done(); 127 | }); 128 | }, 129 | function test_io_get_xml_as_blob (t) { 130 | if (typeof Blob == 'undefined') return; 131 | var x = t.startAsync(); 132 | io.get({ 133 | url: 'http://localhost:3000/api', 134 | responseType: 'blob' 135 | }, {payloadType: 'xml'}).then(function (data) { 136 | eval(t.TEST('data instanceof Blob')); 137 | return io.post('http://localhost:3000/api', data); 138 | }).then(function (data) { 139 | eval(t.TEST('isXml.test(data.headers["content-type"])')); 140 | x.done(); 141 | }); 142 | }, 143 | function test_io_get_xml_as_array_buffer (t) { 144 | if (typeof ArrayBuffer == 'undefined' || typeof DataView == 'undefined') return; 145 | var x = t.startAsync(); 146 | io.get({ 147 | url: 'http://localhost:3000/api', 148 | responseType: 'arraybuffer' 149 | }, {payloadType: 'xml'}).then(function (data) { 150 | eval(t.TEST('data instanceof ArrayBuffer')); 151 | return io.post('http://localhost:3000/api', new DataView(data)); 152 | }).then(function (data) { 153 | eval(t.TEST('isOctetStream.test(data.headers["content-type"])')); 154 | x.done(); 155 | }); 156 | }, 157 | function test_io_post_formdata (t) { 158 | if (typeof FormData == 'undefined') return; 159 | var x = t.startAsync(); 160 | var div = document.createElement('div'); 161 | div.innerHTML = '
'; 162 | var data = new FormData(div.firstChild); 163 | data.append('user', 'heh!'); 164 | io.post('http://localhost:3000/api', data).then(function (data) { 165 | eval(t.TEST('isMultiPart.test(data.headers["content-type"])')); 166 | x.done(); 167 | }); 168 | }, 169 | function test_io_post_int8array (t) { 170 | if (typeof Int8Array == 'undefined') return; 171 | var x = t.startAsync(); 172 | var data = new Int8Array(8); 173 | data[0] = 32; 174 | data[1] = 42; 175 | io.post('http://localhost:3000/api', data).then(function (data) { 176 | eval(t.TEST('isOctetStream.test(data.headers["content-type"])')); 177 | x.done(); 178 | }); 179 | }, 180 | function test_io_post_ignore (t) { 181 | var x = t.startAsync(); 182 | io.post({ 183 | url: 'http://localhost:3000/api', 184 | headers: { 185 | 'Content-Type': 'application/json' 186 | } 187 | }, 'abc').then(function (data) { 188 | eval(t.TEST('isJson.test(data.headers["content-type"])')); 189 | eval(t.TEST('data.body === \'"abc"\'')); 190 | return io.post({ 191 | url: 'http://localhost:3000/api', 192 | headers: { 193 | 'Content-Type': 'application/json' 194 | } 195 | }, new io.Ignore('abc')); 196 | }).then(function (data) { 197 | eval(t.TEST('isJson.test(data.headers["content-type"])')); 198 | eval(t.TEST('data.body === "abc"')); 199 | x.done(); 200 | }); 201 | }, 202 | function test_io_custom_mime_processor (t) { 203 | var x = t.startAsync(); 204 | var restoreOriginal = io.mimeProcessors; 205 | io.mimeProcessors = []; 206 | io.mimeProcessors.push(function(contentType){ 207 | return contentType === 'text/plain; charset=utf-8'; 208 | }); 209 | io.mimeProcessors.push(function(xhr, contentType){ 210 | eval(t.TEST('contentType === "text/plain; charset=utf-8"')); 211 | eval(t.TEST('xhr.responseText == "Hello, world!"')); 212 | return 'Custom Parser Result'; 213 | }); 214 | io.get('http://localhost:3000/api', {payloadType: 'txt'}).then(function (data) { 215 | eval(t.TEST('typeof data == "string"')); 216 | eval(t.TEST('data === "Custom Parser Result"')); 217 | io.mimeProcessors = restoreOriginal; 218 | x.done(); 219 | }); 220 | }, 221 | function test_io_get_as_xhr (t) { 222 | var x = t.startAsync(); 223 | io.get({ 224 | url: 'http://localhost:3000/api', 225 | returnXHR: true 226 | }).then(function (xhr) { 227 | var data = io.getData(xhr), headers = io.getHeaders(xhr); 228 | eval(t.TEST('xhr.status == 200')); 229 | eval(t.TEST('typeof data == "object"')); 230 | eval(t.TEST('isJson.test(headers["content-type"])')); 231 | x.done(); 232 | }); 233 | } 234 | ]); 235 | 236 | return {}; 237 | }); 238 | -------------------------------------------------------------------------------- /tests/test-mock.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/mock', 'heya-async/Deferred', 'heya-async/timeout'], function (module, unit, io, Deferred, timeout) { 2 | 'use strict'; 3 | 4 | unit.add(module, [ 5 | function test_setup () { 6 | io.Deferred = Deferred; 7 | io.mock.attach(); 8 | }, 9 | function test_exist (t) { 10 | eval(t.TEST('typeof io.mock == "function"')); 11 | }, 12 | { 13 | test: function test_exact (t) { 14 | var x = t.startAsync(); 15 | io.mock('http://localhost:3000/a', function (options) { 16 | var verb = options.method || 'GET'; 17 | t.info('mock callback: ' + verb); 18 | return verb; 19 | }); 20 | io.get('http://localhost:3000/a').then(function (value) { 21 | t.info('got ' + value); 22 | return io.patch('http://localhost:3000/a', null); 23 | }).then(function (value) { 24 | t.info('got ' + value); 25 | io.mock('http://localhost:3000/a'); 26 | return io.get('http://localhost:3000/a'); 27 | }).then(function () { 28 | t.info('shouldn\'t be here!'); 29 | x.done(); 30 | }, function () { 31 | t.info('error'); 32 | x.done(); 33 | }); 34 | }, 35 | logs: [ 36 | 'mock callback: GET', 37 | 'got GET', 38 | 'mock callback: PATCH', 39 | 'got PATCH', 40 | 'error' 41 | ] 42 | }, 43 | { 44 | test: function test_prefix (t) { 45 | var x = t.startAsync(); 46 | io.mock('http://localhost:3000/aa*', function (options) { 47 | var value = 'aa' + (options.method || 'GET'); 48 | t.info('mock callback: ' + value); 49 | return value; 50 | }); 51 | io.mock('http://localhost:3000/a*', function (options) { 52 | var value = 'a' + (options.method || 'GET'); 53 | t.info('mock callback: ' + value); 54 | return value; 55 | }); 56 | io.mock('http://localhost:3000/aaa*', function (options) { 57 | var value = 'aaa' + (options.method || 'GET'); 58 | t.info('mock callback: ' + value); 59 | return value; 60 | }); 61 | io.get('http://localhost:3000/a/x').then(function (value) { 62 | t.info('got ' + value); 63 | return io.patch('http://localhost:3000/aa', null); 64 | }).then(function (value) { 65 | t.info('got ' + value); 66 | return io.put('http://localhost:3000/ab'); 67 | }).then(function (value) { 68 | t.info('got ' + value); 69 | return io.post('http://localhost:3000/aaa', {z: 1}); 70 | }).then(function (value) { 71 | t.info('got ' + value); 72 | io.mock('http://localhost:3000/a*'); 73 | io.mock('http://localhost:3000/aa*'); 74 | io.mock('http://localhost:3000/aaa*'); 75 | return io.get('http://localhost:3000/aa'); 76 | }).then(function () { 77 | t.info('shouldn\'t be here!'); 78 | x.done(); 79 | }, function () { 80 | t.info('error'); 81 | x.done(); 82 | }); 83 | }, 84 | logs: [ 85 | 'mock callback: aGET', 86 | 'got aGET', 87 | 'mock callback: aaPATCH', 88 | 'got aaPATCH', 89 | 'mock callback: aPUT', 90 | 'got aPUT', 91 | 'mock callback: aaaPOST', 92 | 'got aaaPOST', 93 | 'error' 94 | ] 95 | }, 96 | { 97 | test: function test_regexp (t) { 98 | var x = t.startAsync(); 99 | io.mock(/^https?:\/\/localhost:3000\/a/, function (options) { 100 | const m = /^https?:\/\/[^\/]+\/(a+)/.exec(options.url); 101 | var value = m[1] + (options.method || 'GET'); 102 | t.info('mock callback: ' + value); 103 | return value; 104 | }); 105 | io.get('http://localhost:3000/a/x').then(function (value) { 106 | t.info('got ' + value); 107 | return io.patch('http://localhost:3000/aa', null); 108 | }).then(function (value) { 109 | t.info('got ' + value); 110 | return io.put('http://localhost:3000/ab'); 111 | }).then(function (value) { 112 | t.info('got ' + value); 113 | return io.post('http://localhost:3000/aaa', {z: 1}); 114 | }).then(function (value) { 115 | t.info('got ' + value); 116 | io.mock(/^https?:\/\/localhost:3000\/a/); 117 | return io.get('http://localhost:3000/aa'); 118 | }).then(function () { 119 | t.info('shouldn\'t be here!'); 120 | x.done(); 121 | }, function () { 122 | t.info('error'); 123 | x.done(); 124 | }); 125 | }, 126 | logs: [ 127 | 'mock callback: aGET', 128 | 'got aGET', 129 | 'mock callback: aaPATCH', 130 | 'got aaPATCH', 131 | 'mock callback: aPUT', 132 | 'got aPUT', 133 | 'mock callback: aaaPOST', 134 | 'got aaaPOST', 135 | 'error' 136 | ] 137 | }, 138 | { 139 | test: function test_match (t) { 140 | var x = t.startAsync(); 141 | 142 | function matcher (options) { 143 | return /^https?:\/\/localhost:3000\/a/.test(options.url); 144 | } 145 | 146 | io.mock(matcher, function (options) { 147 | const m = /^https?:\/\/[^\/]+\/(a+)/.exec(options.url); 148 | var value = m[1] + (options.method || 'GET'); 149 | t.info('mock callback: ' + value); 150 | return value; 151 | }); 152 | io.get('http://localhost:3000/a/x').then(function (value) { 153 | t.info('got ' + value); 154 | return io.patch('http://localhost:3000/aa', null); 155 | }).then(function (value) { 156 | t.info('got ' + value); 157 | return io.put('http://localhost:3000/ab'); 158 | }).then(function (value) { 159 | t.info('got ' + value); 160 | return io.post('http://localhost:3000/aaa', {z: 1}); 161 | }).then(function (value) { 162 | t.info('got ' + value); 163 | io.mock(matcher); 164 | return io.get('http://localhost:3000/aa'); 165 | }).then(function () { 166 | t.info('shouldn\'t be here!'); 167 | x.done(); 168 | }, function () { 169 | t.info('error'); 170 | x.done(); 171 | }); 172 | }, 173 | logs: [ 174 | 'mock callback: aGET', 175 | 'got aGET', 176 | 'mock callback: aaPATCH', 177 | 'got aaPATCH', 178 | 'mock callback: aPUT', 179 | 'got aPUT', 180 | 'mock callback: aaaPOST', 181 | 'got aaaPOST', 182 | 'error' 183 | ] 184 | }, 185 | { 186 | test: function test_xhr (t) { 187 | var x = t.startAsync(); 188 | io.mock('http://localhost:3000/a', function (options) { 189 | return io.mock.makeXHR({ 190 | status: options.data.status, 191 | headers: 'Content-Type: application/json', 192 | responseType: 'json', 193 | responseText: JSON.stringify(options.data) 194 | }); 195 | }); 196 | io.get('http://localhost:3000/a', {status: 200, payload: 1}).then(function (value) { 197 | t.info('payload ' + value.payload); 198 | return io.put('http://localhost:3000/a', {status: 501, payload: 2}); 199 | }).then(function () { 200 | t.info('shouldn\'t be here!'); 201 | x.done(); 202 | }, function (value) { 203 | io.mock('http://localhost:3000/a', null); 204 | t.info('payload ' + value.xhr.response.payload); 205 | t.info('error ' + value.xhr.status); 206 | x.done(); 207 | }); 208 | }, 209 | logs: [ 210 | 'payload 1', 211 | 'payload 2', 212 | 'error 501' 213 | ] 214 | }, 215 | function test_promise (t) { 216 | var x = t.startAsync(); 217 | io.mock('http://localhost:3000/a', function () { 218 | return io.get('http://localhost:3000/api'); 219 | }); 220 | io.get('http://localhost:3000/a').then(function (data) { 221 | eval(t.TEST('typeof data === "object"')); 222 | eval(t.TEST('data.method === "GET"')); 223 | io.mock('http://localhost:3000/a', null); 224 | x.done(); 225 | }); 226 | }, 227 | function test_timeout (t) { 228 | var x = t.startAsync(); 229 | io.mock('http://localhost:3000/a', function () { 230 | return timeout.resolve(20, Deferred).then(function () { 231 | return 42; 232 | }); 233 | }); 234 | io.get('http://localhost:3000/a').then(function (data) { 235 | eval(t.TEST('data === 42')); 236 | io.mock('http://localhost:3000/a', null); 237 | x.done(); 238 | }); 239 | }, 240 | function test_priority (t) { 241 | var x = t.startAsync(); 242 | io.get('http://localhost:3000/api').then(function (data) { 243 | eval(t.TEST('typeof data === "object"')); 244 | eval(t.TEST('data.method === "GET"')); 245 | io.mock('http://localhost:3000/api', function () { return 42; }); 246 | return io.get('http://localhost:3000/api'); 247 | }).then(function (data) { 248 | eval(t.TEST('data === 42')); 249 | io.mock('http://localhost:3000/api', null); 250 | return io.get('http://localhost:3000/api'); 251 | }).then(function (data) { 252 | eval(t.TEST('typeof data === "object"')); 253 | eval(t.TEST('data.method === "GET"')); 254 | x.done(); 255 | }); 256 | }, 257 | function test_teardown () { 258 | io.Deferred = io.FauxDeferred; 259 | io.mock.detach(); 260 | } 261 | ]); 262 | 263 | return {}; 264 | }); 265 | -------------------------------------------------------------------------------- /tests/test-io.js: -------------------------------------------------------------------------------- 1 | define(['module', 'heya-unit', 'heya-io/io', 'heya-async/Deferred'], function (module, unit, io, Deferred) { 2 | 'use strict'; 3 | 4 | var isXml = /^application\/xml\b/, 5 | isJson = /^application\/json\b/, 6 | isOctetStream = /^application\/octet-stream\b/, 7 | isMultiPart = /^multipart\/form-data\b/; 8 | 9 | unit.add(module, [ 10 | function test_setup () { 11 | io.Deferred = Deferred; 12 | }, 13 | function test_exist (t) { 14 | eval(t.TEST('typeof io == "function"')); 15 | eval(t.TEST('typeof io.get == "function"')); 16 | eval(t.TEST('typeof io.put == "function"')); 17 | eval(t.TEST('typeof io.post == "function"')); 18 | eval(t.TEST('typeof io.patch == "function"')); 19 | eval(t.TEST('typeof io.remove == "function"')); 20 | eval(t.TEST('typeof io["delete"] == "function"')); 21 | }, 22 | function test_simple_io (t) { 23 | var x = t.startAsync(); 24 | io('http://localhost:3000/api').then(function (data) { 25 | eval(t.TEST('data.method === "GET"')); 26 | eval(t.TEST('data.body === null')); 27 | x.done(); 28 | }); 29 | }, 30 | function test_io_get (t) { 31 | var x = t.startAsync(); 32 | io.get('http://localhost:3000/api').then(function (data) { 33 | eval(t.TEST('data.method === "GET"')); 34 | eval(t.TEST('data.body === null')); 35 | x.done(); 36 | }); 37 | }, 38 | function test_io_put (t) { 39 | var x = t.startAsync(); 40 | io.put('http://localhost:3000/api', {a: 1}).then(function (data) { 41 | eval(t.TEST('data.method === "PUT"')); 42 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 43 | x.done(); 44 | }); 45 | }, 46 | function test_io_post (t) { 47 | var x = t.startAsync(); 48 | io.post('http://localhost:3000/api', {a: 1}).then(function (data) { 49 | eval(t.TEST('data.method === "POST"')); 50 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 51 | x.done(); 52 | }); 53 | }, 54 | function test_io_patch (t) { 55 | var x = t.startAsync(); 56 | io.patch('http://localhost:3000/api', {a: 1}).then(function (data) { 57 | eval(t.TEST('data.method === "PATCH"')); 58 | eval(t.TEST('data.body === "{\\"a\\":1}"')); 59 | x.done(); 60 | }); 61 | }, 62 | function test_io_remove (t) { 63 | var x = t.startAsync(); 64 | io.remove('http://localhost:3000/api').then(function (data) { 65 | eval(t.TEST('data.method === "DELETE"')); 66 | eval(t.TEST('data.body === null')); 67 | x.done(); 68 | }); 69 | }, 70 | function test_io_get_query (t) { 71 | var x = t.startAsync(); 72 | io.get('http://localhost:3000/api', {a: 1}).then(function (data) { 73 | eval(t.TEST('data.method === "GET"')); 74 | eval(t.TEST('t.unify(data.query, {a: "1"})')); 75 | x.done(); 76 | }); 77 | }, 78 | function test_io_get_error (t) { 79 | var x = t.startAsync(); 80 | io.get('http://localhost:3000/api', {status: 500}).then(function (data) { 81 | t.test(false); // we should not be here 82 | x.done(); 83 | }).catch(function (data) { 84 | eval(t.TEST('data.xhr.status === 500')); 85 | x.done(); 86 | }); 87 | }, 88 | function test_io_get_txt (t) { 89 | var x = t.startAsync(); 90 | io.get('http://localhost:3000/api', {payloadType: 'txt'}).then(function (data) { 91 | eval(t.TEST('typeof data == "string"')); 92 | eval(t.TEST('data == "Hello, world!"')); 93 | x.done(); 94 | }); 95 | }, 96 | function test_io_get_xml (t) { 97 | var x = t.startAsync(); 98 | io.get('http://localhost:3000/api', {payloadType: 'xml'}).then(function (data) { 99 | eval(t.TEST('typeof data == "object"')); 100 | eval(t.TEST('data.nodeName == "#document"')); 101 | eval(t.TEST('data.nodeType == 9')); 102 | return io.post('http://localhost:3000/api', data); 103 | }).then(function (data) { 104 | eval(t.TEST('isXml.test(data.headers["content-type"])')); 105 | x.done(); 106 | }); 107 | }, 108 | function test_io_get_xml_as_text_mime (t) { 109 | var x = t.startAsync(); 110 | io.get({ 111 | url: 'http://localhost:3000/api', 112 | mime: 'text/plain' 113 | }, {payloadType: 'xml'}).then(function (data) { 114 | eval(t.TEST('typeof data == "string"')); 115 | eval(t.TEST('data == "
Hello, world!
"')); 116 | x.done(); 117 | }); 118 | }, 119 | function test_io_get_xml_as_text (t) { 120 | var x = t.startAsync(); 121 | io.get({ 122 | url: 'http://localhost:3000/api', 123 | responseType: 'text' 124 | }, {payloadType: 'xml'}).then(function (data) { 125 | eval(t.TEST('typeof data == "string"')); 126 | eval(t.TEST('data == "
Hello, world!
"')); 127 | x.done(); 128 | }); 129 | }, 130 | function test_io_get_xml_as_blob (t) { 131 | if (typeof Blob == 'undefined') return; 132 | var x = t.startAsync(); 133 | io.get({ 134 | url: 'http://localhost:3000/api', 135 | responseType: 'blob' 136 | }, {payloadType: 'xml'}).then(function (data) { 137 | eval(t.TEST('data instanceof Blob')); 138 | return io.post('http://localhost:3000/api', data); 139 | }).then(function (data) { 140 | eval(t.TEST('isXml.test(data.headers["content-type"])')); 141 | x.done(); 142 | }); 143 | }, 144 | function test_io_get_xml_as_array_buffer (t) { 145 | if (typeof ArrayBuffer == 'undefined' || typeof DataView == 'undefined') return; 146 | var x = t.startAsync(); 147 | io.get({ 148 | url: 'http://localhost:3000/api', 149 | responseType: 'arraybuffer' 150 | }, {payloadType: 'xml'}).then(function (data) { 151 | eval(t.TEST('data instanceof ArrayBuffer')); 152 | return io.post('http://localhost:3000/api', new DataView(data)); 153 | }).then(function (data) { 154 | eval(t.TEST('isOctetStream.test(data.headers["content-type"])')); 155 | x.done(); 156 | }); 157 | }, 158 | function test_io_post_formdata (t) { 159 | if (typeof FormData == 'undefined') return; 160 | var x = t.startAsync(); 161 | var div = document.createElement('div'); 162 | div.innerHTML = '
'; 163 | var data = new FormData(div.firstChild); 164 | data.append('user', 'heh!'); 165 | io.post('http://localhost:3000/api', data).then(function (data) { 166 | eval(t.TEST('isMultiPart.test(data.headers["content-type"])')); 167 | x.done(); 168 | }); 169 | }, 170 | function test_io_post_int8array (t) { 171 | if (typeof Int8Array == 'undefined') return; 172 | var x = t.startAsync(); 173 | var data = new Int8Array(8); 174 | data[0] = 32; 175 | data[1] = 42; 176 | io.post('http://localhost:3000/api', data).then(function (data) { 177 | eval(t.TEST('isOctetStream.test(data.headers["content-type"])')); 178 | x.done(); 179 | }); 180 | }, 181 | function test_io_post_ignore (t) { 182 | var x = t.startAsync(); 183 | io.post({ 184 | url: 'http://localhost:3000/api', 185 | headers: { 186 | 'Content-Type': 'application/json' 187 | } 188 | }, 'abc').then(function (data) { 189 | eval(t.TEST('isJson.test(data.headers["content-type"])')); 190 | eval(t.TEST('data.body === \'"abc"\'')); 191 | return io.post({ 192 | url: 'http://localhost:3000/api', 193 | headers: { 194 | 'Content-Type': 'application/json' 195 | } 196 | }, new (io.Ignore)('abc')); 197 | }).then(function (data) { 198 | eval(t.TEST('isJson.test(data.headers["content-type"])')); 199 | eval(t.TEST('data.body === "abc"')); 200 | x.done(); 201 | }); 202 | }, 203 | function test_io_custom_mime_processor (t) { 204 | var x = t.startAsync(); 205 | var restoreOriginal = io.mimeProcessors; 206 | io.mimeProcessors = []; 207 | io.mimeProcessors.push(function(contentType){ 208 | return contentType === 'text/plain; charset=utf-8'; 209 | }); 210 | io.mimeProcessors.push(function(xhr, contentType){ 211 | eval(t.TEST('contentType === "text/plain; charset=utf-8"')); 212 | eval(t.TEST('xhr.responseText == "Hello, world!"')); 213 | return 'Custom Parser Result'; 214 | }); 215 | io.get('http://localhost:3000/api', {payloadType: 'txt'}).then(function (data) { 216 | eval(t.TEST('typeof data == "string"')); 217 | eval(t.TEST('data === "Custom Parser Result"')); 218 | io.mimeProcessors = restoreOriginal; 219 | x.done(); 220 | }); 221 | }, 222 | function test_io_get_as_xhr (t) { 223 | var x = t.startAsync(); 224 | io.get({ 225 | url: 'http://localhost:3000/api', 226 | returnXHR: true 227 | }).then(function (xhr) { 228 | var data = io.getData(xhr), headers = io.getHeaders(xhr); 229 | eval(t.TEST('xhr.status == 200')); 230 | eval(t.TEST('typeof data == "object"')); 231 | eval(t.TEST('isJson.test(headers["content-type"])')); 232 | x.done(); 233 | }); 234 | }, 235 | function test_io_get_as_xhr_ignore_errors (t) { 236 | var x = t.startAsync(); 237 | io.get({ 238 | url: 'http://localhost:3000/api', 239 | query: {status: 500}, 240 | returnXHR: false, 241 | ignoreBadStatus: true 242 | }).then(function (data) { 243 | t.test(false); // we should not be here 244 | x.done(); 245 | }).catch(function (data) { 246 | eval(t.TEST('data.xhr.status === 500')); 247 | return io.get({ 248 | url: 'http://localhost:3000/api', 249 | query: {status: 500}, 250 | returnXHR: true, 251 | ignoreBadStatus: false 252 | }); 253 | }).then(function (xhr) { 254 | t.test(false); // we should not be here 255 | x.done(); 256 | }).catch(function (data) { 257 | eval(t.TEST('data.xhr.status === 500')); 258 | return io.get({ 259 | url: 'http://localhost:3000/api', 260 | query: {status: 500}, 261 | returnXHR: true, 262 | ignoreBadStatus: true 263 | }); 264 | }).then(function (xhr) { 265 | var data = io.getData(xhr), headers = io.getHeaders(xhr); 266 | eval(t.TEST('xhr.status == 500')); 267 | eval(t.TEST('typeof data == "object"')); 268 | eval(t.TEST('isJson.test(headers["content-type"])')); 269 | x.done(); 270 | }); 271 | }, 272 | function test_teardown () { 273 | io.Deferred = io.FauxDeferred; 274 | } 275 | ]); 276 | 277 | return {}; 278 | }); 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `io` 2 | 3 | 4 | [![Build status][travis-image]][travis-url] 5 | [![NPM version][npm-image]][npm-url] 6 | 7 | [![Greenkeeper][greenkeeper-image]][greenkeeper-url] 8 | [![Dependencies][deps-image]][deps-url] 9 | [![devDependencies][dev-deps-image]][dev-deps-url] 10 | 11 | A minimal, yet flexible I/O for browser and Node with promises. A thin wrapper on top of [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), 12 | and [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), with numerous callbacks to simplify and automate all aspects of I/O especially using [JSON](http://www.json.org/) as an envelope, 13 | including to add more transports, and I/O orchestration plugin services. 14 | 15 | It can run on Node using a specialized transport: [heya-io-node](https://github.com/heya/io-node). It greatly simplifies I/O on Node by leveraging enhanced features of `heya-io` in the server environment. 16 | 17 | The following services are included: 18 | 19 | * `io.cache` — a transparent application-level cache (supports [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) and 20 | [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) out of the box). 21 | * `io.bundle` — a transparent service to bundle requests into one package passing it to a server, and unbundling a result. 22 | It requires a simple server counterpart. [heya-bundler](https://github.com/heya/bundler) is a reference implementation for node.js/express.js. 23 | * `io.track` — a simple plugin to track I/O requests to eliminate duplicates, register an interest without initiating I/O requests, and much more. 24 | * `io.mock` — a way to mock I/O requests without writing a special server courtesy of [Mike Wilcox](https://github.com/clubajax). Very useful for rapid prototyping and writing tests. 25 | * `io.bust` — a simple plugin to generate a randomized query value to bust browser's cache. 26 | * `io.retry` — a plugin to retry unreliable services or watch changes over time. 27 | 28 | The following additional transports are provided: 29 | 30 | * `io.fetch()` — replaces `XHR` with `fetch()`-based transport. 31 | * `io.jsonp()` — [JSON-P](http://json-p.org/) requests. 32 | * `io.load()` — generates ` 173 | ``` 174 | 175 | And used with globals like in examples above: 176 | 177 | ```js 178 | heya.io.get('/hello').then(function (value) { 179 | console.log(value); 180 | }); 181 | ``` 182 | 183 | To support browsers without the standard `Promise`, you may want to use [heya-async](https://github.com/heya/async). 184 | 185 | AMD: 186 | 187 | ```js 188 | define(['heya-io', 'heya-async/FastDeferred'], function (io, Deferred) { 189 | // instrument 190 | io.Deferred = Deferred; 191 | // now we are ready for all browsers 192 | io.get('/hello').then(function (value) { 193 | console.log(value); 194 | }); 195 | }); 196 | ``` 197 | 198 | Globals: 199 | 200 | ```html 201 | 202 | 203 | 204 | ``` 205 | 206 | ```js 207 | // instrument 208 | heya.io.Deferred = heya.async.FastDeferred; 209 | // now we are ready for all browsers 210 | heya.io.get('/hello').then(function (value) { 211 | console.log(value); 212 | }); 213 | ``` 214 | 215 | See [How to include](https://github.com/heya/io/wiki/How-to-include) for more details. 216 | 217 | # Documentation 218 | 219 | All documentation can be found in [project's wiki](https://github.com/heya/io/wiki). 220 | 221 | # Working on this project 222 | 223 | In order to run tests in a browser of your choice, so you can debug interactively, start the test server: 224 | 225 | ```bash 226 | npm start 227 | ``` 228 | 229 | Then open this URL in a browser: http://localhost:3000/tests/tests.html It will show a blank screen, but the output will appear in the console of your developer tools. 230 | 231 | The server runs indefinitely, and can be stopped by Ctrl+C. 232 | 233 | # Versions 234 | 235 | - 1.9.3 *Refreshed dev dependencies.* 236 | - 1.9.2 *Switched `retry` to the UMD loader so it can be used in Node directly.* 237 | - 1.9.1 *Minor improvements of the `retry` service.* 238 | - 1.9.0 *Bugfixes and refactoring in the `retry` service.* 239 | - 1.8.0 *Added `retry` service. Thx [Jason Vanderslice](https://github.com/jasonvanderslice)!* 240 | - 1.7.1 *Refreshed dev dependencies.* 241 | - 1.7.0 *Added `AbortRequest`.* 242 | - 1.6.2 *Added separate `options.onDownloadProgress()` and `options.onUploadProgress()`.* 243 | - 1.6.1 *Added extra properties to progress data.* 244 | - 1.6.0 *Added `options.onProgress()` and tests on Firefox Puppeteer.* 245 | - 1.5.0 *Added cache removal by a function.* 246 | - 1.4.2 *Added `ignoreBadStatus` flag when `returnXHR`.* 247 | - 1.4.1 *Technical release. No changes.* 248 | - 1.4.0 *Added mocks by regular expressions and matcher functions.* 249 | - 1.3.0 *Added cache removal by regular expressions and wildcards.* 250 | - 1.2.6 *Bugfixes: `getHeaders()` behaves like on Node, empty object queries are supported.* 251 | - 1.2.5 *Exposed `io.getData(xhr)` and `io.getHeaders(xhr)`.* 252 | - 1.2.4 *Relaxed cache's detection of Result().* 253 | - 1.2.3 *Regenerated dist.* 254 | - 1.2.2 *Moved tests to Puppeteer, bugfixes, improved docs.* 255 | - 1.2.1 *Added Ignore type for data processors, bugfixes.* 256 | - 1.2.0 *Clarified DELETE, added more well-known types.* 257 | - 1.1.7 *Refreshed dependencies.* 258 | - 1.1.6 *Bugfix: `processFailure` could be skipped.* 259 | - 1.1.5 *Bugfix: MIME processors. Thx [Bryan Pease](https://github.com/Akeron972)!* 260 | - 1.1.4 *Added custom data and MIME processors.* 261 | - 1.1.3 *Formalized requests and responses with no bodies.* 262 | - 1.1.2 *Minor fixes for non-browser environments. New alias and verb.* 263 | - 1.1.1 *Added `url` tagged literals (an ES6 feature).* 264 | - 1.1.0 *Added fetch() as an alternative default transport.* 265 | - 1.0.9 *Correcting typos in README. New version of a test server.* 266 | - 1.0.8 *Add a helper for busting browser cache.* 267 | - 1.0.7 *Regenerated dist.* 268 | - 1.0.6 *Added a helper to extract data from XHR in case of errors.* 269 | - 1.0.5 *XHR can be reinstated from a JSON object, not just a string.* 270 | - 1.0.4 *Regenerated dist.* 271 | - 1.0.3 *Bugfix: cache XHR object directly.* 272 | - 1.0.2 *Fixed formatting errors in README.* 273 | - 1.0.1 *Improved documentation.* 274 | - 1.0.0 *The initial public release as heya-io. Sunset of heya-request. Move from bitbucket.* 275 | 276 | # License 277 | 278 | BSD or AFL — your choice. 279 | 280 | 281 | [npm-image]: https://img.shields.io/npm/v/heya-io.svg 282 | [npm-url]: https://npmjs.org/package/heya-io 283 | [deps-image]: https://img.shields.io/david/heya/io.svg 284 | [deps-url]: https://david-dm.org/heya/io 285 | [dev-deps-image]: https://img.shields.io/david/dev/heya/io.svg 286 | [dev-deps-url]: https://david-dm.org/heya/io?type=dev 287 | [travis-image]: https://img.shields.io/travis/heya/io.svg 288 | [travis-url]: https://travis-ci.org/heya/io 289 | [greenkeeper-image]: https://badges.greenkeeper.io/heya/io.svg 290 | [greenkeeper-url]: https://greenkeeper.io/ 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This library is available under *either* the terms of the modified BSD license 2 | *or* the Academic Free License version 2.1. As a recipient of this work, you 3 | may choose which license to receive this code under. No external contributions 4 | are allowed under licenses which are fundamentally incompatible with the AFL 5 | or BSD licenses that this library is distributed under. 6 | 7 | The text of the AFL and BSD licenses is reproduced below. 8 | 9 | ------------------------------------------------------------------------------- 10 | The "New" BSD License: 11 | ********************** 12 | 13 | Copyright (c) 2005-2012, Eugene Lazutkin 14 | All rights reserved. 15 | 16 | Redistribution and use in source and binary forms, with or without 17 | modification, are permitted provided that the following conditions are met: 18 | 19 | * Redistributions of source code must retain the above copyright notice, this 20 | list of conditions and the following disclaimer. 21 | * Redistributions in binary form must reproduce the above copyright notice, 22 | this list of conditions and the following disclaimer in the documentation 23 | and/or other materials provided with the distribution. 24 | * Neither the name of Eugene Lazutkin nor the names of other contributors 25 | may be used to endorse or promote products derived from this software 26 | without specific prior written permission. 27 | 28 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 29 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 30 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 31 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 32 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 33 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 34 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 35 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 36 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 37 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 38 | 39 | ------------------------------------------------------------------------------- 40 | The Academic Free License, v. 2.1: 41 | ********************************** 42 | 43 | This Academic Free License (the "License") applies to any original work of 44 | authorship (the "Original Work") whose owner (the "Licensor") has placed the 45 | following notice immediately following the copyright notice for the Original 46 | Work: 47 | 48 | Licensed under the Academic Free License version 2.1 49 | 50 | 1) Grant of Copyright License. Licensor hereby grants You a world-wide, 51 | royalty-free, non-exclusive, perpetual, sublicenseable license to do the 52 | following: 53 | 54 | a) to reproduce the Original Work in copies; 55 | 56 | b) to prepare derivative works ("Derivative Works") based upon the Original 57 | Work; 58 | 59 | c) to distribute copies of the Original Work and Derivative Works to the 60 | public; 61 | 62 | d) to perform the Original Work publicly; and 63 | 64 | e) to display the Original Work publicly. 65 | 66 | 2) Grant of Patent License. Licensor hereby grants You a world-wide, 67 | royalty-free, non-exclusive, perpetual, sublicenseable license, under patent 68 | claims owned or controlled by the Licensor that are embodied in the Original 69 | Work as furnished by the Licensor, to make, use, sell and offer for sale the 70 | Original Work and Derivative Works. 71 | 72 | 3) Grant of Source Code License. The term "Source Code" means the preferred 73 | form of the Original Work for making modifications to it and all available 74 | documentation describing how to modify the Original Work. Licensor hereby 75 | agrees to provide a machine-readable copy of the Source Code of the Original 76 | Work along with each copy of the Original Work that Licensor distributes. 77 | Licensor reserves the right to satisfy this obligation by placing a 78 | machine-readable copy of the Source Code in an information repository 79 | reasonably calculated to permit inexpensive and convenient access by You for as 80 | long as Licensor continues to distribute the Original Work, and by publishing 81 | the address of that information repository in a notice immediately following 82 | the copyright notice that applies to the Original Work. 83 | 84 | 4) Exclusions From License Grant. Neither the names of Licensor, nor the names 85 | of any contributors to the Original Work, nor any of their trademarks or 86 | service marks, may be used to endorse or promote products derived from this 87 | Original Work without express prior written permission of the Licensor. Nothing 88 | in this License shall be deemed to grant any rights to trademarks, copyrights, 89 | patents, trade secrets or any other intellectual property of Licensor except as 90 | expressly stated herein. No patent license is granted to make, use, sell or 91 | offer to sell embodiments of any patent claims other than the licensed claims 92 | defined in Section 2. No right is granted to the trademarks of Licensor even if 93 | such marks are included in the Original Work. Nothing in this License shall be 94 | interpreted to prohibit Licensor from licensing under different terms from this 95 | License any Original Work that Licensor otherwise would have a right to 96 | license. 97 | 98 | 5) This section intentionally omitted. 99 | 100 | 6) Attribution Rights. You must retain, in the Source Code of any Derivative 101 | Works that You create, all copyright, patent or trademark notices from the 102 | Source Code of the Original Work, as well as any notices of licensing and any 103 | descriptive text identified therein as an "Attribution Notice." You must cause 104 | the Source Code for any Derivative Works that You create to carry a prominent 105 | Attribution Notice reasonably calculated to inform recipients that You have 106 | modified the Original Work. 107 | 108 | 7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that 109 | the copyright in and to the Original Work and the patent rights granted herein 110 | by Licensor are owned by the Licensor or are sublicensed to You under the terms 111 | of this License with the permission of the contributor(s) of those copyrights 112 | and patent rights. Except as expressly stated in the immediately proceeding 113 | sentence, the Original Work is provided under this License on an "AS IS" BASIS 114 | and WITHOUT WARRANTY, either express or implied, including, without limitation, 115 | the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR 116 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. 117 | This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No 118 | license to Original Work is granted hereunder except under this disclaimer. 119 | 120 | 8) Limitation of Liability. Under no circumstances and under no legal theory, 121 | whether in tort (including negligence), contract, or otherwise, shall the 122 | Licensor be liable to any person for any direct, indirect, special, incidental, 123 | or consequential damages of any character arising as a result of this License 124 | or the use of the Original Work including, without limitation, damages for loss 125 | of goodwill, work stoppage, computer failure or malfunction, or any and all 126 | other commercial damages or losses. This limitation of liability shall not 127 | apply to liability for death or personal injury resulting from Licensor's 128 | negligence to the extent applicable law prohibits such limitation. Some 129 | jurisdictions do not allow the exclusion or limitation of incidental or 130 | consequential damages, so this exclusion and limitation may not apply to You. 131 | 132 | 9) Acceptance and Termination. If You distribute copies of the Original Work or 133 | a Derivative Work, You must make a reasonable effort under the circumstances to 134 | obtain the express assent of recipients to the terms of this License. Nothing 135 | else but this License (or another written agreement between Licensor and You) 136 | grants You permission to create Derivative Works based upon the Original Work 137 | or to exercise any of the rights granted in Section 1 herein, and any attempt 138 | to do so except under the terms of this License (or another written agreement 139 | between Licensor and You) is expressly prohibited by U.S. copyright law, the 140 | equivalent laws of other countries, and by international treaty. Therefore, by 141 | exercising any of the rights granted to You in Section 1 herein, You indicate 142 | Your acceptance of this License and all of its terms and conditions. 143 | 144 | 10) Termination for Patent Action. This License shall terminate automatically 145 | and You may no longer exercise any of the rights granted to You by this License 146 | as of the date You commence an action, including a cross-claim or counterclaim, 147 | against Licensor or any licensee alleging that the Original Work infringes a 148 | patent. This termination provision shall not apply for an action alleging 149 | patent infringement by combinations of the Original Work with other software or 150 | hardware. 151 | 152 | 11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this 153 | License may be brought only in the courts of a jurisdiction wherein the 154 | Licensor resides or in which Licensor conducts its primary business, and under 155 | the laws of that jurisdiction excluding its conflict-of-law provisions. The 156 | application of the United Nations Convention on Contracts for the International 157 | Sale of Goods is expressly excluded. Any use of the Original Work outside the 158 | scope of this License or after its termination shall be subject to the 159 | requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et 160 | seq., the equivalent laws of other countries, and international treaty. This 161 | section shall survive the termination of this License. 162 | 163 | 12) Attorneys Fees. In any action to enforce the terms of this License or 164 | seeking damages relating thereto, the prevailing party shall be entitled to 165 | recover its costs and expenses, including, without limitation, reasonable 166 | attorneys' fees and costs incurred in connection with such action, including 167 | any appeal of such action. This section shall survive the termination of this 168 | License. 169 | 170 | 13) Miscellaneous. This License represents the complete agreement concerning 171 | the subject matter hereof. If any provision of this License is held to be 172 | unenforceable, such provision shall be reformed only to the extent necessary to 173 | make it enforceable. 174 | 175 | 14) Definition of "You" in This License. "You" throughout this License, whether 176 | in upper or lower case, means an individual or a legal entity exercising rights 177 | under, and complying with all of the terms of, this License. For legal 178 | entities, "You" includes any entity that controls, is controlled by, or is 179 | under common control with you. For purposes of this definition, "control" means 180 | (i) the power, direct or indirect, to cause the direction or management of such 181 | entity, whether by contract or otherwise, or (ii) ownership of fifty percent 182 | (50%) or more of the outstanding shares, or (iii) beneficial ownership of such 183 | entity. 184 | 185 | 15) Right to Use. You may use the Original Work in all ways not otherwise 186 | restricted or conditioned by this License or by law, and Licensor promises not 187 | to interfere with or be responsible for such uses by You. 188 | 189 | This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. 190 | Permission is hereby granted to copy and distribute this license without 191 | modification. This license may not be modified without the express written 192 | permission of its copyright owner. 193 | -------------------------------------------------------------------------------- /dist/io.js: -------------------------------------------------------------------------------- 1 | (function(_,f,g){g=window;g=g.heya||(g.heya={});g.io=f();}) 2 | ([], function () { 3 | 'use strict'; 4 | 5 | // the I/O powerhouse 6 | 7 | function Result (xhr, options, event) { this.xhr = xhr; this.options = options; this.event = event; } 8 | Result.prototype = { 9 | getData: function () { return io.getData(this.xhr); }, 10 | getHeaders: function () { return io.getHeaders(this.xhr); } 11 | }; 12 | function FailedIO () { Result.apply(this, arguments); } 13 | FailedIO.prototype = Object.create(Result.prototype); 14 | function TimedOut () { FailedIO.apply(this, arguments); } 15 | TimedOut.prototype = Object.create(FailedIO.prototype); 16 | function BadStatus () { FailedIO.apply(this, arguments); } 17 | BadStatus.prototype = Object.create(FailedIO.prototype); 18 | 19 | function Ignore (data) { this.data = data; } 20 | 21 | function FauxDeferred () { 22 | var resolve, reject, 23 | promise = new Promise(function executor (res, rej) { 24 | resolve = res; 25 | reject = rej; 26 | }); 27 | return { 28 | resolve: resolve, 29 | reject: reject, 30 | promise: promise 31 | }; 32 | } 33 | if (typeof Promise != 'undefined') { 34 | FauxDeferred.Promise = Promise; 35 | FauxDeferred.resolve = function (value) { return Promise.resolve(value); }; 36 | FauxDeferred.reject = function (value) { return Promise.reject(value); }; 37 | } 38 | 39 | function dictToPairs (dict, processPair) { 40 | var i, key, value; 41 | for(key in dict) { 42 | value = dict[key]; 43 | if(Array.isArray(value)){ 44 | for(i = 0; i < value.length; ++i) { 45 | processPair(key, value[i]); 46 | } 47 | }else{ 48 | processPair(key, value); 49 | } 50 | } 51 | } 52 | 53 | function makeQuery (dict) { 54 | var query = []; 55 | if (dict && typeof dict == 'object') { 56 | dictToPairs(dict, function (key, value) { 57 | query.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); 58 | }); 59 | } 60 | return query.join('&'); 61 | } 62 | 63 | var requestHasNoBody = {GET: 1, HEAD: 1, OPTIONS: 1, DELETE: 1}, 64 | responseHasNoBody = {HEAD: 1, OPTIONS: 1}; 65 | 66 | function buildUrl (options) { 67 | var url = options.url, query = options.query, data = options.data; 68 | if (query) { 69 | query = typeof query == 'string' ? query : io.makeQuery(query); 70 | } else { 71 | if((!options.method || requestHasNoBody[options.method.toUpperCase()]) && data) { 72 | query = io.makeQuery(data); 73 | } 74 | } 75 | if (query) { 76 | url += (url.indexOf('?') < 0 ? '?' : '&') + query; 77 | } 78 | return url; 79 | } 80 | 81 | function setProp (xhr, value, prop) { 82 | xhr[prop] = value; 83 | } 84 | 85 | var propHandlers = { 86 | timeout: setProp, 87 | responseType: setProp, 88 | withCredentials: setProp, 89 | mime: function (xhr, value) { 90 | xhr.overrideMimeType(value); 91 | }, 92 | headers: function (xhr, value) { 93 | dictToPairs(value, function (key, val) { 94 | xhr.setRequestHeader(key, val); 95 | }); 96 | } 97 | }, 98 | whiteListedProps = Object.keys(propHandlers); 99 | 100 | function xhrTransport (options, prep) { 101 | var xhr = new XMLHttpRequest(), 102 | d = new io.Deferred(function () { 103 | // canceller 104 | xhr.abort(); 105 | }); 106 | // add event listeners 107 | xhr.onload = function (event) { 108 | d.resolve(new io.Result(xhr, options, event), true); 109 | }; 110 | xhr.onerror = function (event) { 111 | d.reject(new io.FailedIO(xhr, options, event), true); 112 | }; 113 | xhr.ontimeout = function (event) { 114 | d.reject(new io.TimedOut(xhr, options, event), true); 115 | }; 116 | var dFlag = typeof d.progress == 'function', oFlag = typeof options.onProgress == 'function', 117 | downloadFlag = typeof options.onDownloadProgress == 'function', 118 | uploadFlag = typeof options.onUploadProgress == 'function'; 119 | if (downloadFlag || oFlag || dFlag) { 120 | xhr.onprogress = function (event) { 121 | var p = {xhr: xhr, options: options, event: event, loaded: event.loaded, total: event.total, lengthComputable: event.lengthComputable, upload: false}; 122 | downloadFlag && options.onDownloadProgress(p); 123 | oFlag && options.onProgress(p); 124 | dFlag && d.progress(p); 125 | }; 126 | } 127 | if (xhr.upload && (uploadFlag || oFlag || dFlag)) { 128 | xhr.upload.onprogress = function (event) { 129 | var p = {xhr: xhr, options: options, event: event, loaded: event.loaded, total: event.total, lengthComputable: event.lengthComputable, upload: true}; 130 | uploadFlag && options.onUploadProgress(p); 131 | oFlag && options.onProgress(p); 132 | dFlag && d.progress(p); 133 | }; 134 | } 135 | // build a URL 136 | var url = prep.url; 137 | // open a connection 138 | if ('user' in options) { 139 | xhr.open(options.method || 'GET', url, true, options.user || '', options.password || ''); 140 | } else { 141 | xhr.open(options.method || 'GET', url, true); 142 | } 143 | // set properties, if any 144 | whiteListedProps.forEach(function (key) { 145 | if (key in options) { 146 | propHandlers[key](xhr, options[key], key); 147 | } 148 | }); 149 | // send data, if any 150 | xhr.send(io.processData(xhr, options, prep.data)); 151 | // set up an abort 152 | if (options.signal) { 153 | if (typeof options.signal.then == 'function') { 154 | options.signal.then(function () { xhr.abort(); }); 155 | } else if (typeof options.signal.addEventListener == 'function') { 156 | options.signal.addEventListener('abort', function () { xhr.abort(); }); 157 | } 158 | } 159 | return d.promise || d; 160 | } 161 | 162 | var isJson = /^application\/json\b/; 163 | 164 | function processData (xhr, options, data) { 165 | if (!options.headers || !options.headers.Accept) { 166 | xhr.setRequestHeader('Accept', 'application/json'); 167 | } 168 | if (!options.method || requestHasNoBody[options.method]) { 169 | return null; // ignore payload for certain verbs 170 | } 171 | if (data && typeof data == 'object') { 172 | for (var i = 0; i < io.dataProcessors.length; i += 2) { 173 | if (data instanceof io.dataProcessors[i]) { 174 | data = io.dataProcessors[i + 1](xhr, options, data); 175 | break; 176 | } 177 | } 178 | } 179 | if (data instanceof Ignore) return data.data; 180 | var contentType = options.headers && options.headers['Content-Type']; 181 | if (data) { 182 | switch (true) { 183 | case typeof Document != 'undefined' && data instanceof Document: 184 | case typeof FormData != 'undefined' && data instanceof FormData: 185 | case typeof URLSearchParams != 'undefined' && data instanceof URLSearchParams: 186 | case typeof Blob != 'undefined' && data instanceof Blob: 187 | return data; // do not process well-known types 188 | case typeof ReadableStream != 'undefined' && data instanceof ReadableStream: 189 | case typeof ArrayBuffer != 'undefined' && data instanceof ArrayBuffer: 190 | case typeof Int8Array != 'undefined' && data instanceof Int8Array: 191 | case typeof Int16Array != 'undefined' && data instanceof Int16Array: 192 | case typeof Int32Array != 'undefined' && data instanceof Int32Array: 193 | case typeof Uint8Array != 'undefined' && data instanceof Uint8Array: 194 | case typeof Uint16Array != 'undefined' && data instanceof Uint16Array: 195 | case typeof Uint32Array != 'undefined' && data instanceof Uint32Array: 196 | case typeof Uint8ClampedArray != 'undefined' && data instanceof Uint8ClampedArray: 197 | case typeof Float32Array != 'undefined' && data instanceof Float32Array: 198 | case typeof Float64Array != 'undefined' && data instanceof Float64Array: 199 | case typeof DataView != 'undefined' && data instanceof DataView: 200 | !contentType && xhr.setRequestHeader('Content-Type', 'application/octet-stream'); 201 | return data; 202 | } 203 | } 204 | if (!contentType) { 205 | if (data && typeof data == 'object') { 206 | xhr.setRequestHeader('Content-Type', 'application/json'); 207 | return JSON.stringify(data); 208 | } 209 | } else if (isJson.test(contentType)) { 210 | return JSON.stringify(data); 211 | } 212 | return data; 213 | } 214 | 215 | function processOptions (options) { 216 | return options; 217 | } 218 | 219 | function getData (xhr) { 220 | if (!xhr) return; // return undefined 221 | if (xhr.status === 204) { 222 | // no body was sent 223 | return; // return undefined 224 | } 225 | if (xhr.responseType) { 226 | return xhr.response; 227 | } 228 | var contentType = xhr.getResponseHeader('Content-Type'); 229 | mimeLoop: for (var i = 0; i < io.mimeProcessors.length; i += 2) { 230 | var mime = io.mimeProcessors[i], result; 231 | switch (true) { 232 | case mime instanceof RegExp && mime.test(contentType): 233 | case typeof mime == 'function' && mime(contentType): 234 | case typeof mime == 'string' && mime === contentType: 235 | result = io.mimeProcessors[i + 1](xhr, contentType); 236 | if (result !== undefined) { 237 | return result; 238 | } 239 | break mimeLoop; 240 | } 241 | } 242 | if (xhr.responseXML) { 243 | return xhr.responseXML; 244 | } 245 | if (isJson.test(xhr.getResponseHeader('Content-Type'))) { 246 | return JSON.parse(xhr.responseText); 247 | } 248 | return xhr.responseText; 249 | } 250 | 251 | function processSuccess (result) { 252 | if (!(result instanceof io.Result)) { 253 | return result; 254 | } 255 | if ((!result.options.returnXHR || !result.options.ignoreBadStatus) && (result.xhr.status < 200 || result.xhr.status >= 300)) { 256 | return io.Deferred.reject(new io.BadStatus(result.xhr, result.options, result.event)); 257 | } 258 | if (result.options.returnXHR) { 259 | return result.xhr; 260 | } 261 | if (result.options.method && responseHasNoBody[result.options.method.toUpperCase()]) { 262 | // no body was sent 263 | return; // return undefined 264 | } 265 | return io.getData(result.xhr); 266 | } 267 | 268 | function processFailure (failure) { 269 | return io.Deferred.reject(failure); 270 | } 271 | 272 | function io (options) { 273 | // options.url - a URL endpoint 274 | // options.method? - a verb like GET, POST, PUT, and so on. Default: GET 275 | // options.query? - an optional query dictionary. Default: none. 276 | // options.data? - a data object to send. Default: null. 277 | // options.headers? - a dictionary of headers. Default: null. 278 | // options.user? - a user. Default: not sent. 279 | // options.password? - a password. Default: not sent. 280 | // options.timeout? - a wait time in ms. Default: not set. 281 | // options.responseType? - 'arraybuffer', 'blob', 'json', 'document', 'text', or ''. Default: not set. 282 | // options.withCredentials? - a Boolean flag for cross-site requests. Default: not set. 283 | // options.mime? - a string. If present, overrides a MIME type. Default: not set. 284 | // options.wait? - a Boolean flag to indicate our interest in a request without initiating it. Default: false. 285 | // options.mock? - a Boolean flag to opt-in/out in mocking. Default: as set in io.mock.defaultOptIn. 286 | // options.track? - a Boolean flag to opt-in/out in tracking. Default: as set in io.track.defaultOptIn. 287 | // options.cache? - a Boolean flag to opt-in/out in caching. Default: as set in io.cache.defaultOptIn. 288 | // options.bundle? - a Boolean flag to opt-in/out of bundling. Default: as set in io.bundle.defaultOptIn. 289 | // options.returnXHR - a Boolean flag to return an XHR object instead of a decoded data, if available. 290 | // options.processSuccess - a function to extract a value for a successful I/O. Default: io.processSuccess. 291 | // options.processFailure - a function to extract a value for a failed I/O. Default: io.processFailure. 292 | 293 | switch (typeof options) { 294 | case 'string': 295 | options = {url: options, method: 'GET'}; 296 | break; 297 | case 'object': 298 | break; 299 | default: 300 | return io.Deferred.reject(new Error('IO: a parameter should be an object or a string (url).')); 301 | } 302 | 303 | options = io.processOptions(options); 304 | 305 | return io.request(options). 306 | then(options.processSuccess || io.processSuccess). 307 | catch(options.processFailure || io.processFailure); 308 | } 309 | 310 | 311 | // services 312 | 313 | // Service is represented by an object with three properties: 314 | // name - a unique id of a service 315 | // priority - a number indicating a priority, services with higher priority 316 | // will be called first. A range of 0-100 is suggested. 317 | // callback(options, prep, level) - a function called in the context of 318 | // a service structure. It should return a correctly formed promise, 319 | // or a falsy value to indicate that the next service should be tried. 320 | 321 | function byPriority (a, b) { return a.priority - b.priority; } 322 | 323 | function attach (service) { 324 | io.detach(service.name); 325 | io.services.push(service); 326 | io.services.sort(byPriority); 327 | } 328 | 329 | function detach (name) { 330 | for (var s = io.services, i = 0; i < s.length; ++i) { 331 | if (s[i].name === name) { 332 | s.splice(i, 1); 333 | break; 334 | } 335 | } 336 | } 337 | 338 | function request (options, prep, level) { 339 | prep = prep || io.prepareRequest(options); 340 | for (var s = io.services, i = Math.min(s.length - 1, isNaN(level) ? Infinity : level); i >= 0; --i) { 341 | var result = s[i].callback(options, prep, i); 342 | if (result) { 343 | return result; 344 | } 345 | } 346 | return (io.transports[options.transport] || io.defaultTransport)(options, prep); 347 | } 348 | 349 | function makeKey (options) { 350 | return io.prefix + (options.method || 'GET') + '-' + io.buildUrl(options); 351 | } 352 | 353 | function prepareRequest (options) { 354 | var prep = {url: io.buildUrl(options)}; 355 | prep.key = io.prefix + (options.method || 'GET') + '-' + prep.url; 356 | prep.data = options.data || null; 357 | if(!options.query && prep.data && 358 | (!options.method || requestHasNoBody[options.method.toUpperCase()])) { 359 | prep.data = null; // we processed it as a query, no need to send it 360 | } 361 | return prep; 362 | } 363 | 364 | 365 | // convenience methods 366 | 367 | function makeVerb (verb, method) { 368 | return function (url, data) { 369 | var options = typeof url == 'string' ? {url: url} : Object.create(url); 370 | options[method || 'method'] = verb; 371 | if (data) { 372 | options.data = data; 373 | } 374 | return io(options); 375 | }; 376 | } 377 | 378 | var noDups = {age: 1, authorization: 1, 'content-length': 1, 'content-type': 1, etag: 1, expires: 1, from: 1, host: 1, 'if-modified-since': 1, 379 | 'if-unmodified-since': 1, 'last-modified': 1, location: 1, 'max-forwards': 1, 'proxy-authorization': 1, referer: 1, 'retry-after': 1, 'user-agent': 1}; 380 | function getHeaders (xhr) { 381 | var headers = {}; 382 | if (!xhr || typeof xhr.getAllResponseHeaders != 'function') return headers; 383 | var rawHeaders = xhr.getAllResponseHeaders(); 384 | if (!rawHeaders) return headers; 385 | rawHeaders.split('\r\n').forEach(function (line) { 386 | var header = /^\s*([\w\-]+)\s*:\s*(.*)$/.exec(line); 387 | if (!header) return; 388 | var key = header[1].toLowerCase(), value = headers[key]; 389 | if (key === 'set-cookie') { 390 | if (!(value instanceof Array)) headers[key] = []; 391 | headers[key].push(header[2]); 392 | } else if (typeof value == 'string') { 393 | headers[key] = noDups[key] ? header[2] : (value + ', ' + header[2]); 394 | } else { 395 | headers[key] = header[2]; 396 | } 397 | }); 398 | return headers; 399 | } 400 | 401 | function AbortRequest () { 402 | var d = new io.Deferred(); 403 | this.promise = d.promise || d; 404 | this.abort = function() { d.resolve(); }; 405 | } 406 | 407 | ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].forEach(function (verb) { 408 | io[verb.toLowerCase()] = makeVerb(verb); 409 | }); 410 | io.del = io.remove = io['delete']; // alias for simplicity 411 | 412 | 413 | // export 414 | 415 | io.Result = Result; 416 | io.FailedIO = FailedIO; 417 | io.TimedOut = TimedOut; 418 | io.BadStatus = BadStatus; 419 | io.Deferred = io.FauxDeferred = FauxDeferred; 420 | io.Ignore = Ignore; 421 | 422 | io.AbortRequest = AbortRequest; 423 | 424 | io.prefix = 'io-'; 425 | io.makeKey = makeKey; 426 | io.makeQuery = makeQuery; 427 | io.buildUrl = buildUrl; 428 | io.getHeaders = getHeaders; 429 | io.getData = getData; 430 | io.makeVerb = makeVerb; 431 | 432 | io.processOptions = processOptions; 433 | io.processSuccess = processSuccess; 434 | io.processFailure = processFailure; 435 | io.processData = processData; 436 | io.prepareRequest = prepareRequest; 437 | io.dataProcessors = []; 438 | io.mimeProcessors = []; 439 | 440 | io.defaultTransport = io.xhrTransport = xhrTransport; 441 | io.transports = {}; 442 | 443 | io.request = request; 444 | 445 | io.services = []; 446 | io.attach = attach; 447 | io.detach = detach; 448 | 449 | return io; 450 | }); 451 | -------------------------------------------------------------------------------- /io.js: -------------------------------------------------------------------------------- 1 | /* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))}) 2 | ([], function () { 3 | 'use strict'; 4 | 5 | // the I/O powerhouse 6 | 7 | function Result (xhr, options, event) { this.xhr = xhr; this.options = options; this.event = event; } 8 | Result.prototype = { 9 | getData: function () { return io.getData(this.xhr); }, 10 | getHeaders: function () { return io.getHeaders(this.xhr); } 11 | }; 12 | function FailedIO () { Result.apply(this, arguments); } 13 | FailedIO.prototype = Object.create(Result.prototype); 14 | function TimedOut () { FailedIO.apply(this, arguments); } 15 | TimedOut.prototype = Object.create(FailedIO.prototype); 16 | function BadStatus () { FailedIO.apply(this, arguments); } 17 | BadStatus.prototype = Object.create(FailedIO.prototype); 18 | 19 | function Ignore (data) { this.data = data; } 20 | 21 | function FauxDeferred () { 22 | var resolve, reject, 23 | promise = new Promise(function executor (res, rej) { 24 | resolve = res; 25 | reject = rej; 26 | }); 27 | return { 28 | resolve: resolve, 29 | reject: reject, 30 | promise: promise 31 | }; 32 | } 33 | if (typeof Promise != 'undefined') { 34 | FauxDeferred.Promise = Promise; 35 | FauxDeferred.resolve = function (value) { return Promise.resolve(value); }; 36 | FauxDeferred.reject = function (value) { return Promise.reject(value); }; 37 | } 38 | 39 | function dictToPairs (dict, processPair) { 40 | var i, key, value; 41 | for(key in dict) { 42 | value = dict[key]; 43 | if(Array.isArray(value)){ 44 | for(i = 0; i < value.length; ++i) { 45 | processPair(key, value[i]); 46 | } 47 | }else{ 48 | processPair(key, value); 49 | } 50 | } 51 | } 52 | 53 | function makeQuery (dict) { 54 | var query = []; 55 | if (dict && typeof dict == 'object') { 56 | dictToPairs(dict, function (key, value) { 57 | query.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); 58 | }); 59 | } 60 | return query.join('&'); 61 | } 62 | 63 | var requestHasNoBody = {GET: 1, HEAD: 1, OPTIONS: 1, DELETE: 1}, 64 | responseHasNoBody = {HEAD: 1, OPTIONS: 1}; 65 | 66 | function buildUrl (options) { 67 | var url = options.url, query = options.query, data = options.data; 68 | if (query) { 69 | query = typeof query == 'string' ? query : io.makeQuery(query); 70 | } else { 71 | if((!options.method || requestHasNoBody[options.method.toUpperCase()]) && data) { 72 | query = io.makeQuery(data); 73 | } 74 | } 75 | if (query) { 76 | url += (url.indexOf('?') < 0 ? '?' : '&') + query; 77 | } 78 | return url; 79 | } 80 | 81 | function setProp (xhr, value, prop) { 82 | xhr[prop] = value; 83 | } 84 | 85 | var propHandlers = { 86 | timeout: setProp, 87 | responseType: setProp, 88 | withCredentials: setProp, 89 | mime: function (xhr, value) { 90 | xhr.overrideMimeType(value); 91 | }, 92 | headers: function (xhr, value) { 93 | dictToPairs(value, function (key, val) { 94 | xhr.setRequestHeader(key, val); 95 | }); 96 | } 97 | }, 98 | whiteListedProps = Object.keys(propHandlers); 99 | 100 | function xhrTransport (options, prep) { 101 | var xhr = new XMLHttpRequest(), 102 | d = new io.Deferred(function () { 103 | // canceller 104 | xhr.abort(); 105 | }); 106 | // add event listeners 107 | xhr.onload = function (event) { 108 | d.resolve(new io.Result(xhr, options, event), true); 109 | }; 110 | xhr.onerror = function (event) { 111 | d.reject(new io.FailedIO(xhr, options, event), true); 112 | }; 113 | xhr.ontimeout = function (event) { 114 | d.reject(new io.TimedOut(xhr, options, event), true); 115 | }; 116 | var dFlag = typeof d.progress == 'function', oFlag = typeof options.onProgress == 'function', 117 | downloadFlag = typeof options.onDownloadProgress == 'function', 118 | uploadFlag = typeof options.onUploadProgress == 'function'; 119 | if (downloadFlag || oFlag || dFlag) { 120 | xhr.onprogress = function (event) { 121 | var p = {xhr: xhr, options: options, event: event, loaded: event.loaded, total: event.total, lengthComputable: event.lengthComputable, upload: false}; 122 | downloadFlag && options.onDownloadProgress(p); 123 | oFlag && options.onProgress(p); 124 | dFlag && d.progress(p); 125 | }; 126 | } 127 | if (xhr.upload && (uploadFlag || oFlag || dFlag)) { 128 | xhr.upload.onprogress = function (event) { 129 | var p = {xhr: xhr, options: options, event: event, loaded: event.loaded, total: event.total, lengthComputable: event.lengthComputable, upload: true}; 130 | uploadFlag && options.onUploadProgress(p); 131 | oFlag && options.onProgress(p); 132 | dFlag && d.progress(p); 133 | }; 134 | } 135 | // build a URL 136 | var url = prep.url; 137 | // open a connection 138 | if ('user' in options) { 139 | xhr.open(options.method || 'GET', url, true, options.user || '', options.password || ''); 140 | } else { 141 | xhr.open(options.method || 'GET', url, true); 142 | } 143 | // set properties, if any 144 | whiteListedProps.forEach(function (key) { 145 | if (key in options) { 146 | propHandlers[key](xhr, options[key], key); 147 | } 148 | }); 149 | // send data, if any 150 | xhr.send(io.processData(xhr, options, prep.data)); 151 | // set up an abort 152 | if (options.signal) { 153 | if (typeof options.signal.then == 'function') { 154 | options.signal.then(function () { xhr.abort(); }); 155 | } else if (typeof options.signal.addEventListener == 'function') { 156 | options.signal.addEventListener('abort', function () { xhr.abort(); }); 157 | } 158 | } 159 | return d.promise || d; 160 | } 161 | 162 | var isJson = /^application\/json\b/; 163 | 164 | function processData (xhr, options, data) { 165 | if (!options.headers || !options.headers.Accept) { 166 | xhr.setRequestHeader('Accept', 'application/json'); 167 | } 168 | if (!options.method || requestHasNoBody[options.method]) { 169 | return null; // ignore payload for certain verbs 170 | } 171 | if (data && typeof data == 'object') { 172 | for (var i = 0; i < io.dataProcessors.length; i += 2) { 173 | if (data instanceof io.dataProcessors[i]) { 174 | data = io.dataProcessors[i + 1](xhr, options, data); 175 | break; 176 | } 177 | } 178 | } 179 | if (data instanceof Ignore) return data.data; 180 | var contentType = options.headers && options.headers['Content-Type']; 181 | if (data) { 182 | switch (true) { 183 | case typeof Document != 'undefined' && data instanceof Document: 184 | case typeof FormData != 'undefined' && data instanceof FormData: 185 | case typeof URLSearchParams != 'undefined' && data instanceof URLSearchParams: 186 | case typeof Blob != 'undefined' && data instanceof Blob: 187 | return data; // do not process well-known types 188 | case typeof ReadableStream != 'undefined' && data instanceof ReadableStream: 189 | case typeof ArrayBuffer != 'undefined' && data instanceof ArrayBuffer: 190 | case typeof Int8Array != 'undefined' && data instanceof Int8Array: 191 | case typeof Int16Array != 'undefined' && data instanceof Int16Array: 192 | case typeof Int32Array != 'undefined' && data instanceof Int32Array: 193 | case typeof Uint8Array != 'undefined' && data instanceof Uint8Array: 194 | case typeof Uint16Array != 'undefined' && data instanceof Uint16Array: 195 | case typeof Uint32Array != 'undefined' && data instanceof Uint32Array: 196 | case typeof Uint8ClampedArray != 'undefined' && data instanceof Uint8ClampedArray: 197 | case typeof Float32Array != 'undefined' && data instanceof Float32Array: 198 | case typeof Float64Array != 'undefined' && data instanceof Float64Array: 199 | case typeof DataView != 'undefined' && data instanceof DataView: 200 | !contentType && xhr.setRequestHeader('Content-Type', 'application/octet-stream'); 201 | return data; 202 | } 203 | } 204 | if (!contentType) { 205 | if (data && typeof data == 'object') { 206 | xhr.setRequestHeader('Content-Type', 'application/json'); 207 | return JSON.stringify(data); 208 | } 209 | } else if (isJson.test(contentType)) { 210 | return JSON.stringify(data); 211 | } 212 | return data; 213 | } 214 | 215 | function processOptions (options) { 216 | return options; 217 | } 218 | 219 | function getData (xhr) { 220 | if (!xhr) return; // return undefined 221 | if (xhr.status === 204) { 222 | // no body was sent 223 | return; // return undefined 224 | } 225 | if (xhr.responseType) { 226 | return xhr.response; 227 | } 228 | var contentType = xhr.getResponseHeader('Content-Type'); 229 | mimeLoop: for (var i = 0; i < io.mimeProcessors.length; i += 2) { 230 | var mime = io.mimeProcessors[i], result; 231 | switch (true) { 232 | case mime instanceof RegExp && mime.test(contentType): 233 | case typeof mime == 'function' && mime(contentType): 234 | case typeof mime == 'string' && mime === contentType: 235 | result = io.mimeProcessors[i + 1](xhr, contentType); 236 | if (result !== undefined) { 237 | return result; 238 | } 239 | break mimeLoop; 240 | } 241 | } 242 | if (xhr.responseXML) { 243 | return xhr.responseXML; 244 | } 245 | if (isJson.test(xhr.getResponseHeader('Content-Type'))) { 246 | return JSON.parse(xhr.responseText); 247 | } 248 | return xhr.responseText; 249 | } 250 | 251 | function processSuccess (result) { 252 | if (!(result instanceof io.Result)) { 253 | return result; 254 | } 255 | if ((!result.options.returnXHR || !result.options.ignoreBadStatus) && (result.xhr.status < 200 || result.xhr.status >= 300)) { 256 | return io.Deferred.reject(new io.BadStatus(result.xhr, result.options, result.event)); 257 | } 258 | if (result.options.returnXHR) { 259 | return result.xhr; 260 | } 261 | if (result.options.method && responseHasNoBody[result.options.method.toUpperCase()]) { 262 | // no body was sent 263 | return; // return undefined 264 | } 265 | return io.getData(result.xhr); 266 | } 267 | 268 | function processFailure (failure) { 269 | return io.Deferred.reject(failure); 270 | } 271 | 272 | function io (options) { 273 | // options.url - a URL endpoint 274 | // options.method? - a verb like GET, POST, PUT, and so on. Default: GET 275 | // options.query? - an optional query dictionary. Default: none. 276 | // options.data? - a data object to send. Default: null. 277 | // options.headers? - a dictionary of headers. Default: null. 278 | // options.user? - a user. Default: not sent. 279 | // options.password? - a password. Default: not sent. 280 | // options.timeout? - a wait time in ms. Default: not set. 281 | // options.responseType? - 'arraybuffer', 'blob', 'json', 'document', 'text', or ''. Default: not set. 282 | // options.withCredentials? - a Boolean flag for cross-site requests. Default: not set. 283 | // options.mime? - a string. If present, overrides a MIME type. Default: not set. 284 | // options.wait? - a Boolean flag to indicate our interest in a request without initiating it. Default: false. 285 | // options.mock? - a Boolean flag to opt-in/out in mocking. Default: as set in io.mock.defaultOptIn. 286 | // options.track? - a Boolean flag to opt-in/out in tracking. Default: as set in io.track.defaultOptIn. 287 | // options.cache? - a Boolean flag to opt-in/out in caching. Default: as set in io.cache.defaultOptIn. 288 | // options.bundle? - a Boolean flag to opt-in/out of bundling. Default: as set in io.bundle.defaultOptIn. 289 | // options.returnXHR - a Boolean flag to return an XHR object instead of a decoded data, if available. 290 | // options.processSuccess - a function to extract a value for a successful I/O. Default: io.processSuccess. 291 | // options.processFailure - a function to extract a value for a failed I/O. Default: io.processFailure. 292 | 293 | switch (typeof options) { 294 | case 'string': 295 | options = {url: options, method: 'GET'}; 296 | break; 297 | case 'object': 298 | break; 299 | default: 300 | return io.Deferred.reject(new Error('IO: a parameter should be an object or a string (url).')); 301 | } 302 | 303 | options = io.processOptions(options); 304 | 305 | return io.request(options). 306 | then(options.processSuccess || io.processSuccess). 307 | catch(options.processFailure || io.processFailure); 308 | } 309 | 310 | 311 | // services 312 | 313 | // Service is represented by an object with three properties: 314 | // name - a unique id of a service 315 | // priority - a number indicating a priority, services with higher priority 316 | // will be called first. A range of 0-100 is suggested. 317 | // callback(options, prep, level) - a function called in the context of 318 | // a service structure. It should return a correctly formed promise, 319 | // or a falsy value to indicate that the next service should be tried. 320 | 321 | function byPriority (a, b) { return a.priority - b.priority; } 322 | 323 | function attach (service) { 324 | io.detach(service.name); 325 | io.services.push(service); 326 | io.services.sort(byPriority); 327 | } 328 | 329 | function detach (name) { 330 | for (var s = io.services, i = 0; i < s.length; ++i) { 331 | if (s[i].name === name) { 332 | s.splice(i, 1); 333 | break; 334 | } 335 | } 336 | } 337 | 338 | function request (options, prep, level) { 339 | prep = prep || io.prepareRequest(options); 340 | for (var s = io.services, i = Math.min(s.length - 1, isNaN(level) ? Infinity : level); i >= 0; --i) { 341 | var result = s[i].callback(options, prep, i); 342 | if (result) { 343 | return result; 344 | } 345 | } 346 | return (io.transports[options.transport] || io.defaultTransport)(options, prep); 347 | } 348 | 349 | function makeKey (options) { 350 | return io.prefix + (options.method || 'GET') + '-' + io.buildUrl(options); 351 | } 352 | 353 | function prepareRequest (options) { 354 | var prep = {url: io.buildUrl(options)}; 355 | prep.key = io.prefix + (options.method || 'GET') + '-' + prep.url; 356 | prep.data = options.data || null; 357 | if(!options.query && prep.data && 358 | (!options.method || requestHasNoBody[options.method.toUpperCase()])) { 359 | prep.data = null; // we processed it as a query, no need to send it 360 | } 361 | return prep; 362 | } 363 | 364 | 365 | // convenience methods 366 | 367 | function makeVerb (verb, method) { 368 | return function (url, data) { 369 | var options = typeof url == 'string' ? {url: url} : Object.create(url); 370 | options[method || 'method'] = verb; 371 | if (data) { 372 | options.data = data; 373 | } 374 | return io(options); 375 | }; 376 | } 377 | 378 | var noDups = {age: 1, authorization: 1, 'content-length': 1, 'content-type': 1, etag: 1, expires: 1, from: 1, host: 1, 'if-modified-since': 1, 379 | 'if-unmodified-since': 1, 'last-modified': 1, location: 1, 'max-forwards': 1, 'proxy-authorization': 1, referer: 1, 'retry-after': 1, 'user-agent': 1}; 380 | function getHeaders (xhr) { 381 | var headers = {}; 382 | if (!xhr || typeof xhr.getAllResponseHeaders != 'function') return headers; 383 | var rawHeaders = xhr.getAllResponseHeaders(); 384 | if (!rawHeaders) return headers; 385 | rawHeaders.split('\r\n').forEach(function (line) { 386 | var header = /^\s*([\w\-]+)\s*:\s*(.*)$/.exec(line); 387 | if (!header) return; 388 | var key = header[1].toLowerCase(), value = headers[key]; 389 | if (key === 'set-cookie') { 390 | if (!(value instanceof Array)) headers[key] = []; 391 | headers[key].push(header[2]); 392 | } else if (typeof value == 'string') { 393 | headers[key] = noDups[key] ? header[2] : (value + ', ' + header[2]); 394 | } else { 395 | headers[key] = header[2]; 396 | } 397 | }); 398 | return headers; 399 | } 400 | 401 | function AbortRequest () { 402 | var d = new io.Deferred(); 403 | this.promise = d.promise || d; 404 | this.abort = function() { d.resolve(); }; 405 | } 406 | 407 | ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].forEach(function (verb) { 408 | io[verb.toLowerCase()] = makeVerb(verb); 409 | }); 410 | io.del = io.remove = io['delete']; // alias for simplicity 411 | 412 | 413 | // export 414 | 415 | io.Result = Result; 416 | io.FailedIO = FailedIO; 417 | io.TimedOut = TimedOut; 418 | io.BadStatus = BadStatus; 419 | io.Deferred = io.FauxDeferred = FauxDeferred; 420 | io.Ignore = Ignore; 421 | 422 | io.AbortRequest = AbortRequest; 423 | 424 | io.prefix = 'io-'; 425 | io.makeKey = makeKey; 426 | io.makeQuery = makeQuery; 427 | io.buildUrl = buildUrl; 428 | io.getHeaders = getHeaders; 429 | io.getData = getData; 430 | io.makeVerb = makeVerb; 431 | 432 | io.processOptions = processOptions; 433 | io.processSuccess = processSuccess; 434 | io.processFailure = processFailure; 435 | io.processData = processData; 436 | io.prepareRequest = prepareRequest; 437 | io.dataProcessors = []; 438 | io.mimeProcessors = []; 439 | 440 | io.defaultTransport = io.xhrTransport = xhrTransport; 441 | io.transports = {}; 442 | 443 | io.request = request; 444 | 445 | io.services = []; 446 | io.attach = attach; 447 | io.detach = detach; 448 | 449 | return io; 450 | }); 451 | --------------------------------------------------------------------------------