Source

client/core/modules/pubsub.js

/**
 * 
 * ## A simple PubSub
 * 
 * 1. subscribe a function to a channel:
 * ```
 * let subscriptionId = kiss.pubsub.subscribe("MY_CHANNEL_NAME", (messageData) => { console.log(messageData) })
 * ```
 * 
 * 2. publish a message on a channel:
 * ```
 * kiss.pubsub.publish("MY_CHANNEL_NAME", {foo: "bar"})
 * ```
 * 
 * 3. unsubscribe a function:
 * ```
 * kiss.pubsub.unsubscribe(subscriptionId)
 * ```
 * 
 * 4. list all active subscriptions
 * ```
 * kiss.pubsub.list()
 * ```
 * 
 * @namespace
 * 
 */
kiss.pubsub = {
    subscriptions: {},

    channelsNotLogged: ["EVT_ROUTE_UPDATED", "EVT_CONTAINERS_RESIZED"],

    /**
     * Publish a message on a specific channel + on the global channel, if any
     * 
     * The global channel is the "*" channel, which is a special channel that will be called for all messages.
     * Sometimes, it can be convenient to observe all messages, without subscribing to a specific channel.
     * 
     * @param {string} channel - The channel name
     * @param {object} [messageData] - The data published into the channel
     */
    publish(channel, messageData) {
        let targetChannel = kiss.pubsub.subscriptions[channel]

        // If the channel doesn't exist, we publish only the global channel
        if (!targetChannel) {
            kiss.pubsub.publishOnGlobalChannel(messageData)
            return
        }

        if (!kiss.pubsub.channelsNotLogged.includes(channel.toUpperCase())) {
            log.info("kiss.pubsub - publish on channel: " + channel, messageData)
        }
        
        // Browse all the subscriptions of kiss.pubsub channel
        Object.keys(targetChannel).forEach(subscriptionId => {
            // Get the function to execute
            let fn = targetChannel[subscriptionId]
            
            try {
                // Pass the message data to the function
                fn(messageData)
            }
            catch(err) {
                log.err("kiss.pubsub - publish - Error with subscription id: " + subscriptionId, err)
            }
        })

        // Publish the message on the global channel
        kiss.pubsub.publishOnGlobalChannel(messageData)
    },

    /**
     * Publish a message on the global channel (*)
     * 
     * @param {*} messageData
     */
    publishOnGlobalChannel(messageData) {
        if (kiss.pubsub.subscriptions["*"]) {
            let globalChannel = kiss.pubsub.subscriptions["*"]
            Object.keys(globalChannel).forEach(subscriptionId => {
                // Get the function to execute
                let fn = globalChannel[subscriptionId]
                
                try {
                    // Pass the message data to the function
                    fn(messageData)
                }
                catch(err) {
                    log.err("kiss.pubsub - publish - Error with subscription id: " + subscriptionId, err)
                }
            })
        }        
    },

    /**
     * Subscribe a function to a channel
     * 
     * The global channel is the "*" channel, which is a special channel that will be called for all messages.
     * Sometimes, it can be convenient to observe all messages, without subscribing to a specific channel.
     * 
     * @param {string} channel - The channel name, or "*" to subscribe to all channels
     * @param {function} fn - The function to subscribe to the channel
     * @param {string} [description] - Optional description of the subscription
     * @returns {string} The subscription id
     */
    subscribe(channel, fn, description) {
        let subscriptionId = kiss.tools.shortUid()

        // If the channel doesn't exist yet, we create it
        if (!kiss.pubsub.subscriptions[channel]) kiss.pubsub.subscriptions[channel] = {}

        // Subscribe the function to the channel
        kiss.pubsub.subscriptions[channel][subscriptionId] = fn
        kiss.pubsub.subscriptions[channel][subscriptionId].description = description

        // Return the subscription id so we can use it to unsubscribe the function later
        return subscriptionId
    },

    /**
     * Subscribe a function to a channel, but unsubscribe it after the first call
     * 
     * @param {string} channel - The channel name
     * @param {function} fn - The function to subscribe to the channel
     * @param {string} [description] - Optional description of the subscription
     * @returns {string} The subscription id
     */
    subscribeOnce(channel, fn, description) {
        let subscriptionId = kiss.pubsub.subscribe(channel, fn, description)

        // Unsubscribe the function after the first call
        let originalFn = kiss.pubsub.subscriptions[channel][subscriptionId]
        kiss.pubsub.subscriptions[channel][subscriptionId] = function(messageData) {
            originalFn(messageData)
            kiss.pubsub.unsubscribe(subscriptionId)
        }

        return subscriptionId
    },

    /**
     * Unsubscribe a function from the pubsub
     * 
     * @param {string} subscriptionId - The id of the subscription to remove
     */
    unsubscribe(subscriptionId) {
        // Scan all the channels
        Object.keys(kiss.pubsub.subscriptions).forEach(channel => {
            let channelSubscriptions = kiss.pubsub.subscriptions[channel]

            // For each channel, scan all the subscriptions
            Object.keys(channelSubscriptions).forEach(channelSubscriptionId => {
                // If the subscription is found, we delete it
                if (channelSubscriptionId == subscriptionId) {
                    delete kiss.pubsub.subscriptions[channel][subscriptionId]
                    return
                }
            })
        })
    },

    /**
     * Return the number of subscriptions.
     * 
     * Mainly used for debugging (useful to track memory leaks).
     * 
     * @returns {number}
     */
    getCount() {
        let count = 0
        Object.keys(kiss.pubsub.subscriptions).forEach(channel => {
            let channelSubscriptions = kiss.pubsub.subscriptions[channel]
            Object.keys(channelSubscriptions).forEach(() => count++)
        })
        return count
    },

    /**
     * Get a subscription by id.
     * 
     * Mainly used for debug purpose, when you need to check which function is registered in the pubsub.
     * 
     * @param {string} subscriptionId - The id of the subscription to retrieve
     * @return {function} The subscribed function
     */
    get(subscriptionId) {
        let subscription

        // Scan all the channels
        Object.keys(kiss.pubsub.subscriptions).forEach(channel => {
            let channelSubscriptions = kiss.pubsub.subscriptions[channel]

            // For each channel, scan all the subscriptions
            Object.keys(channelSubscriptions).forEach(channelSubscriptionId => {
                // If the subscription is found, we return it
                if (channelSubscriptionId == subscriptionId) {
                    subscription = kiss.pubsub.subscriptions[channel][subscriptionId]
                }
            })
        })
        return subscription
    },

    /**
     * List all the subscriptions in the console.
     * 
     * Mainly used for debug purpose, when you need an overview of subscriptions.
     * 
     * @param {boolean} [showFunction] - If true, show the subscribed function
     */    
    list(showFunction) {
        let counter = 0

        // Scan all the channels
        Object.keys(kiss.pubsub.subscriptions).forEach(channel => {
            let channelSubscriptions = kiss.pubsub.subscriptions[channel]

            // For each channel, scan all the subscriptions
            Object.keys(channelSubscriptions).forEach(channelSubscriptionId => {
                let subscription = kiss.pubsub.subscriptions[channel][channelSubscriptionId]
                let description = kiss.pubsub.subscriptions[channel][channelSubscriptionId].description

                log("-----------------------------------------------------------------")
                log.info("Subscription " + counter.pad(5) + " - subscription id: " + channelSubscriptionId)
                if (description) log.ack("Description: ", description)
                if (showFunction) log.ack("Function: ", subscription)
                counter++
            })
        })
        log("Total number of subscriptions: " + counter)  
    }
};

// Shortcuts
const publish = kiss.pubsub.publish
const subscribe = kiss.pubsub.subscribe
const unsubscribe = kiss.pubsub.unsubscribe;