milo

createPromiseOverride

function
 createPromiseOverride() 

Creates a function which is used to override standard promise behaviour and allow promise instances
created to maintain a reference to the request object no matter if .then() or .catch() is called.

function createPromiseOverride(functionName) {
    return function() {
        var promise = Promise.prototype[functionName].apply(this, arguments);
        keepRequestObject(promise, this._request);
        return promise;
    };
}


function request(url, opts, callback) {
    opts.url = url;
    opts.contentType = opts.contentType || 'application/json;charset=UTF-8';

    if (_messenger) request.postMessageSync('request', { options: opts });

    var req = new XMLHttpRequest();
    req.open(opts.method, opts.url, true);
    req.setRequestHeader('Content-Type', opts.contentType);
    setRequestHeaders(req, opts.headers);

    req.timeout = opts.timeout || config.request.defaults.timeout;
    req.onloadend = req.ontimeout = req.onabort = onReady;

    var xPromise = _createXPromise(req);

    req.send(JSON.stringify(opts.data));
    req[config.request.optionsKey] = opts;

    if (opts.trackCompletion !== false) _pendingRequests.push(req);

    return xPromise.promise;

    function onReady(e) {
        _onReady(req, callback, xPromise, e.type);
    }
}


function _createXPromise(request) {
    var resolvePromise, rejectPromise;
    var promise = new Promise(function(resolve, reject) {
        resolvePromise = resolve;
        rejectPromise = reject;
    });

    keepRequestObject(promise, request);
    promise.catch(_.noop); // Sometimes errors are handled within callbacks, so uncaught promise error message should be suppressed.

    return {
        promise: promise,
        resolve: resolvePromise,
        reject: rejectPromise
    };
}

// Ensures that the promise (and any promises created when calling .then/.catch) has a reference to the original request object
function keepRequestObject(promise, request) {
    promise._request = request;
    promise.then = promiseThen;
    promise.catch = promiseCatch;

    return promise;
}


function setRequestHeaders(req, headers) {
    if (headers)
        _.eachKey(headers, function(value, key) {
            req.setRequestHeader(key, value);
        });
}

function _onReady(req, callback, xPromise, eventType) {
    if (req.readyState != 4) return;
    if (req[config.request.completedKey]) return;

    _.spliceItem(_pendingRequests, req);

    var error;
    try {
        if ( req.status >= 200 && req.status < 400 ) {
            try {
                postMessage('success');
                callback && callback(null, req.responseText, req);
            } catch(e) { error = e; }
            xPromise.resolve(req.responseText);
        }
        else {
            var errorReason = req.status || eventType;
            try {
                postMessage('error');
                postMessage('error' + errorReason);
                callback && callback(errorReason, req.responseText, req);
            } catch(e) { error = e; }
            xPromise.reject({ reason: errorReason, response: req.responseText });
        }
    } catch(e) {
        error = error || e;
    } finally {
        req[config.request.completedKey] = true;
    }

    // not removing subscription creates memory leak, deleting property would not remove subscription
    req.onloadend = req.ontimeout = req.onabort = undefined;

    if (!_pendingRequests.length)
        postMessage('requestscompleted');

    if (error) {
        var errObj = new Error('Exception: ' + error);
        logger.error(error.stack);
        throw errObj;
    }

    function postMessage(msg) {
        if (_messenger) request.postMessage(msg,
            { status: status, response: req.responseText });
    }
}


_.extend(request, {
    get: request$get,
    post: request$post,
    json: request$json,
    jsonp: request$jsonp,
    file: request$file,
    useMessenger: request$useMessenger,
    destroy: request$destroy,
    whenRequestsCompleted: whenRequestsCompleted
});


var _messenger;


function request$useMessenger() {
    _messenger = new Messenger(request, ['on', 'once', 'onSync', 'off', 'onMessages', 'offMessages', 'postMessage', 'postMessageSync']);
}


function request$get(url, callback) {
    return request(url, { method: 'GET' }, callback);
}


function request$post(url, data, callback) {
    return request(url, { method: 'POST', data: data }, callback);
}


function request$json(url, callback) {
    var promise = request(url, { method: 'GET' });

    var jsonPromise = promise.then(JSON.parse);

    if (callback)
        jsonPromise
        .then(function(data) {
            callback(null, data);
        }, function(errData) {
            callback(errData.reason, errData.response);
        });

    return jsonPromise;
}


var jsonpOptions = { method: 'GET', jsonp: true };
function request$jsonp(url, callback) {
    var script = document.createElement('script'),
        xPromise = _createXPromise(script),
        head = window.document.head,
        uniqueCallback = config.request.jsonpCallbackPrefix + uniqueId();

    var opts = _.extend({ url: url }, jsonpOptions);
    if (_messenger) request.postMessageSync('request', { options: opts });

    if (! _.isEqual(_.omitKeys(opts, 'url'), jsonpOptions))
        logger.warn('Ignored not allowed request options change in JSONP request - only URL can be changed');

    var timeout = setTimeout(function() {
        var err = new Error('No JSONP response or no callback in response');
        _onResult(err);
    }, config.request.jsonpTimeout);

    window[uniqueCallback] = _.partial(_onResult, null);

    _pendingRequests.push(window[uniqueCallback]);

    script.type = 'text/javascript';
    script.src = opts.url + (opts.url.indexOf('?') == -1 ? '?' : '&') + 'callback=' + uniqueCallback;

    head.appendChild(script);

    return xPromise.promise;


    function _onResult(err, result) {
        _.spliceItem(_pendingRequests, window[uniqueCallback]);
        var error;
        try {
            postMessage(err ? 'error' : 'success', err, result);
            if (err) {
                logger.error('No JSONP response or timeout');
                postMessage('errorjsonptimeout', err);
            }
            callback && callback(err, result);
        }
        catch(e) { error = e; }
        if (err) xPromise.reject(err);
        else xPromise.resolve(result);

        cleanUp();
        if (!_pendingRequests.length)
            postMessage('requestscompleted');

        if (error) throw error;
    }


    function cleanUp() {
        clearTimeout(timeout);
        head.removeChild(script);
        delete window[uniqueCallback];
    }


    function postMessage(msg, status, result) {
        if (_messenger) request.postMessage(msg,
            { status: status, response: result });
    }
}


function request$file(opts, fileData, callback, progress) {
    if (typeof opts == 'string')
        opts = { method: 'POST', url: opts };

    opts.method = opts.method || 'POST';
    opts.file = true;

    if (_messenger) request.postMessageSync('request', { options: opts });

    var req = new XMLHttpRequest();
    if (progress) req.upload.onprogress = progress;

    req.open(opts.method, opts.url, true);
    setRequestHeaders(req, opts.headers);

    req.timeout = opts.timeout || config.request.defaults.timeout;
    req.onloadend = req.ontimeout = req.onabort = onReady;

    var xPromise = _createXPromise(req);

    if (opts.binary)
        req.send(fileData);
    else {
        var formData = new FormData();
        formData.append('file', fileData);
        req.send(formData);
    }

    req[config.request.optionsKey] = opts;

    if (opts.trackCompletion !== false) _pendingRequests.push(req);

    return xPromise.promise;

    function onReady(e) {
        if (progress) req.upload.onprogress = undefined;
        _onReady(req, callback, xPromise, e.type);
    }
}


function request$destroy() {
    if (_messenger) _messenger.destroy();
    request._destroyed = true;
}


function whenRequestsCompleted(callback, timeout) {
    callback = _.once(callback);
    if (timeout)
        _.delay(callback, timeout, 'timeout');

    if (_pendingRequests.length)
        _messenger.once('requestscompleted', callback);
    else
        _.defer(callback);
}