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
	 * @returns {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 subscribeOnce = kiss.pubsub.subscribeOnce
const unsubscribe = kiss.pubsub.unsubscribe