All Downloads are FREE. Search and download functionalities are using the official Maven repository.

joynr.messaging.channel.LongPollingChannelMessageReceiver.js Maven / Gradle / Ivy

There is a newer version: 1.4.0
Show newest version
/*jslint es5: true, nomen: true, node: true */
/*
 * #%L
 * %%
 * Copyright (C) 2011 - 2017 BMW Car IT GmbH
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
var Promise = require("../../../global/Promise");
var uuid = require("../../../lib/uuid-annotated");
var JoynrMessage = require("../JoynrMessage");
var JsonParser = require("../../../lib/JsonParser");
var MessagingQos = require("../MessagingQos");
var DiagnosticTags = require("../../system/DiagnosticTags");
var LoggerFactory = require("../../system/LoggerFactory");
var Util = require("../../util/UtilInternal");
var LongTimer = require("../../util/LongTimer");

var log = LoggerFactory.getLogger("joynr.messaging.LongPollingChannelMessageReceiver");
var storagePrefix = "joynr.channels";

/**
 * LongPollingChannelMessageReceiver handles the long poll to the bounce proxy.
 *
 * @name LongPollingChannelMessageReceiver
 * @constructor
 *
 * @param {Object}
 *            settings an object containing required input for LongPollingChannelMessageReceiver
 * @param {String}
 *            settings.bounceProxyUrl an object describing the bounce proxy URL
 * @param {CommunicationModule}
 *            settings.communicationModule the API required by the LongPollingChannelMessageReceiver to perform XMLHTTPRequests
 *            socket communication
 * @param {Object}
 *            settings.channelQos parameters specifying the qos requirements for the channel (creation)
 * @param {Number}
 *            settings.channelQos.creationTimeout_ms max time to create a channel
 * @param {Number}
 *            settings.channelQos.creationRetryDelay_ms retry interval after a failing channel creation
 *
 * @returns the LongPollingChannelMessageReceiver
 */
function LongPollingChannelMessageReceiver(settings) {
    this._persistency = settings.persistency;
    this._channelOpMessagingQos = new MessagingQos();
    this._communicationModule = settings.communicationModule;

    this._bounceProxyChannelBaseUrl = settings.bounceProxyUrl + "channels/";
    this._channelCreationTimeout_ms =
        settings.channelQos && settings.channelQos.creationTimeout_ms
            ? settings.channelQos.creationTimeout_ms
            : 1000 * 60 * 60 * 24; // default: 1 day
    this._channelCreationRetryDelay_ms =
        settings.channelQos && settings.channelQos.creationRetryDelay_ms
            ? settings.channelQos.creationRetryDelay_ms
            : 1000 * 30; // default: 30s
}

/**
 * Retrieve the receiverId for the given channelId
 *
 * @name LongPollingChannelMessageReceiver#getReceiverId
 * @function
 * @private
 */
LongPollingChannelMessageReceiver.prototype._getReceiverId = function(channelId) {
    var receiverId = this._persistency.getItem(storagePrefix + "." + channelId + ".receiverId");
    if (receiverId === undefined || receiverId === null) {
        receiverId = "tid-" + uuid();
        this._persistency.setItem(storagePrefix + "." + channelId + ".receiverId", receiverId);
    }
    return receiverId;
};

LongPollingChannelMessageReceiver.prototype._callCreate = function callCreate() {
    var that = this;
    var createChannelUrl = this._bounceProxyChannelBaseUrl + "?ccid=" + encodeURIComponent(this._channelId);
    var receiverId = this._getReceiverId(this._channelId);

    function callCreateOnSuccess(xhr) {
        that._channelUrl = xhr.getResponseHeader("Location");

        if (!that._channelUrl) {
            that._channelUrl = xhr.responseText;
        }

        log.debug("created channel with id " + that._channelId + " and url " + that._channelUrl);

        return that._channelUrl;
    }

    return this._communicationModule
        .createXMLHTTPRequest({
            // TODO: check why headers ist not an array
            type: "POST",
            url: createChannelUrl,
            headers: {
                "X-Atmosphere-tracking-id": receiverId
            },
            timeout: this._channelOpMessagingQos.ttl
        })
        .then(callCreateOnSuccess.bind(this));
};

LongPollingChannelMessageReceiver.prototype._logChannelCreationError = function(xhr) {
    log.error("error while creating channel on bounce proxy", {
        channelId: this._channelId,
        channelUrl: this._channelUrl,
        status: xhr.status,
        responseText: xhr.responseText
    });
};

LongPollingChannelMessageReceiver.prototype._createInternal = function createInternal(resolve, reject) {
    var that = this;

    function createInternalOnError(xhr, errorType) {
        that._logChannelCreationError(xhr);

        if (that._createChannelTimestamp + that._channelCreationTimeout_ms <= Date.now()) {
            reject(new Error("Error creating channel"));
        } else {
            LongTimer.setTimeout(that._createInternal.bind(that), that._channelCreationRetryDelay_ms, resolve, reject);
        }
    }

    this._callCreate()
        .then(resolve)
        .catch(createInternalOnError.bind(that));
};

/**
 * Stop long polling
 *
 * @name LongPollingChannelMessageReceiver#stop
 * @function
 */
LongPollingChannelMessageReceiver.prototype.stop = function stop() {
    this._communicationModule.atmosphere.unsubscribeUrl(this._channelUrl);
};

/**
 * Removes the channel with a given channel id
 *
 * @name LongPollingChannelMessageReceiver#clear
 * @function
 *
 * @returns {Object} a promise object for async event handling
 */
LongPollingChannelMessageReceiver.prototype.clear = function clear() {
    var that = this;

    function createXMLHTTPRequestOnError(xhr, errorType) {
        var errorString =
            "error while deleting channel on bounce proxy: " +
            errorType +
            (xhr.statusText ? ", " + xhr.statusText : "") +
            (xhr.status === 0 ? "" : ", HTTP" + xhr.status) +
            (xhr.responseText ? ", " + xhr.responseText : "");
        log.error(
            "error while deleting channel on bounce proxy",
            JSON.stringify(
                DiagnosticTags.forChannel({
                    channelId: that._channelId,
                    channelUrl: that._channelUrl,
                    status: xhr.status,
                    responseText: xhr.responseText
                })
            )
        );
        throw new Error(errorString);
    }

    function returnUndefined() {}

    log.debug("clearing channel with id " + that._channelId + " and url " + that._channelUrl);
    return that._communicationModule
        .createXMLHTTPRequest({
            type: "DELETE",
            url: that._channelUrl,
            timeout: that._channelOpMessagingQos.ttl
        })
        .then(returnUndefined) // required for some tests no idea why
        .catch(createXMLHTTPRequestOnError);
};

/**
 * Start long polling. Side effect: first makes sure that the channel exists on the bounceproxy
 *
 * @name LongPollingChannelMessageReceiver#start
 * @function
 *
 * @param {Function}
 *            onMessageCallback callback used to pass the received messages up the processing chain
 * @param {Function}
 *            onError callback used to inform about errors occurred during message recieve
 */
LongPollingChannelMessageReceiver.prototype.start = function start(onMessageCallback, onError) {
    var pollRequest;
    var receiverId = this._getReceiverId(this._channelId);

    this._communicationModule.atmosphere.unsubscribeUrl(this._channelUrl);

    pollRequest = {
        url: this._channelUrl,
        transport: "long-polling",
        contentType: "application/json",
        timeout: 30000,
        enableXDR: false,
        enableProtocol: false,
        readResponseHeaders: false,
        attachHeadersAsQueryString: false,
        dropAtmosphereHeaders: true,
        dropHeaders: false,
        logLevel: "debug",
        headers: {
            "X-Atmosphere-tracking-id": receiverId
        }
    };

    // Called whenever one or more new
    // messages are received
    // from the bounceproxy.
    pollRequest.onMessage = function onMessage(response) {
        var detectedTransport = response.transport,
            data,
            jsonParser,
            joynrMessage;

        try {
            if (response.status === 200) {
                data = response.responseBody;

                if (data.length > 0) {
                    jsonParser = new JsonParser(data);
                    while (jsonParser.hasNext) {
                        // pass the message on
                        joynrMessage = JoynrMessage.parseMessage(jsonParser.next);

                        log.info(
                            "received message with id " + joynrMessage.msgId + ": ",
                            JSON.stringify(DiagnosticTags.forJoynrMessage(joynrMessage))
                        );
                        onMessageCallback(joynrMessage);
                    }
                }
            }
        } catch (e) {
            log.debug("Exception while processing message: " + e.message);
            if (onError) {
                onError(e);
            }
        }
    };

    pollRequest.onError = function(response) {
        log.debug("Encountered atmosphere error :" + response.error);
        if (onError) {
            onError(response.error);
        }
    };

    this._communicationModule.atmosphere.subscribe(pollRequest);

    log.debug("starting to listen on channel with id " + this._channelId + " and url " + this._channelUrl);
};

/**
 * Create the given Channel on the bounceproxy
 *
 * @name LongPollingChannelMessageReceiver#create
 * @function
 *
 * @param {String}
 *            channelId the channelId that will be created, will be URI-Component-encoded
 * @returns {Object} a promise object for async event handling
 */
LongPollingChannelMessageReceiver.prototype.create = function(theChannelId) {
    var that = this;
    if (this._channelId !== undefined) {
        throw new Error(
            "LongPollingChannelMessageReceiver.create has already been called for channelId: " + theChannelId
        );
    }
    this._channelId = theChannelId;
    this._createChannelTimestamp = Date.now();

    function _callCreateOnError(xhr, errorType) {
        that._logChannelCreationError(xhr);
        if (that._createChannelTimestamp + that._channelCreationTimeout_ms <= Date.now()) {
            throw new Error("Error creating channel");
        } else {
            var deferred = Util.createDeferred();
            LongTimer.setTimeout(
                that._createInternal.bind(that),
                that._channelCreationRetryDelay_ms,
                deferred.resolve,
                deferred.reject
            );
            return deferred.promise;
        }
    }

    return this._callCreate().catch(_callCreateOnError);
};
module.exports = LongPollingChannelMessageReceiver;




© 2015 - 2025 Weber Informatics LLC | Privacy Policy