Skip to content

Instantly share code, notes, and snippets.

@lsmith
Last active December 15, 2015 16:39
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lsmith/5290799 to your computer and use it in GitHub Desktop.
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.
<!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>
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"]});
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" ] });
{
"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"
]
}
}
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"]});
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"]});
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"] });
/*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