Source

frontend/controllers/event_controller.js

/* This file is part of Ezra Bible App.

   Copyright (C) 2019 - 2023 Ezra Bible App Development Team <contact@ezrabibleapp.net>

   Ezra Bible App is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 2 of the License, or
   (at your option) any later version.

   Ezra Bible App is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with Ezra Bible App. See the file LICENSE.
   If not, see <http://www.gnu.org/licenses/>. */


require('../event_types.js');

/**
 * This controller implements pub/sub pattern and serves as a broker (event bus) between
 * event emitter (publisher) and event consumer (subscriber).
 * https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
 * 
 * @module eventController
 * @category Controller
 */

var subscribers = {};

function notCreated(event) {
  return !(event in subscribers);
}

/**
 * @callback SubscribedCallback
 * @param {*} payload Optional payload
 */

/**
 * @typedef {Object} Subscription
 * @property {function} remove Function to call to unsubscribe from the event
 */

/**
 * This function subscribes callback to be called when future events are published.
 * The subscriber is handled with priority by inserting it on top of the subscriber list.
 * @param {EzraEvent} event Event key to be notified on publish
 * @param {SubscribedCallback} callback Function to call when when event is published
 * @returns {Subscription} subscription
 */
module.exports.subscribePrioritized = function (event, callback) {
  return this.subscribe(event, callback, true);
};

/**
 * This function subscribes on multiple events, so that the callback function gets
 * called when any of those future events are published.
 * @param {SubscribedCallback} callback Function to call when events are published
 * @returns {Subscription} subscription
 */
module.exports.subscribeMultiple = function (eventList, callback) {
  if (typeof(eventList) != 'object') {
    throw new Error('subscribeMultiple expects array as first parameter!');
  }

  let returnObjects = [];

  eventList.forEach((ezraEvent) => {
    let subscription = this.subscribe(ezraEvent, callback);
    returnObjects.push(subscription);
  });

  return returnObjects;
};

/**
 * This function subscribes callback to be called when future events are published
 * @param {EzraEvent} event Event key to be notified on publish
 * @param {SubscribedCallback} callback Function to call when when event is published
 * @param {Boolean} prioritize Whether or not this callback should be prioritized when the event is published
 * @returns {Subscription} subscription
 */
module.exports.subscribe = function (event, callback, prioritize=false) {
  if (notCreated(event)) {
    subscribers[event] = [];
  }

  var index = null;
  
  if (prioritize) {
    index = subscribers[event].unshift(callback) - 1;
  } else {
    index = subscribers[event].push(callback) - 1;
  }
  
  return {
    remove: () => {
      delete subscribers[event][index];
    }
  };
};

/**
 * This function calls all callbacks that subscribed to the specific event
 * @param {EzraEvent} event Event key to be notified
 * @param {*} payload 
 * @param {Boolean} waitAsync Whether or not the function should wait for the subscribers (default: false)
 * @returns {Array} Array of callback results
 */
module.exports.publish = async function (event, payload=undefined, waitAsync=false) {
  var results = [];

  if (notCreated(event)) {
    return results;
  }
  

  for (let subscribedCallback of subscribers[event]) {
    if (typeof subscribedCallback === 'function') {
      const r = waitAsync ? await subscribedCallback(payload) : subscribedCallback(payload);
      results.push(r);
    }
  }

  return results;
};

/**
 * This function calls all callbacks that subscribed to the specific event and awaits for all callbacks to finish
 * @param {EzraEvent} event Event key to be notified
 * @param {*} payload 
 * @returns {Promise<Array>} Promise that resolves to array of callback results
 */
module.exports.publishAsync = async function (event, payload=undefined) {

  const promisifiedResults = await this.publish(event, payload, true);

  var results = [];
  
  try {
    // TODO: It's better to use Promise.allSettled()
    results = await Promise.all(promisifiedResults);
  } catch(error) {
    console.log('One of the callbacks was rejected:', error);
    console.trace();
  }

  return results;
};

/**
 * This function unsubscribes all callbacks from the specific event
 * @param { EzraEvent | RegExp } event Event key to unsubscribe, can be a RegExp
 */
module.exports.unsubscribeAll = function (event) {
  if (typeof event === 'string') {
    subscribers[event] = [];

  } else if (event instanceof RegExp) {
    const allEvents = Object.keys(subscribers);
    
    allEvents.forEach(key => {
      if (event.test(key)) {
        subscribers[key] = [];
      }
    });
  }
};