milojs/milo-core

View on GitHub
lib/messenger/m_source.js

Summary

Maintainability
A
1 hr
Test Coverage
'use strict';

var Mixin = require('ml-mixin')
    , MessengerAPI = require('./m_api')
    , _ = require('protojs')
    , check = require('ml-check')
    , Match = check.Match;


/**
 * `milo.classes.MessageSource`
 * An abstract class (subclass of [Mixin](../abstract/mixin.js.html)) for connecting [Messenger](./index.js.html) to external sources of messages (like DOM events) and defining higher level messages.
 * An instance of MessageSource can either be passed to Messenger constructor or later using `_setMessageSource` method of Messenger. Once set, MessageSource of Messenger cannot be changed.
 */
var MessageSource = _.createSubclass(Mixin, 'MessageSource', true);

module.exports = MessageSource;


/**
 * ####MessageSource instance methods####
 *
 * - [init](#init) - initializes messageSource - called by Mixin superclass
 * - [setMessenger](#setMessenger) - connects Messenger to MessageSource, is called from `init` or `_setMessageSource` methods of [Messenger](./index.js.html).
 * - [onSubscriberAdded](#onSubscriberAdded) - called by Messenger to notify when the first subscriber for an internal message was added, so MessageSource can subscribe to source
 * - [onSubscriberRemoved](#onSubscriberRemoved) - called by Messenger to notify when the last subscriber for an internal message was removed, so MessageSource can unsubscribe from source
 * - [dispatchMessage](#dispatchMessage) - dispatches source message. MessageSource subclass should implement mechanism when on actual source message this method is called.
 *
 * Methods below should be implemented in subclass:
 *
 * - [trigger](#trigger) - triggers messages on the source (an optional method)
 * - [addSourceSubscriber](#addSourceSubscriber) - adds listener/subscriber to external message
 * - [removeSourceSubscriber](#removeSourceSubscriber) - removes listener/subscriber from external message
 */
_.extendProto(MessageSource, {
    init: init,
    destroy: MessageSource$destroy,
    setMessenger: setMessenger,
    onSubscriberAdded: onSubscriberAdded,
    onSubscriberRemoved: onSubscriberRemoved, 
    dispatchMessage: dispatchMessage,
    postMessage: postMessage,
    _prepareMessengerAPI: _prepareMessengerAPI,

    // Methods below must be implemented in subclass
    trigger: toBeImplemented,
    addSourceSubscriber: toBeImplemented,
    removeSourceSubscriber: toBeImplemented
});


/**
 * MessageSource instance method.
 * Called by Mixin constructor.
 * MessageSource constructor should be passed the same parameters as this method signature.
 * If an instance of [MessengerAPI](./m_api.js.html) is passed as the third parameter, it extends MessageSource functionality to allow it to define new messages, to filter messages based on their data and to change message data. See [MessengerAPI](./m_api.js.html).
 *
 * @param {Object} hostObject Optional object that stores the MessageSource on one of its properties. It is used to proxy methods of MessageSource.
 * @param {Object[String]} proxyMethods Optional map of method names; key - proxy method name, value - MessageSource's method name.
 * @param {MessengerAPI} messengerAPI Optional instance of MessengerAPI.
 */
function init(hostObject, proxyMethods, messengerAPI) {
    this._prepareMessengerAPI(messengerAPI);
}


/**
 * Destroys message source
 */
function MessageSource$destroy() {
    if (this.messengerAPI)
        this.messengerAPI.destroy();
}


/**
 * MessageSource instance method.
 * Sets reference to Messenger instance.
 *
 * @param {Messenger} messenger reference to Messenger instance linked to this MessageSource
 */
function setMessenger(messenger) {
    _.defineProperty(this, 'messenger', messenger);
}


/**
 * MessageSource instance method.
 * Prepares [MessengerAPI](./m_api.js.html) passed to constructor by proxying its methods to itself or if MessengerAPI wasn't passed defines two methods to avoid checking their availability every time the message is dispatched.
 *
 * @private
 * @param {MessengerAPI} messengerAPI Optional instance of MessengerAPI
 */
function _prepareMessengerAPI(messengerAPI) {
    check(messengerAPI, Match.Optional(MessengerAPI));

    if (! messengerAPI)
        messengerAPI = new MessengerAPI;

    _.defineProperty(this, 'messengerAPI', messengerAPI);
}


/**
 * MessageSource instance method.
 * Subscribes to external source using `addSourceSubscriber` method that should be implemented in subclass.
 * This method is called by [Messenger](./index.js.html) when the first subscriber to the `message` is added.
 * Delegates to supplied or default [MessengerAPI](./m_api.js.html) for translation of `message` to `sourceMessage`. `MessageAPI.prototype.addInternalMessage` will return undefined if this `sourceMessage` was already subscribed to to prevent duplicate subscription.
 *
 * @param {String} message internal Messenger message that has to be subscribed to at the external source of messages.
 */
function onSubscriberAdded(message) {
    var newSourceMessage = this.messengerAPI.addInternalMessage(message);
    if (typeof newSourceMessage != 'undefined')
        this.addSourceSubscriber(newSourceMessage);
}


/**
 * MessageSource instance method.
 * Unsubscribes from external source using `removeSourceSubscriber` method that should be implemented in subclass.
 * This method is called by [Messenger](./index.js.html) when the last subscriber to the `message` is removed.
 * Delegates to supplied or default [MessengerAPI](./m_api.js.html) for translation of `message` to `sourceMessage`. `MessageAPI.prototype.removeInternalMessage` will return undefined if this `sourceMessage` was not yet subscribed to to prevent unsubscription without previous subscription.
 *
 * @param {String} message internal Messenger message that has to be unsubscribed from at the external source of messages.
 */
function onSubscriberRemoved(message) {
    var removedSourceMessage = this.messengerAPI.removeInternalMessage(message);
    if (typeof removedSourceMessage != 'undefined')
        this.removeSourceSubscriber(removedSourceMessage);
}


/**
 * MessageSource instance method.
 * Dispatches sourceMessage to Messenger.
 * Mechanism that calls this method when the source message is received should be implemented by subclass (see [DOMEventsSource](../components/msg_src/dom_events.js.html) for example).
 * Delegates to supplied or default [MessengerAPI](./m_api.js.html) to create internal message data (`createInternalData`) and to filter the message based on its data and/or message (`filterSourceMessage`).
 * Base MessengerAPI class implements these two methods in a trivial way (`createInternalData` simply returns external data, `filterSourceMessage` returns `true`), they are meant to be implemented by subclass.
 *
 * @param {String} sourceMessage source message received from external source
 * @param {Object} sourceData data received from external source
 */
function dispatchMessage(sourceMessage, sourceData) {
    var api = this.messengerAPI
        , internalMessages = api.getInternalMessages(sourceMessage);

    if (internalMessages) 
        internalMessages.forEach(function (message) {
            var internalData = api.createInternalData(sourceMessage, message, sourceData);

            var shouldDispatch = api.filterSourceMessage(sourceMessage, message, internalData);
            if (shouldDispatch) 
                this.postMessage(message, internalData);      
            
        }, this);
}


/**
 * Posts message on the messenger. This method is separated so specific message sources can make message dispatch synchronous by using `postMessageSync`
 * 
 * @param  {String} message
 * @param  {Object} data
 */
function postMessage(message, data) {
    this.messenger.postMessage(message, data);
}


function toBeImplemented() {
    throw new Error('calling the method of an absctract class');
}