Last active
December 15, 2015 16:39
-
-
Save lsmith/5290799 to your computer and use it in GitHub Desktop.
WIP IO implementation using transports with send(requestObj, callback(err, data)) and a generic io(requestObj[, callback]) that returns promises.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>IO with transports and promises</title> | |
</head> | |
<body> | |
<script src="http://yui.yahooapis.com/3.9.1/build/yui/yui.js"></script> | |
<script> | |
// sm-io is an alias for sm-io-core and sm-io-xhr. The two modules don't require one another. | |
YUI({ filter: 'raw' }).use('sm-io', function (Y) { | |
var dataUrl = '/test/data.php'; | |
// io() returns a promise | |
var data = Y.SM.io(dataUrl); | |
console.log(data); | |
// get the response or error via promise.then(...) | |
data.then(function (data) { | |
console.log("Y.SM.io(url).then(callback)", data); | |
}); | |
// optionally pass a callback that bypasses the promise logic and is called by the transport | |
// as callback(err, data); io() still returns a promise. | |
Y.SM.io({ url: dataUrl }, function (err, data) { | |
console.log("Y.SM.io({ url: url }, callback)", data); | |
}); | |
// optionally include success and failure handlers in the request config. | |
// context and extra args must be bound with Y.bind or rbind. | |
Y.SM.io({ | |
url: dataUrl, | |
success: function (data) { | |
console.log("Y.SM.io(url, { success: fn, failure: fn })", data); | |
}, | |
failure: function (reason) { | |
console.log("Y.SM.io(url, { callbacks }) error:", reason); | |
} | |
}); | |
// Pass request configuration as a second param. Configs need only make sense | |
// to the transport's send(resource, config, callback) method. | |
Y.SM.io({ url: dataUrl, method: 'post' }, function (err, data) { | |
console.log("Y.SM.io(url, config, callback)", data); | |
}); | |
// Bypass io() and call the transport's send() method directly. Same signature, | |
// but transports require callbacks and don't create promises. Instead, send() | |
// returns the XMLHttpRequest object or other relevant object representing the | |
// connection (e.g. the <script> for jsonp). Also, they do little-to-no request | |
// prep or defaulting for you, the request config that you pass must be fully | |
// populated. | |
Y.SM.io.xhr.send({ url: dataUrl, method: 'GET' }, function (err, data) { | |
console.log("Y.SM.io.xhr.send({ url: url, method: 'GET' }, callback)", data); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
YUI.add('sm-io-form', function (Y, NAME) { | |
/** | |
A form transport that accepts a form (Node, element, or selector) as the | |
request, and submits data to the form's `action` url using the form's `method` | |
via XHR. | |
This is basically a wrapper for the XHR transport that moves things around and | |
collects form data automatically. | |
Allows: | |
* Y.SM.io('#my-form', 'form').then(...); | |
* Y.SM.io.form.send(formEl, { method: 'post' }, function (err, response) {...}); | |
* Y.SM.io(formNode, { type: 'form' }, function (err, response)); | |
* etc | |
@module sm-io | |
@submodule sm-io-form | |
**/ | |
var xhrTransport = Y.SM.io.xhr, | |
form = Y.Object(xhrTransport); | |
form.prepare = function (request, callback) { | |
// default form from request.url to handle io('#frm', { type: 'form' }) | |
var form = request.form || request.url; | |
if (form && !form.nodeType) { | |
form = (typeof form === 'string') ? | |
Y.Selector.query(form, null, true) : | |
form._node; | |
} | |
if (form) { | |
// DON'T CHANGE THIS CONDITIONAL UNTIL YOU UNDERSTAND THIS COMMENT: | |
// if request.url is unset, default from the form. If request.url was | |
// set, then either request.form is set, which means request.url | |
// overrides form.action, OR request.form is unset, which means the | |
// form was identified via request.url, so the actual url needs to be | |
// set from form.action. | |
if (!request.url || !request.form) { | |
request.url = form.action || Y.config.doc.location.href; | |
} | |
if (!request.method) { | |
request.method = form.method; | |
} | |
} | |
// TODO: check the action url of the form vs the page url and fork to xdr | |
// if necessary? | |
return xhrTransport.prepare.call(this, request, callback); | |
}; | |
Y.SM.io.form = form; | |
}, '@VERSION@', {"requires": ["sm-io-xhr-form-data"]}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
YUI.add('sm-io-core', function (Y) { | |
/** | |
Provides a standard `Y.SM.io` method that wraps raw transport calls, returning | |
promises representing the response. | |
Relies on the transports registered on the `Y.SM.io` namespace. | |
Transports are objects with a `send` method that take a _request_, in whatever | |
form makes sense to them, optional _config_ object, and a callback as | |
parameters. The callback should be passed two parameters: | |
1. an error indicator | |
2. the response data | |
Successful transactions should pass `null` to the first parameter. | |
@module sm-io | |
@submodule sm-io-core | |
@for YUI | |
**/ | |
var isObject = Y.Lang.isObject, | |
isArray = Y.Lang.isArray; | |
/** | |
Make a request to a resource. The type of resource is determined by the | |
_request_ object's `type` property. The default `type` is "xhr". A transport | |
of the requested `type` will be used to send the request, and a Promise object | |
will be returned to represent the transaction and response. | |
Different transports may return different transaction objects, but they will | |
all implement a `then` method to get the results of the transaction. | |
To receive notification of the transaction completion, you may: | |
* pass a direct transport callback to the _callback_ param | |
* call the `then()` method of the returned promise | |
* pass success and failure callbacks in the config as | |
``` | |
Y.SM.io({ | |
... | |
success: successFn, // signature successFn(data) | |
failure: failureFn // signature failureFn(err) | |
}); | |
``` | |
If passing a direct transport callback, it must have a signature | |
`callback(err, data)`. If the transaction completed successfully, the first | |
argument will be `null`. | |
Because url-based transports are common, if the first parameter is a string, | |
the second param will be used as the request configuration object if it's an | |
object. If so, the third arg will be treated as the callback. | |
@method SM.io | |
@param {Object} request Configuration object describing a request | |
@param {Function} [callback] Callback to be passed directly to transport | |
@return {Promise} | |
**/ | |
function io(request, callback, _callback) { | |
var transport, Transaction, trx; | |
// Convenience for XHR use | |
if (typeof request === 'string') { | |
if (callback && typeof callback === 'object') { | |
request = Y.merge(callback); | |
request.url = request; | |
callback = _callback; | |
} else { | |
request = { url: request }; | |
} | |
} | |
transport = io[request.type] || io[io._defaultType]; | |
Transaction = transport.Transaction; | |
if (Transaction) { | |
trx = new Transaction(request, callback, transport); | |
} else { | |
// No Transaction class defined for the transport, so fall back to a | |
// basic promise. | |
trx = new Y.Promise(function (resolve, reject) { | |
transport.send(request, function (err, response) { | |
if (err) { | |
reject(err); | |
} else { | |
resolve(response); | |
} | |
if (callback) { | |
callback(err, response); | |
} | |
}); | |
}); | |
} | |
if (request.success || request.failure) { | |
trx.then(request.success, request.failure); | |
} | |
return trx; | |
} | |
/** | |
Name of the default transport. | |
@property SM.io._defaultType | |
@type {String} | |
@default "xhr" | |
@protected | |
**/ | |
io._defaultType = 'xhr'; | |
// Preserve any existing transports | |
Y.namespace('SM').io = Y.mix(io, Y.SM.io); | |
}, "", { requires: [ "promise" ] }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"sm-io": { | |
"use": [ | |
"sm-io-core", | |
"sm-io-xhr-transport", | |
"sm-io-xdr-transport", | |
"sm-io-xhr-transaction" | |
] | |
}, | |
"sm-io-xhr": { | |
"use": [ | |
"sm-io-xhr-transport", | |
"sm-io-xhr-transaction" | |
] | |
}, | |
"sm-io-xdr": { | |
"use": [ | |
"sm-io-xdr-transport", | |
"sm-io-xhr-transaction" | |
] | |
}, | |
"sm-io-core": { | |
"requires": [ | |
"promise" | |
] | |
}, | |
"sm-io-xhr-transport": { | |
"requires": [ | |
"sm-io-core", | |
"json-parse" | |
] | |
}, | |
"sm-io-xhr-transaction": { | |
"requires": [ | |
"querystring-stringify", | |
"sm-io-xhr-transport" | |
] | |
}, | |
"sm-io-xdr-transport": { | |
"requires": [ | |
"sm-io-xhr-transport" | |
] | |
}, | |
"sm-io-form": { | |
"requires": [ | |
"sm-io-xhr-form-data" | |
] | |
}, | |
"sm-io-xhr-form-data": { | |
"requires": [ | |
"sm-io-xhr-transaction", | |
"gallery-sm-dom-form-values" | |
] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
YUI.add('sm-io-xdr-transport', function (Y, NAME) { | |
/*global XMLHttpRequest:true*/ | |
/** | |
An XMLHttpRequest level 2 transport for Y.SM.io. This will handle xdr requests. | |
@module sm-io | |
@submodule sm-io-xdr | |
**/ | |
var win = Y.config.win, | |
xhrClass = win && win.XMLHttpRequest, | |
xdrClass = win && win.XDomainRequest, | |
CORS = xhrClass && ('withCredentials' in (new XMLHttpRequest())), | |
xhrTransport = Y.SM.io.xhr, | |
xdr = Y.Object(xhrTransport); | |
Y.mix(xdr, { | |
send: function (request, callback) { | |
var connection; | |
try { | |
connection = this.connect(request, callback); | |
if (connection) { | |
if (connection && request.xdrCredentials) { | |
connection.withCredentials = true; | |
} | |
this.setHeaders(connection, request); | |
if (request.data) { | |
connection.send(request.data); | |
} else { | |
// Broken out to avoid IE sending 'undefined' etc | |
connection.send(); | |
} | |
if (request.sync) { | |
this._handleResponse(connection, callback, request); | |
} | |
} else { | |
callback(new Error("Could not create XDR connection")); | |
} | |
} | |
catch (e) { | |
// Exceptions may be due to browsers that don't support XHR level 2. | |
// Retry with the xdr-flash transport if it's available | |
// TODO: is the onreadystatechange flush/abort necessary or just | |
// useful in YUI's IO for its transaction `end` method logic. | |
if (connection) { | |
if (xhrClass) { | |
connection.onreadystatechange = null; | |
} else { | |
// IE with ActiveXObject throws a "Type Mismatch" error if | |
// onreadystatechange is set to null. | |
// TODO: can we call abort() on all connections? | |
connection.abort(); | |
} | |
} | |
if (Y.SM.io['flash-xdr']) { | |
return Y.SM.io['flash-xdr'].send(request, callback); | |
} | |
callback(e); | |
} | |
return connection; | |
}, | |
defaultHeaders: function (connection, request) { | |
var headers = request.headers; | |
// Default behavior is to omit the X-Requested-With header to avoid | |
// the preflight OPTIONS request. | |
if (request.allowXDRPreflight | |
|| (headers && headers['X-Requested-With'])) { | |
connection.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); | |
} | |
if (request.method === 'POST' || request.method === 'PUT') { | |
connection.setRequestHeader('Content-Type', | |
'application/x-www-form-urlencoded; charset=UTF-8'); | |
} | |
}, | |
Connection: (!CORS && xdrClass) || xhrClass | |
}, true); | |
Y.SM.io.xdr = xdr; | |
}, '@VERSION@', {"requires": ["sm-io-xhr-transport"]}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
YUI.add('sm-io-xhr-form-data', function (Y, NAME) { | |
/** | |
Adds support for form and object serialization, ready to send via XHR. | |
@module sm-io | |
@submodule sm-io-xhr-form-data | |
**/ | |
var init = Y.SM.io.xhr.Transaction.prototype._init; | |
Y.SM.io.xhr.Transaction.prototype._init = function () { | |
var request = this.request; | |
if (request && request.form) { | |
// Bummer that this will make for two merge() calls :( | |
this.request = request = Y.merge(request); | |
request.data = | |
Y.DOM.formToObject(request.form, request.includeDisabled); | |
} | |
return init.call(this, request); | |
}; | |
}, '@VERSION@', {"requires": ["sm-io-xhr-transaction", "gallery-sm-dom-form-values"]}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
YUI.add('sm-io-xhr-transaction', function (Y) { | |
/** | |
A class for wrapping the transaction details for and providing a | |
customization point to add/extend functionality for IO transactions. | |
@module sm-io | |
@submodule sm-io-xhr-transaction | |
**/ | |
var isObject = Y.Lang.isObject, | |
toUrl = Y.QueryString.stringify; | |
/** | |
A class for wrapping the transaction details for and providing a | |
customization point to add/extend functionality for IO transactions. | |
@class SM.io.Transaction | |
@constructor | |
@param {String|Object} resource The target of the request (e.g. a url string) | |
@param {Object} [config] Request configuration | |
@param {Function} [callback] Callback to be passed directly to transport | |
@param {Object} transport The transport being used to make the request | |
**/ | |
function Transaction(request, callback, transport) { | |
// The transport is stored as a property to allow other transports to use | |
// this same class if little/none of the default logic needs overriding. | |
this.transport = transport; | |
this.request = request; | |
this.callback = callback; | |
this._init(); | |
// TODO: better name for this config property? | |
if (!request.lazy) { | |
this.send(); | |
} | |
} | |
Y.mix(Transaction.prototype, { | |
/** | |
Serializes the request data from an object to a url encoded string and adds | |
it to the url for GET requests. Default the request method to GET. | |
@method _init | |
@protected | |
**/ | |
_init: function () { | |
var request = this.request = Y.merge(this.request), | |
url = request.url || (request.url = ''), | |
data = request.data; | |
request.method = (request.method || 'GET').toUpperCase(); | |
if (data) { | |
if (typeof data === 'object') { | |
data = toUrl(data); | |
} | |
if (request.method !== 'PUT' && request.method !== 'POST') { | |
request.url += ((url.indexOf('?') > -1) ? '&' : '?') + data; | |
request.data = null; | |
} else { | |
request.data = data; | |
} | |
} | |
}, | |
/** | |
Attaches _onFulfilled_ and/or _onRejected_ callbacks to the response | |
promise. If the request hasn't been sent to the transport yet, it is now. | |
The promise resulting from the `then()` call is returned to allow chaining. | |
@method then | |
@param {Function} onFulfilled Callback to execute with response data | |
@param {Function} onRejected Callback to execute with err data | |
@return {Promise} | |
**/ | |
then: function (onFulfilled, onRejected) { | |
return this.send().then(onFulfilled, onRejected); | |
}, | |
/** | |
Calls the transport's `send()` method with the transaction's request and | |
(optionally) callback. | |
Creates the following instance properties: | |
* `response` - the Promise for the response data passed to the transport | |
callback | |
* `_connection` - the value returned from by the `transport.send()` method | |
If `send()` has been called before, it does NOT call `transport.send()` | |
again. | |
Returns the response promise. | |
@method send | |
@return {Promise} | |
**/ | |
send: function () { | |
var trx = this; | |
if (!trx.response) { | |
trx.response = new Y.Promise(function (resolve, reject) { | |
trx._connection = | |
trx.transport.send(trx.request, function (err, response) { | |
// Resolving the promise before calling the callback | |
// because promise callbacks are executed async, so the | |
// promise state will be updated before the callback is | |
// called, but the callback will be called before the | |
// promise subscribers. | |
if (err) { | |
resolve(response); | |
} else { | |
reject(err); | |
} | |
if (trx.callback) { | |
trx.callback(err, response); | |
} | |
}); | |
}); | |
} | |
return trx.response; | |
} | |
}, true); | |
// Assign the Transaction class to the xhr transport for use by Y.SM.io() | |
Y.SM.io.xhr.Transaction = Transaction; | |
}, '', { requires: ["promise", "sm-io-xhr"] }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global XMLHttpRequest:true, ActiveXObject:true*/ | |
YUI.add('sm-io-xhr', function (Y) { | |
/*global XMLHttpRequest:true, ActiveXObject:true*/ | |
/** | |
An XMLHttpRequest transport for Y.SM.io. This will be the default transport. | |
@module sm-io | |
@submodule sm-io-xhr | |
**/ | |
var win = Y.config.win, | |
Connection = win && win.XMLHttpRequest; | |
if (!Connection && win.ActiveXObject) { | |
Connection = function () { | |
return new ActiveXObject('Microsoft.XMLHTTP'); | |
}; | |
} | |
Y.namespace('SM.io').xhr = { | |
/** | |
Send a request to a URL via XMLHttpRequest. Supported configurations are: | |
* url - (string) REQUIRED the destination of the connection | |
* method - (string) HTTP method to use (defaults to GET) | |
* data - (string) data to send in the request. | |
* headers - map of header name => value | |
* sync - (boolean) make the request synchronously | |
* jsonReviver - (function) passed to `JSON.parse(responseText, HERE)` | |
The callback signature is `callback(err, data)`. If an error occurred, it | |
will be passed as the first parameter. If no error occurred, `null` will be | |
passed as the first param, and the response data as the second param. | |
If the response is JSON data (identified by response header | |
Content-Type=application/json), it will be parsed, and the raw data | |
returned. | |
Returns the generated XMLHttpRequest object. | |
@method send | |
@param {Object} request Request configurations for the transaction | |
@param {Function} [callback] Callback to notify on completion or error | |
@return {XMLHttpRequest} | |
**/ | |
send: function (request, callback) { | |
var connection; | |
try { | |
connection = this.connect(request, callback); | |
if (connection) { | |
if (request.data) { | |
connection.send(request.data); | |
} else { | |
// Broken out to avoid IE sending 'undefined' etc | |
connection.send(); | |
} | |
if (request.sync) { | |
this._handleResponse(connection, callback, request); | |
} | |
} else if (callback) { | |
callback(new Error("Could not create XHR connection")); | |
} | |
} | |
catch (e) { | |
if (callback) { | |
callback(e); | |
} | |
} | |
return connection; | |
}, | |
/** | |
Creates and `open()`s an XMLHttpRequest (or ActiveXObject on old IE) based | |
on the url and configuration supplied. If not a synchronous XHR, the | |
callback will be bound to the XHR's `onreadystatechange`. | |
@method connect | |
@param {Object} request Request configurations for the transaction | |
@param {Function} [callback] Callback to notify on completion or error | |
@return {XMLHttpRequest} | |
**/ | |
connect: function (request, callback) { | |
if (!request.url) { | |
return null; | |
} | |
var connection = new this.Connection(); | |
if (connection) { | |
if (!request.sync) { | |
connection.onreadystatechange = | |
this._bindNotifier(callback, request); | |
} | |
connection.open(request.method, request.url, !request.sync); | |
this.setHeaders(connection, request); | |
} | |
return connection; | |
}, | |
/** | |
Adds request headers to the XMLHttpRequest object. | |
@method setHeaders | |
@param {XMLHttpRequest} connection The XHR instance | |
@param {Object} request The request configuration | |
**/ | |
setHeaders: function (connection, request) { | |
var headers = request.headers, | |
header; | |
this.defaultHeaders(connection, request); | |
if (headers) { | |
for (header in headers) { | |
// Only set truthy headers | |
// TODO: This will break if a header value needs to be "". | |
// Numeric 0 should be passed as "0". | |
if (headers.hasOwnProperty(header) && headers[header]) { | |
connection.setRequestHeader(header, headers[header]); | |
} | |
} | |
} | |
}, | |
/** | |
Adds default headers to the XMLHttpRequest object. | |
@method defaultHeaders | |
@param {XMLHttpRequest} connection The XMLHttpRequest object | |
@param {Object} request Request configuration | |
**/ | |
defaultHeaders: function (connection, request) { | |
var headers = request.headers; | |
if (!headers || !('X-Requested-With' in request.headers)) { | |
connection.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); | |
} | |
if (request.method === 'POST' || request.method === 'PUT') { | |
connection.setRequestHeader('Content-Type', | |
'application/x-www-form-urlencoded; charset=UTF-8'); | |
} | |
}, | |
/** | |
Returns a function to relay notification of completion (`readyState = 4`) | |
to the `_handleResponse` function, which in turn will relay to the callback. | |
@method _bindNotifier | |
@param {Function} callback Callback function passed to `send()` | |
@param {Function} request Request configuration | |
@return {Function} | |
@protected | |
**/ | |
_bindNotifier: function (callback, request) { | |
var transport = this; | |
return function () { | |
// `this` is the XHR object | |
if (this.readyState === 4) { | |
transport._handleResponse(this, callback, request); | |
} | |
}; | |
}, | |
/** | |
Relays success of failure of the XHR transaction to the supplied callback. | |
Response statuses 200-299, 304, and 1223 are considered successful. Others | |
as failures. | |
If the response data is JSON (identified by the response header | |
"Content-Type=application/json"), it will be parsed before sending to the | |
callback. The callback will be called with the XHR object as `this`. | |
If a `config.jsonReviver` is supplied, it will be passed to the | |
`JSON.parse()` method. | |
@method _handleResponse | |
@param {XMLHttpRequest} connection The XHR object | |
@param {Function} callback Callback to notify of success or failure | |
@param {Function} request Request configuration | |
**/ | |
_handleResponse: function (connection, callback, request) { | |
var status, response, responseType; | |
// Noted in IO source that FF throws when accessing status | |
// property from aborted xhr. | |
try { | |
status = connection.status; | |
} catch (e) { | |
status = 0; | |
} | |
// IE reports HTTP 204 as 1223 | |
if (status >= 200 | |
&& (status < 300 || status === 304 || status === 1223)) { | |
response = connection.response || connection.responseText; | |
responseType = connection.getResponseHeader('Content-Type') || ''; | |
if (responseType.indexOf('application/json') === 0) { | |
try { | |
response = Y.JSON.parse(response, request.jsonReviver); | |
} | |
catch (e) { | |
callback.call(connection, e); | |
} | |
} | |
callback.call(connection, null, response); | |
} | |
}, | |
Connection: Connection | |
}; | |
}, "", { requires: [ "json-parse" ] }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment