<commit_before>'use strict'

const C = require('../constants/constants')
const DistributedStateRegistry = require('../cluster/distributed-state-registry')
const SocketWrapper = require('../message/socket-wrapper')

class SubscriptionRegistry {
  /**
   * A generic mechanism to handle subscriptions from sockets to topics.
   * A bit like an event-hub, only that it registers SocketWrappers rather
   * than functions
   *
   * @constructor
   *
   * @param {Object} options deepstream options
   * @param {String} topic one of C.TOPIC
   * @param {[String]} clusterTopic A unique cluster topic, if not created uses format: topic_SUBSCRIPTIONS
   */
  constructor(options, topic, clusterTopic) {
    this._delayedBroadcasts = new Map()
    this._delay = -1
    if (options.broadcastTimeout !== undefined) {
      this._delay = options.broadcastTimeout
    }
    this._subscriptions = new Map()
    this._options = options
    this._topic = topic
    this._subscriptionListener = null
    this._constants = {
      MULTIPLE_SUBSCRIPTIONS: C.EVENT.MULTIPLE_SUBSCRIPTIONS,
      SUBSCRIBE: C.ACTIONS.SUBSCRIBE,
      UNSUBSCRIBE: C.ACTIONS.UNSUBSCRIBE,
      NOT_SUBSCRIBED: C.EVENT.NOT_SUBSCRIBED
    }
    this._onBroadcastTimeout = this._onBroadcastTimeout.bind(this)
    this._onSocketClose = this._onSocketClose.bind(this)

    this._setupRemoteComponents(clusterTopic)
  }

  /**
   * Setup all the remote components and actions required to deal with the subscription
   * via the cluster.
   */
  _setupRemoteComponents(clusterTopic) {
    this._clusterSubscriptions = new DistributedStateRegistry(clusterTopic || `${this._topic}_${C.TOPIC.SUBSCRIPTIONS}`, this._options)
    this._clusterSubscriptions.on('add', this._onClusterSubscriptionAdded.bind(this))
    this._clusterSubscriptions.on('remove', this._onClusterSubscriptionRemoved.bind(this))
  }

  /**
   * Return all the servers that have this subscription.
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllServers(subscriptionName) {
    return this._clusterSubscriptions.getAllServers(subscriptionName)
  }

  /**
   * Return all the servers that have this subscription excluding the current
   * server name
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllRemoteServers(subscriptionName) {
    const serverNames = this._clusterSubscriptions.getAllServers(subscriptionName)
    const localServerIndex = serverNames.indexOf(this._options.serverName)
    if (localServerIndex > -1) {
      serverNames.splice(serverNames.indexOf(this._options.serverName), 1)
    }
    return serverNames
  }

  /**
   * Returns a list of all the topic this registry
   * currently has subscribers for
   *
   * @public
   * @returns {Array} names
   */
  getNames() {
    return this._clusterSubscriptions.getAll()
  }

  /**
   * Returns true if the subscription exists somewhere
   * in the cluster
   *
   * @public
   * @returns {Array} names
   */
  hasName(subscriptionName) {
    return this._clusterSubscriptions.has(subscriptionName)
  }

  /**
  * This method allows you to customise the SubscriptionRegistry so that it can send custom events and ack messages back.
  * For example, when using the C.ACTIONS.LISTEN, you would override SUBSCRIBE with C.ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE
  *
  * @param {string} name The name of the the variable to override. This can be either MULTIPLE_SUBSCRIPTIONS, SUBSCRIBE, UNSUBSCRIBE, NOT_SUBSCRIBED
  * @param {string} value The value to override with.
  *
  * @public
  * @returns {void}
  */
  setAction(name, value) {
    this._constants[name.toUpperCase()] = value
  }

  _onSocketClose (socket) {
    for (const entry of this._subscriptions) {
      const name = entry[0]
      const sockets = entry[1]
      if (sockets.has(socket)) {
        this.unsubscribe(name, socket)
      }
    }
  }

  /**
   * Broadcasts the enqueued messages for the timed out subscription room.
   *
   * @param   {Object} delayedBroadcasts holds information of what messages to send and where
   *
   * @public
   * @returns {void}
   */
  _onBroadcastTimeout() {
    this._delayedBroadcastsTimer = null
    for (const entry of this._delayedBroadcasts) {
      const name = entry[0]
      const delayedBroadcasts = entry[1]
      const uniqueSenders = delayedBroadcasts.uniqueSenders
      const sharedMessages = delayedBroadcasts.sharedMessages

      if (sharedMessages.length === 0) {
        this._delayedBroadcasts.delete(name)
        continue
      }

      // for all unique senders and their gaps, build their special messages
      for (const entry of uniqueSenders) {
        let socket = entry[0]
        let gaps = entry[1]
        let i = 0
        let message = sharedMessages.substring(0, gaps[i++])
        let lastStop = gaps[i++]
        while (i < gaps.length) {
          message += sharedMessages.substring(lastStop, gaps[i++])
          lastStop = gaps[i++]
        }
        message += sharedMessages.substring(lastStop, sharedMessages.length)

        if (message) {
          socket.sendNative(message)
        }
      }

      // for all sockets in this subscription name, send either sharedMessage or this socket's
      // specialized message. only sockets that sent something will have a special message, all
      // other sockets are only listeners and receive the exact same (sharedMessage) message.
      const sockets = this._subscriptions.get(name)
      if (sockets) {
        const preparedMessage = SocketWrapper.prepareMessage(sharedMessages)
        for (const socket of sockets) {
          if (!uniqueSenders.has(socket)) {
            socket.sendPrepared(preparedMessage)
          }
        }
        SocketWrapper.finalizeMessage(preparedMessage)
      }

      delayedBroadcasts.uniqueSenders.clear()
      delayedBroadcasts.sharedMessages = ''
    }
  }

  /**
   * Enqueues a message string to be broadcast to all subscribers. Broadcasts will potentially
   * be reordered in relation to *other* subscription names, but never in relation to the same
   * subscription name. Each broadcast is given 'broadcastTimeout' ms to coalesce into one big
   * broadcast.
   *
   * @param   {String} name      the name/topic the subscriber was previously registered for
   * @param   {String} msgString the message as string
   * @param   {[SocketWrapper]} socket an optional socket that shouldn't receive the message
   *
   * @public
   * @returns {void}
   */
  sendToSubscribers(name, msgString, socket) {
    if (!this._subscriptions.has(name)) {
      return
    }

    // not all messages are valid, this should be fixed elsewhere!
    if (msgString.charAt(msgString.length - 1) !== C.MESSAGE_SEPERATOR) {
      msgString += C.MESSAGE_SEPERATOR
    }

    // if not already a delayed broadcast, create it
    let delayedBroadcasts = this._delayedBroadcasts.get(name)
    if (delayedBroadcasts === undefined) {
      delayedBroadcasts = {
        uniqueSenders: new Map(),
        sharedMessages: ''
      }
      this._delayedBroadcasts.set(name, delayedBroadcasts)
    }

    // append this message to the sharedMessage, the message that
    // is shared in the broadcast to every listener-only
    const start = delayedBroadcasts.sharedMessages.length
    delayedBroadcasts.sharedMessages += msgString
    const stop = delayedBroadcasts.sharedMessages.length

    // uniqueSendersMap maps from uuid to offset in uniqueSendersVector
    // each uniqueSender has a vector of "gaps" in relation to sharedMessage
    // sockets should not receive what they sent themselves, so a gap is inserted
    // for every send from this socket
    if (socket && socket.uuid !== undefined) {
      let uniqueSenders = delayedBroadcasts.uniqueSenders
      let gaps = uniqueSenders.get(socket)

      if (!gaps) {
        gaps = []
        uniqueSenders.set(socket, gaps)
      }

      gaps.push(start, stop)
    }

    // reuse the same timer if already started
    if (!this._delayedBroadcastsTimer) {
      if (this._delay !== -1) {
        this._delayedBroadcastsTimer = setTimeout(this._onBroadcastTimeout, this._delay)
      } else {
        this._onBroadcastTimeout()
      }
    }
  }

  /**
   * Adds a SocketWrapper as a subscriber to a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socket
   *
   * @public
   * @returns {void}
   */
  subscribe(name, socket) {
    let sockets = this._subscriptions.get(name) || new Set()

    if (sockets.size === 0) {
      this._subscriptions.set(name, sockets)
    } else if (sockets.has(socket)) {
      const msg = `repeat supscription to "${name}" by ${socket.user}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.MULTIPLE_SUBSCRIPTIONS, msg)
      socket.sendError(this._topic, this._constants.MULTIPLE_SUBSCRIPTIONS, name)
      return
    }

    sockets.add(socket)

    if (socket.listeners('close').indexOf(this._onSocketClose) === -1) {
      socket.once('close', this._onSocketClose)
    }

    if (this._subscriptionListener) {
      this._subscriptionListener.onSubscriptionMade(
        name,
        socket,
        sockets.size
      )
    }

    this._clusterSubscriptions.add(name)

    const logMsg = `for ${this._topic}:${name} by ${socket.user}`
    this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.SUBSCRIBE, logMsg)
    socket.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.SUBSCRIBE, name])
  }

  /**
   * Removes a SocketWrapper from the list of subscriptions for a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socket
   * @param   {Boolean} silent supresses logs and unsubscribe ACK messages
   *
   * @public
   * @returns {void}
   */
  unsubscribe(name, socket, silent) {
    const sockets = this._subscriptions.get(name)

    if (!sockets || !sockets.has(socket)) {
      const msg = `${socket.user} is not subscribed to ${name}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.NOT_SUBSCRIBED, msg)
      socket.sendError(this._topic, this._constants.NOT_SUBSCRIBED, name)
      return
    }

    sockets.delete(socket)

    this._clusterSubscriptions.remove(name)

    if (sockets.size === 0) {
      this._subscriptions.delete(name)
    }

    if (this._subscriptionListener) {
      this._subscriptionListener.onSubscriptionRemoved(
        name,
        socket,
        sockets.size,
        this.getAllRemoteServers(name).length
      )
    }

    if (!silent) {
      const logMsg = `for ${this._topic}:${name} by ${socket.user}`
      this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.UNSUBSCRIBE, logMsg)
      socket.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.UNSUBSCRIBE, name])
    }
  }

  /**
   * Returns an array of SocketWrappers that are subscribed
   * to <name> or null if there are no subscribers
   *
   * @param   {String} name
   *
   * @public
   * @returns {Array} SocketWrapper[]
   */
  getLocalSubscribers(name) {
    const sockets = this._subscriptions.get(name)
    return sockets ? Array.from(sockets) : []
  }

  /**
   * Returns true if there are SocketWrappers that
   * are subscribed to <name> or false if there
   * aren't any subscribers
   *
   * @param   {String}  name
   *
   * @public
   * @returns {Boolean} hasLocalSubscribers
   */
  hasLocalSubscribers(name) {
    return this._subscriptions.has(name)
  }

  /**
   * Allows to set a subscriptionListener after the class had been instantiated
   *
   * @param {SubscriptionListener} subscriptionListener - a class exposing a onSubscriptionMade and onSubscriptionRemoved method
   *
   * @public
   * @returns {void}
   */
  setSubscriptionListener(subscriptionListener) {
    this._subscriptionListener = subscriptionListener
  }

  /**
   * Called when a subscription has been added to the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from subscribe
   * @param  {String} name the name that was added
   */
  _onClusterSubscriptionAdded(name) {
    if (this._subscriptionListener && !this.hasLocalSubscribers(name)) {
      this._subscriptionListener.onSubscriptionMade(name, null, 1)
    }
  }

  /**
   * Called when a subscription has been removed from the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from unsubscribe
   * @param  {String} name the name that was removed
   */
  _onClusterSubscriptionRemoved(name) {
    if (this._subscriptionListener && !this.hasLocalSubscribers(name)) {
      this._subscriptionListener.onSubscriptionRemoved(name, null, 0, 0)
    }
  }
}

module.exports = SubscriptionRegistry
<commit_msg>perf(subscription-registry): Optimized socket close.<commit_after>'use strict'

const C = require('../constants/constants')
const DistributedStateRegistry = require('../cluster/distributed-state-registry')
const SocketWrapper = require('../message/socket-wrapper')

class SubscriptionRegistry {
  /**
   * A generic mechanism to handle subscriptions from sockets to topics.
   * A bit like an event-hub, only that it registers SocketWrappers rather
   * than functions
   *
   * @constructor
   *
   * @param {Object} options deepstream options
   * @param {String} topic one of C.TOPIC
   * @param {[String]} clusterTopic A unique cluster topic, if not created uses format: topic_SUBSCRIPTIONS
   */
  constructor(options, topic, clusterTopic) {
    this._delayedBroadcasts = new Map()
    this._delay = -1
    if (options.broadcastTimeout !== undefined) {
      this._delay = options.broadcastTimeout
    }
    this._names = new Map()
    this._subscriptions = new Map()
    this._options = options
    this._topic = topic
    this._subscriptionListener = null
    this._constants = {
      MULTIPLE_SUBSCRIPTIONS: C.EVENT.MULTIPLE_SUBSCRIPTIONS,
      SUBSCRIBE: C.ACTIONS.SUBSCRIBE,
      UNSUBSCRIBE: C.ACTIONS.UNSUBSCRIBE,
      NOT_SUBSCRIBED: C.EVENT.NOT_SUBSCRIBED
    }
    this._onBroadcastTimeout = this._onBroadcastTimeout.bind(this)
    this._onSocketClose = this._onSocketClose.bind(this)

    this._setupRemoteComponents(clusterTopic)
  }

  /**
   * Setup all the remote components and actions required to deal with the subscription
   * via the cluster.
   */
  _setupRemoteComponents(clusterTopic) {
    this._clusterSubscriptions = new DistributedStateRegistry(clusterTopic || `${this._topic}_${C.TOPIC.SUBSCRIPTIONS}`, this._options)
    this._clusterSubscriptions.on('add', this._onClusterSubscriptionAdded.bind(this))
    this._clusterSubscriptions.on('remove', this._onClusterSubscriptionRemoved.bind(this))
  }

  /**
   * Return all the servers that have this subscription.
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllServers(subscriptionName) {
    return this._clusterSubscriptions.getAllServers(subscriptionName)
  }

  /**
   * Return all the servers that have this subscription excluding the current
   * server name
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllRemoteServers(subscriptionName) {
    const serverNames = this._clusterSubscriptions.getAllServers(subscriptionName)
    const localServerIndex = serverNames.indexOf(this._options.serverName)
    if (localServerIndex > -1) {
      serverNames.splice(serverNames.indexOf(this._options.serverName), 1)
    }
    return serverNames
  }

  /**
   * Returns a list of all the topic this registry
   * currently has subscribers for
   *
   * @public
   * @returns {Array} names
   */
  getNames() {
    return this._clusterSubscriptions.getAll()
  }

  /**
   * Returns true if the subscription exists somewhere
   * in the cluster
   *
   * @public
   * @returns {Array} names
   */
  hasName(subscriptionName) {
    return this._clusterSubscriptions.has(subscriptionName)
  }

  /**
  * This method allows you to customise the SubscriptionRegistry so that it can send custom events and ack messages back.
  * For example, when using the C.ACTIONS.LISTEN, you would override SUBSCRIBE with C.ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE
  *
  * @param {string} name The name of the the variable to override. This can be either MULTIPLE_SUBSCRIPTIONS, SUBSCRIBE, UNSUBSCRIBE, NOT_SUBSCRIBED
  * @param {string} value The value to override with.
  *
  * @public
  * @returns {void}
  */
  setAction(name, value) {
    this._constants[name.toUpperCase()] = value
  }

  _onSocketClose (socket) {
    for (const name of this._names.get(socket) || []) {
      this.unsubscribe(name, socket)
    }
  }

  /**
   * Broadcasts the enqueued messages for the timed out subscription room.
   *
   * @param   {Object} delayedBroadcasts holds information of what messages to send and where
   *
   * @public
   * @returns {void}
   */
  _onBroadcastTimeout() {
    this._delayedBroadcastsTimer = null
    for (const entry of this._delayedBroadcasts) {
      const name = entry[0]
      const delayedBroadcasts = entry[1]
      const uniqueSenders = delayedBroadcasts.uniqueSenders
      const sharedMessages = delayedBroadcasts.sharedMessages

      if (sharedMessages.length === 0) {
        this._delayedBroadcasts.delete(name)
        continue
      }

      // for all unique senders and their gaps, build their special messages
      for (const entry of uniqueSenders) {
        let socket = entry[0]
        let gaps = entry[1]
        let i = 0
        let message = sharedMessages.substring(0, gaps[i++])
        let lastStop = gaps[i++]
        while (i < gaps.length) {
          message += sharedMessages.substring(lastStop, gaps[i++])
          lastStop = gaps[i++]
        }
        message += sharedMessages.substring(lastStop, sharedMessages.length)

        if (message) {
          socket.sendNative(message)
        }
      }

      // for all sockets in this subscription name, send either sharedMessage or this socket's
      // specialized message. only sockets that sent something will have a special message, all
      // other sockets are only listeners and receive the exact same (sharedMessage) message.
      const sockets = this._subscriptions.get(name)
      if (sockets) {
        const preparedMessage = SocketWrapper.prepareMessage(sharedMessages)
        for (const socket of sockets) {
          if (!uniqueSenders.has(socket)) {
            socket.sendPrepared(preparedMessage)
          }
        }
        SocketWrapper.finalizeMessage(preparedMessage)
      }

      delayedBroadcasts.uniqueSenders.clear()
      delayedBroadcasts.sharedMessages = ''
    }
  }

  /**
   * Enqueues a message string to be broadcast to all subscribers. Broadcasts will potentially
   * be reordered in relation to *other* subscription names, but never in relation to the same
   * subscription name. Each broadcast is given 'broadcastTimeout' ms to coalesce into one big
   * broadcast.
   *
   * @param   {String} name      the name/topic the subscriber was previously registered for
   * @param   {String} msgString the message as string
   * @param   {[SocketWrapper]} socket an optional socket that shouldn't receive the message
   *
   * @public
   * @returns {void}
   */
  sendToSubscribers(name, msgString, socket) {
    if (!this._subscriptions.has(name)) {
      return
    }

    // not all messages are valid, this should be fixed elsewhere!
    if (msgString.charAt(msgString.length - 1) !== C.MESSAGE_SEPERATOR) {
      msgString += C.MESSAGE_SEPERATOR
    }

    // if not already a delayed broadcast, create it
    let delayedBroadcasts = this._delayedBroadcasts.get(name)
    if (delayedBroadcasts === undefined) {
      delayedBroadcasts = {
        uniqueSenders: new Map(),
        sharedMessages: ''
      }
      this._delayedBroadcasts.set(name, delayedBroadcasts)
    }

    // append this message to the sharedMessage, the message that
    // is shared in the broadcast to every listener-only
    const start = delayedBroadcasts.sharedMessages.length
    delayedBroadcasts.sharedMessages += msgString
    const stop = delayedBroadcasts.sharedMessages.length

    // uniqueSendersMap maps from uuid to offset in uniqueSendersVector
    // each uniqueSender has a vector of "gaps" in relation to sharedMessage
    // sockets should not receive what they sent themselves, so a gap is inserted
    // for every send from this socket
    if (socket && socket.uuid !== undefined) {
      let uniqueSenders = delayedBroadcasts.uniqueSenders
      let gaps = uniqueSenders.get(socket)

      if (!gaps) {
        gaps = []
        uniqueSenders.set(socket, gaps)
      }

      gaps.push(start, stop)
    }

    // reuse the same timer if already started
    if (!this._delayedBroadcastsTimer) {
      if (this._delay !== -1) {
        this._delayedBroadcastsTimer = setTimeout(this._onBroadcastTimeout, this._delay)
      } else {
        this._onBroadcastTimeout()
      }
    }
  }

  /**
   * Adds a SocketWrapper as a subscriber to a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socket
   *
   * @public
   * @returns {void}
   */
  subscribe(name, socket) {
    let sockets = this._subscriptions.get(name) || new Set()

    if (sockets.size === 0) {
      this._subscriptions.set(name, sockets)
    } else if (sockets.has(socket)) {
      const msg = `repeat supscription to "${name}" by ${socket.user}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.MULTIPLE_SUBSCRIPTIONS, msg)
      socket.sendError(this._topic, this._constants.MULTIPLE_SUBSCRIPTIONS, name)
      return
    }

    sockets.add(socket)

    if (socket.listeners('close').indexOf(this._onSocketClose) === -1) {
      socket.once('close', this._onSocketClose)
    }

    const names = this._names.get(socket) || new Set()
    if (names.size === 0) {
      this._names.set(socket, names)
    }
    names.add(name)

    if (this._subscriptionListener) {
      this._subscriptionListener.onSubscriptionMade(
        name,
        socket,
        sockets.size
      )
    }

    this._clusterSubscriptions.add(name)

    const logMsg = `for ${this._topic}:${name} by ${socket.user}`
    this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.SUBSCRIBE, logMsg)
    socket.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.SUBSCRIBE, name])
  }

  /**
   * Removes a SocketWrapper from the list of subscriptions for a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socket
   * @param   {Boolean} silent supresses logs and unsubscribe ACK messages
   *
   * @public
   * @returns {void}
   */
  unsubscribe(name, socket, silent) {
    const sockets = this._subscriptions.get(name)

    if (!sockets || !sockets.has(socket)) {
      const msg = `${socket.user} is not subscribed to ${name}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.NOT_SUBSCRIBED, msg)
      socket.sendError(this._topic, this._constants.NOT_SUBSCRIBED, name)
      return
    }

    sockets.delete(socket)

    this._clusterSubscriptions.remove(name)

    if (sockets.size === 0) {
      this._subscriptions.delete(name)
    }

    const names = this._names.get(socket)
    names.delete(name)

    if (names.size === 0) {
      this._names.delete(socket)
    }

    if (this._subscriptionListener) {
      this._subscriptionListener.onSubscriptionRemoved(
        name,
        socket,
        sockets.size,
        this.getAllRemoteServers(name).length
      )
    }

    if (!silent) {
      const logMsg = `for ${this._topic}:${name} by ${socket.user}`
      this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.UNSUBSCRIBE, logMsg)
      socket.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.UNSUBSCRIBE, name])
    }
  }

  /**
   * Returns an array of SocketWrappers that are subscribed
   * to <name> or null if there are no subscribers
   *
   * @param   {String} name
   *
   * @public
   * @returns {Array} SocketWrapper[]
   */
  getLocalSubscribers(name) {
    const sockets = this._subscriptions.get(name)
    return sockets ? Array.from(sockets) : []
  }

  /**
   * Returns true if there are SocketWrappers that
   * are subscribed to <name> or false if there
   * aren't any subscribers
   *
   * @param   {String}  name
   *
   * @public
   * @returns {Boolean} hasLocalSubscribers
   */
  hasLocalSubscribers(name) {
    return this._subscriptions.has(name)
  }

  /**
   * Allows to set a subscriptionListener after the class had been instantiated
   *
   * @param {SubscriptionListener} subscriptionListener - a class exposing a onSubscriptionMade and onSubscriptionRemoved method
   *
   * @public
   * @returns {void}
   */
  setSubscriptionListener(subscriptionListener) {
    this._subscriptionListener = subscriptionListener
  }

  /**
   * Called when a subscription has been added to the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from subscribe
   * @param  {String} name the name that was added
   */
  _onClusterSubscriptionAdded(name) {
    if (this._subscriptionListener && !this.hasLocalSubscribers(name)) {
      this._subscriptionListener.onSubscriptionMade(name, null, 1)
    }
  }

  /**
   * Called when a subscription has been removed from the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from unsubscribe
   * @param  {String} name the name that was removed
   */
  _onClusterSubscriptionRemoved(name) {
    if (this._subscriptionListener && !this.hasLocalSubscribers(name)) {
      this._subscriptionListener.onSubscriptionRemoved(name, null, 0, 0)
    }
  }
}

module.exports = SubscriptionRegistry
<|endoftext|>


<commit_before>/* exported update_browserstring, config_open, text_preview_init */
/* global queues, active, notice_visible */

String.prototype.repeat = function(num){
	return new Array( num + 1 ).join( this );
};

/* globals */
var delete_id = undefined; /* which slide is about to be deleted */

var queue = function(){
	return {
		sorting: function(){
			/* test if queues exists on this page */
			if ( typeof(queues) == 'undefined' ){
				return;
			}

			/* queues is a global variable */
			for ( var i in queues ){
				var id = queues[i];
				var other = queues.filter(function(x){ return x != id; });
				$(id).bind('updated', function() {
					var self = '#' + $(this).attr('id');
					var list = $(this).sortable('toArray');
					var n = list.length;

					/* show/hide warning about empty queue */
					if ( self == active ){
						if ( notice_visible && n > 0 ){
							notice_visible = false;
							$("#empty_notice").fadeOut("slow");
						}
						if ( !notice_visible && n == 0 ){
							notice_visible = true;
							$("#empty_notice").fadeIn("slow");
						}
					}

					/* notify server about update */
					$.ajax({
						type: "POST",
						url: "/slides/ajax/move",
						data: "queue=" + $(this).attr('id') + "&slides=" + list,
						error: function(x, status, error){
							alert(status + '\n' + error);
						},
					});
				});
				$(id).sortable({
					connectWith: other,
					placeholder: 'slide_placeholder',
					tolerance: 'pointer',
					distance: 10,
					update: function(){
						$(this).trigger('updated');
					},
				});
				$(id).disableSelection();
			}
		},
	};
}();

var slide = function(){
	var post = function(url, data, func){
		$.ajax({
			type: "POST",
			url: url,
			data: data,
			dataType: 'json',
			success: function(data){
				if ( !data.success ){
					alert(data.message);
					return;
				}
				return func(data);
			},
			error: function(x, status, error){
				alert(status + '\n' + error);
			},
		});
	};

	return {
		/* show a confirmation dialog and delete if user confirms (exception
		 * being if quick is true in which case it is deleted immediately) */
		delete: function(id, quick){
			delete_id = id;

			if ( quick ){
				slide.real_delete();
				return;
			}

			$("#delete_dialog img").attr('src', '/slides/show/' + id + '/800/600');
			$("#delete_dialog").dialog({
				modal: true,
				resizable: false,
				position: 'center',
				width: 834, /* 800 + 17 + 17 (padding) */
				height: 700,
				close: function(){
					delete_id = undefined;
				},
			});
		},

		/* delete slide without confirmation */
		real_delete: function(){
			$.ajax({
				type: "POST",
				url: "/slides/ajax/delete",
				data: {id: delete_id},
				dataType: 'json',
				success: function(data){
					if ( data['success'] ){
						$('#slide_' + delete_id).remove();
					} else {
						alert(data['message']);
					}
					$('#delete_dialog').dialog('close');
				},
				error: function(x, status, error){
					alert(status + '\n' + error);
					$('#delete_dialog').dialog('close');
				},
			});
		},

		activate: function(id){
			return post("/slides/ajax/activate", {id: id}, function(data){
				parent = '#slide_' + id;
				$(parent).attr('class', data.class);
				console.log('actiaved');
			});
		},

		deactivate: function(id){
			return post("/slides/ajax/deactivate", {id: id}, function(data){
				parent = '#slide_' + id;
				$(parent).attr('class', data.class);
				console.log('deactiaved');
			});
		},
	};
}();

var config = function(){
	function preview_transition(){
		var selected = $('.conf .transition :selected').val();
		$('#transition_preview').html('');
	}

	return {
		preview_transition: preview_transition,
	};
}();

$(document).ready(function(){
	/* enable sorting on main page */
	queue.sorting();

	/* setup delete dialog buttons */
	$('#delete_cancel').bind('click', function(){
		$('#delete_dialog').dialog('close');
	});
	$('#delete_confirm').bind('click', slide.real_delete);

	/* fold slide upload fieldsets */
	var $f = $('.foldable');
	$f.foldable({
		collapsed: function(){
			/* all start collapsed but text assembler */
			return $(this).attr('id') != 'assembler_text';
		},
		expanded_html: '',
		collapsed_html: '',
		connected: $f,
	});

	/* preview transition during configuration */
	config.preview_transition();
	$('.conf .transition').change(function(){
		config.preview_transition();
	});
});

function update_browserstring(){
	var provider = $("input[name='Database.Provider']").val();
	var username = $("input[name='Database.Username']").val();
	var hostname = $("input[name='Database.Hostname']").val();
	var name     = $("input[name='Database.Name']").val();

	/* build credential-part of the browserstring */
	var credential = '';
	if ( username !== '' ){
		credential = username + '@';
	}

	/* append a / to hostname if specified */
	if ( hostname != "" ){
		hostname += "/";
	}

	$('.browserstring').html(provider + "://" + credential + hostname + name);
}

function config_open(section){
	console.log('open section ' + section);

	/* reset current selection */
	$('#sidebar a').attr('class','');
	$('.conf fieldset').hide();

	$('#menu_'+section+' a').attr('class', 'selected');
	$('#conf_'+section).show();
}

var $text_preview_fields = [];
var text_preview_timer;
function text_preview(){
	clearTimeout(text_preview_timer);

	var data = {};
	$text_preview_fields.each(function(){
		data[$(this).attr('name')] = $(this).val();
	});

	$('#assembler_text .preview img').attr('src', '/slides/preview?' + $.param(data));
}

function text_preview_init(){
	$text_preview_fields = $('#assembler_text .fields span').find('input, textarea');
	$text_preview_fields.each(function(){
		$(this).blur(text_preview);
		$(this).keydown(function(){
			clearTimeout(text_preview_timer);
			text_preview_timer = setTimeout(text_preview, 800);
		});
	});

	/* initial preview */
	text_preview();
}
<commit_msg>eslint: fix comma-spacing<commit_after>/* exported update_browserstring, config_open, text_preview_init */
/* global queues, active, notice_visible */

String.prototype.repeat = function(num){
	return new Array( num + 1 ).join( this );
};

/* globals */
var delete_id = undefined; /* which slide is about to be deleted */

var queue = function(){
	return {
		sorting: function(){
			/* test if queues exists on this page */
			if ( typeof(queues) == 'undefined' ){
				return;
			}

			/* queues is a global variable */
			for ( var i in queues ){
				var id = queues[i];
				var other = queues.filter(function(x){ return x != id; });
				$(id).bind('updated', function() {
					var self = '#' + $(this).attr('id');
					var list = $(this).sortable('toArray');
					var n = list.length;

					/* show/hide warning about empty queue */
					if ( self == active ){
						if ( notice_visible && n > 0 ){
							notice_visible = false;
							$("#empty_notice").fadeOut("slow");
						}
						if ( !notice_visible && n == 0 ){
							notice_visible = true;
							$("#empty_notice").fadeIn("slow");
						}
					}

					/* notify server about update */
					$.ajax({
						type: "POST",
						url: "/slides/ajax/move",
						data: "queue=" + $(this).attr('id') + "&slides=" + list,
						error: function(x, status, error){
							alert(status + '\n' + error);
						},
					});
				});
				$(id).sortable({
					connectWith: other,
					placeholder: 'slide_placeholder',
					tolerance: 'pointer',
					distance: 10,
					update: function(){
						$(this).trigger('updated');
					},
				});
				$(id).disableSelection();
			}
		},
	};
}();

var slide = function(){
	var post = function(url, data, func){
		$.ajax({
			type: "POST",
			url: url,
			data: data,
			dataType: 'json',
			success: function(data){
				if ( !data.success ){
					alert(data.message);
					return;
				}
				return func(data);
			},
			error: function(x, status, error){
				alert(status + '\n' + error);
			},
		});
	};

	return {
		/* show a confirmation dialog and delete if user confirms (exception
		 * being if quick is true in which case it is deleted immediately) */
		delete: function(id, quick){
			delete_id = id;

			if ( quick ){
				slide.real_delete();
				return;
			}

			$("#delete_dialog img").attr('src', '/slides/show/' + id + '/800/600');
			$("#delete_dialog").dialog({
				modal: true,
				resizable: false,
				position: 'center',
				width: 834, /* 800 + 17 + 17 (padding) */
				height: 700,
				close: function(){
					delete_id = undefined;
				},
			});
		},

		/* delete slide without confirmation */
		real_delete: function(){
			$.ajax({
				type: "POST",
				url: "/slides/ajax/delete",
				data: {id: delete_id},
				dataType: 'json',
				success: function(data){
					if ( data['success'] ){
						$('#slide_' + delete_id).remove();
					} else {
						alert(data['message']);
					}
					$('#delete_dialog').dialog('close');
				},
				error: function(x, status, error){
					alert(status + '\n' + error);
					$('#delete_dialog').dialog('close');
				},
			});
		},

		activate: function(id){
			return post("/slides/ajax/activate", {id: id}, function(data){
				parent = '#slide_' + id;
				$(parent).attr('class', data.class);
				console.log('actiaved');
			});
		},

		deactivate: function(id){
			return post("/slides/ajax/deactivate", {id: id}, function(data){
				parent = '#slide_' + id;
				$(parent).attr('class', data.class);
				console.log('deactiaved');
			});
		},
	};
}();

var config = function(){
	function preview_transition(){
		var selected = $('.conf .transition :selected').val();
		$('#transition_preview').html('');
	}

	return {
		preview_transition: preview_transition,
	};
}();

$(document).ready(function(){
	/* enable sorting on main page */
	queue.sorting();

	/* setup delete dialog buttons */
	$('#delete_cancel').bind('click', function(){
		$('#delete_dialog').dialog('close');
	});
	$('#delete_confirm').bind('click', slide.real_delete);

	/* fold slide upload fieldsets */
	var $f = $('.foldable');
	$f.foldable({
		collapsed: function(){
			/* all start collapsed but text assembler */
			return $(this).attr('id') != 'assembler_text';
		},
		expanded_html: '',
		collapsed_html: '',
		connected: $f,
	});

	/* preview transition during configuration */
	config.preview_transition();
	$('.conf .transition').change(function(){
		config.preview_transition();
	});
});

function update_browserstring(){
	var provider = $("input[name='Database.Provider']").val();
	var username = $("input[name='Database.Username']").val();
	var hostname = $("input[name='Database.Hostname']").val();
	var name     = $("input[name='Database.Name']").val();

	/* build credential-part of the browserstring */
	var credential = '';
	if ( username !== '' ){
		credential = username + '@';
	}

	/* append a / to hostname if specified */
	if ( hostname != "" ){
		hostname += "/";
	}

	$('.browserstring').html(provider + "://" + credential + hostname + name);
}

function config_open(section){
	console.log('open section ' + section);

	/* reset current selection */
	$('#sidebar a').attr('class', '');
	$('.conf fieldset').hide();

	$('#menu_'+section+' a').attr('class', 'selected');
	$('#conf_'+section).show();
}

var $text_preview_fields = [];
var text_preview_timer;
function text_preview(){
	clearTimeout(text_preview_timer);

	var data = {};
	$text_preview_fields.each(function(){
		data[$(this).attr('name')] = $(this).val();
	});

	$('#assembler_text .preview img').attr('src', '/slides/preview?' + $.param(data));
}

function text_preview_init(){
	$text_preview_fields = $('#assembler_text .fields span').find('input, textarea');
	$text_preview_fields.each(function(){
		$(this).blur(text_preview);
		$(this).keydown(function(){
			clearTimeout(text_preview_timer);
			text_preview_timer = setTimeout(text_preview, 800);
		});
	});

	/* initial preview */
	text_preview();
}
<|endoftext|>


<commit_before>const {app, BrowserWindow, ipcMain} = require('electron');
const path = require('path');
const url = require('url');

// hot reload for electron
// should be commented befor packaging
require('electron-reload')(__dirname);

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win, detailsWin;

function createWindow() {
    win = new BrowserWindow({
        frame: false,
        resizable: false,
        width: 550,
        height: 350,
        // Don't show the window until it ready, this prevents any white flickering
        show: false,
        icon: path.resolve(__dirname, 'app', 'css', 'app_icon.png')
    });

    // and load the index.html of the app.
    win.loadURL(url.format({
        pathname: path.join(__dirname, 'app/index.html'),
        protocol: 'file:',
        slashes: true
    }));

    // just for debugging
    //win.webContents.openDevTools();

    // Emitted when the window is closed.
    win.on('closed', () => win = null);

    // Show window when page is ready
    win.once('ready-to-show', function () {
        win.show();
    });
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.once('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () => {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
        createWindow();
    }
});

//Events
ipcMain.on('close-main-window', (event, arg) => {
    app.quit();
});

// event to open a window and load an extra resource
ipcMain.on('open-details', (event, link) => {
    if (detailsWin) {
        detailsWin.close();
    }

    detailsWin = new BrowserWindow({
        height: 1000,
        width: 1600,
        icon: path.resolve(__dirname, 'app', 'css', 'app_icon.png'),
        webPreferences: {
            nodeIntegration: false
        }
    });

    detailsWin.setMenu(null);
    detailsWin.loadURL(link);
    //detailsWin.webContents.executeJavaScript();
    detailsWin.on('closed', () => detailsWin = null);
});<commit_msg>commented dev reload<commit_after>const {app, BrowserWindow, ipcMain} = require('electron');
const path = require('path');
const url = require('url');

// hot reload for electron
// should be commented befor packaging
//require('electron-reload')(__dirname);

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win, detailsWin;

function createWindow() {
    win = new BrowserWindow({
        frame: false,
        resizable: false,
        width: 550,
        height: 350,
        // Don't show the window until it ready, this prevents any white flickering
        show: false,
        icon: path.resolve(__dirname, 'app', 'css', 'app_icon.png')
    });

    // and load the index.html of the app.
    win.loadURL(url.format({
        pathname: path.join(__dirname, 'app/index.html'),
        protocol: 'file:',
        slashes: true
    }));

    // just for debugging
    //win.webContents.openDevTools();

    // Emitted when the window is closed.
    win.on('closed', () => win = null);

    // Show window when page is ready
    win.once('ready-to-show', function () {
        win.show();
    });
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.once('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () => {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
        createWindow();
    }
});

//Events
ipcMain.on('close-main-window', (event, arg) => {
    app.quit();
});

// event to open a window and load an extra resource
ipcMain.on('open-details', (event, link) => {
    if (detailsWin) {
        detailsWin.close();
    }

    detailsWin = new BrowserWindow({
        height: 1000,
        width: 1600,
        icon: path.resolve(__dirname, 'app', 'css', 'app_icon.png'),
        webPreferences: {
            nodeIntegration: false
        }
    });

    detailsWin.setMenu(null);
    detailsWin.loadURL(link);
    //detailsWin.webContents.executeJavaScript();
    detailsWin.on('closed', () => detailsWin = null);
});<|endoftext|>


<commit_before>import Ember from "ember";
import config from "../config/environment";

export default {
  // this function navigates the relationship tree unloading records unless
  // related record is a taxonomy type or user
  unloadRecordTree: function(record) {
    var taxonomyTypes = config.APP.PRELOAD_TYPES
      .concat(config.APP.PRELOAD_AUTHORIZED_TYPES)
      .concat("user")
      .map(t => t.replace(/_/, '-'));

    if (!record || record.unloading || taxonomyTypes.indexOf(record.constructor.modelName) !== -1) {
      return;
    }

    record.unloading = true;
    record.eachRelationship((name, descriptor) => {
      if (descriptor.options.async && !this.isAsyncRelationshipLoaded(record, name)) {
        return;
      }

      var unload = r => {
        if (descriptor.kind === "belongsTo") {
          this.unloadRecordTree(r);
        } else if (descriptor.kind === "hasMany") {
          r.forEach(r2 => this.unloadRecordTree(r2));
        }
      };

      if (descriptor.options.async) {
        Ember.run.next(() => record.get(name).then(r => unload(r)));
      } else {
        unload(record.get(name));
      }
    });

    Ember.run.next(() => record.unloadRecord());
  },

  isAsyncRelationshipLoaded: function(record, relationshipName) {
    // todo replace use of private api "record._relationships" https://github.com/emberjs/data/issues/2431
    var relatedRecord = record._relationships[relationshipName].inverseRecord;

    // this is the workaround I read in github issue
    if (!relatedRecord) {
      return false;
    }

    // this is the workaround I seem to need from unit test
    var relationshipKind = Ember.get(record.constructor, "relationshipsByName").get(relationshipName).kind;
    if (relationshipKind === "belongsTo") {
      return record.store.hasRecordForId(relatedRecord.constructor.modelName, relatedRecord.id);
    } else {
      throw "NotYetImplemented";
    }
  }
};
<commit_msg>fixed issue with deletion of rejected items<commit_after>import Ember from "ember";
import config from "../config/environment";

export default {
  // this function navigates the relationship tree unloading records unless
  // related record is a taxonomy type or user
  unloadRecordTree: function(record) {
    var taxonomyTypes = ["territory", "package-type","donor-condition","rejection-reason","permission", "timeslot", "gogovan-transport", "crossroads-transport", "version", "user"]

    // TO_DO FIX ISSUE WITH DELETION
    // var taxonomyTypes = config.APP.PRELOAD_TYPES
    //   .concat(config.APP.PRELOAD_AUTHORIZED_TYPES)
    //   .concat("user")
    //   .map(t => t.replace(/_/, '-'));

    if (!record || record.unloading || taxonomyTypes.indexOf(record.constructor.modelName) !== -1) {
      return;
    }

    record.unloading = true;
    record.eachRelationship((name, descriptor) => {
      if (descriptor.options.async && !this.isAsyncRelationshipLoaded(record, name)) {
        return;
      }

      var unload = r => {
        if (descriptor.kind === "belongsTo") {
          this.unloadRecordTree(r);
        } else if (descriptor.kind === "hasMany") {
          r.forEach(r2 => this.unloadRecordTree(r2));
        }
      };

      if (descriptor.options.async) {
        Ember.run.next(() => record.get(name).then(r => unload(r)));
      } else {
        unload(record.get(name));
      }
    });

    Ember.run.next(() => record.unloadRecord());
  },

  isAsyncRelationshipLoaded: function(record, relationshipName) {
    // todo replace use of private api "record._relationships" https://github.com/emberjs/data/issues/2431
    var relatedRecord = record._relationships[relationshipName].inverseRecord;

    // this is the workaround I read in github issue
    if (!relatedRecord) {
      return false;
    }

    // this is the workaround I seem to need from unit test
    var relationshipKind = Ember.get(record.constructor, "relationshipsByName").get(relationshipName).kind;
    if (relationshipKind === "belongsTo") {
      return record.store.hasRecordForId(relatedRecord.constructor.modelName, relatedRecord.id);
    } else {
      throw "NotYetImplemented";
    }
  }
};
<|endoftext|>


<commit_before>import React from "react";
import PropTypes from "prop-types";
import VictoryPortal from "../victory-portal/victory-portal";
import CustomPropTypes from "../victory-util/prop-types";
import Collection from "../victory-util/collection";
import Helpers from "../victory-util/helpers";
import LabelHelpers from "../victory-util/label-helpers";
import Style from "../victory-util/style";
import Log from "../victory-util/log";
import { assign, merge } from "lodash";

const defaultStyles = {
  fill: "#252525",
  fontSize: 14,
  fontFamily: "'Gill Sans', 'Gill Sans MT', 'Seravek', 'Trebuchet MS', sans-serif",
  stroke: "transparent"
};

export default class VictoryLabel extends React.Component {
  static displayName = "VictoryLabel";
  static role = "label";
  static propTypes = {
    active: PropTypes.bool,
    angle: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number
    ]),
    capHeight: PropTypes.oneOfType([
      PropTypes.string,
      CustomPropTypes.nonNegative,
      PropTypes.func
    ]),
    className: PropTypes.string,
    data: PropTypes.array,
    datum: PropTypes.any,
    desc: PropTypes.string,
    dx: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string,
      PropTypes.func
    ]),
    dy: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string,
      PropTypes.func
    ]),
    events: PropTypes.object,
    index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    labelPlacement: PropTypes.oneOf(["parallel", "perpendicular", "vertical"]),
    lineHeight: PropTypes.oneOfType([
      PropTypes.string,
      CustomPropTypes.nonNegative,
      PropTypes.func
    ]),
    origin: PropTypes.shape({ x: CustomPropTypes.nonNegative, y: CustomPropTypes.nonNegative }),
    polar: PropTypes.bool,
    renderInPortal: PropTypes.bool,
    scale: PropTypes.shape({ x: CustomPropTypes.scale, y: CustomPropTypes.scale }),
    style: PropTypes.oneOfType([
      PropTypes.object,
      PropTypes.array
    ]),
    text: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number,
      PropTypes.func,
      PropTypes.array
    ]),
    textAnchor: PropTypes.oneOfType([
      PropTypes.oneOf([
        "start",
        "middle",
        "end",
        "inherit"
      ]),
      PropTypes.func
    ]),
    title: PropTypes.string,
    transform: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object,
      PropTypes.func
    ]),
    verticalAnchor: PropTypes.oneOfType([
      PropTypes.oneOf([
        "start",
        "middle",
        "end"
      ]),
      PropTypes.func
    ]),
    x: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string
    ]),
    y: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string
    ])
  };

  static defaultProps = {
    capHeight: 0.71, // Magic number from d3.
    lineHeight: 1
  };

  componentWillMount() {
    this.cacheAttributes(this.calculateAttributes(this.props));
  }

  shouldComponentUpdate(nextProps) {
    const attrs = this.calculateAttributes(nextProps);
    const { style, dx, dy, x, y, content, lineHeight, textAnchor, transform } = attrs;
    const { angle, className, datum, active } = this.props;
    if (!Collection.allSetsEqual([
      [active, nextProps.active],
      [angle, nextProps.angle],
      [className, nextProps.className],
      [x, this.x],
      [y, this.y],
      [dx, this.dx],
      [dy, this.dy],
      [lineHeight, this.lineHeight],
      [textAnchor, this.textAnchor],
      [transform, this.transform],
      [content, this.content],
      [style, this.style],
      [datum, nextProps.datum]
    ])) {
      this.cacheAttributes(attrs);
      return true;
    }
    return false;
  }

  cacheAttributes(attrs) {
    const { style, dx, dy, x, y, content, textAnchor, transform, lineHeight } = attrs;
    this.style = style;
    this.dx = dx;
    this.dy = dy;
    this.x = x;
    this.y = y;
    this.content = content;
    this.textAnchor = textAnchor;
    this.lineHeight = lineHeight;
    this.transform = transform;
  }

  calculateAttributes(props) {
    const style = this.getStyles(props);
    const lineHeight = this.getHeight(props, "lineHeight");
    const textAnchor = props.textAnchor ?
      Helpers.evaluateProp(props.textAnchor, props.datum) : "start";
    const content = this.getContent(props);
    const dx = props.dx ? Helpers.evaluateProp(props.dx, props.datum) : 0;
    const dy = this.getDy(props, style, content, lineHeight);
    const transform = this.getTransform(props, style);
    const x = typeof props.x !== "undefined" ? props.x : this.getPosition(props, "x");
    const y = typeof props.y !== "undefined" ? props.y : this.getPosition(props, "y");
    return {
      style, dx, dy, content, lineHeight, textAnchor, transform, x, y
    };
  }

  getPosition(props, dimension) {
    if (!props.datum) {
      return 0;
    }
    const scaledPoint = Helpers.scalePoint(props, props.datum);
    return scaledPoint[dimension];
  }

  getStyle(props, style) {
    style = style ? merge({}, defaultStyles, style) : defaultStyles;
    const datum = props.datum || props.data;
    const baseStyles = Helpers.evaluateStyle(style, datum, props.active);
    return assign({}, baseStyles, { fontSize: this.getFontSize(baseStyles) });
  }

  getStyles(props) {
    return Array.isArray(props.style) ?
      props.style.map((style) => this.getStyle(props, style)) : [this.getStyle(props, props.style)];
  }

  getHeight(props, type) {
    const datum = props.datum || props.data;
    return Helpers.evaluateProp(props[type], datum, props.active);
  }

  getContent(props) {
    if (props.text === undefined || props.text === null) {
      return [" "];
    }
    const datum = props.datum || props.data;
    if (Array.isArray(props.text)) {
      return props.text.map((line) => Helpers.evaluateProp(line, datum, props.active));
    }
    const child = Helpers.evaluateProp(props.text, datum, props.active);
    return `${child}`.split("\n");
  }

  getDy(props, style, content, lineHeight) { //eslint-disable-line max-params
    style = Array.isArray(style) ? style[0] : style;
    const fontSize = style.fontSize;
    const datum = props.datum || props.data;
    const dy = props.dy ? Helpers.evaluateProp(props.dy, datum) : 0;
    const length = content.length;
    const capHeight = this.getHeight(props, "capHeight");
    const verticalAnchor = style.verticalAnchor || props.verticalAnchor;
    const anchor = verticalAnchor ?
      Helpers.evaluateProp(verticalAnchor, datum) : "middle";
    switch (anchor) {
    case "end":
      return dy + (capHeight / 2 + (0.5 - length) * lineHeight) * fontSize;
    case "middle":
      return dy + (capHeight / 2 + (0.5 - length / 2) * lineHeight) * fontSize;
    default:
      return dy + (capHeight / 2 + lineHeight / 2) * fontSize;
    }
  }

  getTransform(props, style) {
    const { datum, x, y, polar } = props;
    const defaultAngle = polar ? LabelHelpers.getPolarAngle(props) : 0;
    const angle = style.angle || props.angle || defaultAngle;
    const transform = props.transform || style.transform;
    const transformPart = transform && Helpers.evaluateProp(transform, datum);
    const rotatePart = angle && { rotate: [angle, x, y] };
    return transformPart || angle ?
      Style.toTransformString(transformPart, rotatePart) : undefined;
  }

  getFontSize(style) {
    const baseSize = style && style.fontSize;
    if (typeof baseSize === "number") {
      return baseSize;
    } else if (baseSize === undefined || baseSize === null) {
      return defaultStyles.fontSize;
    } else if (typeof baseSize === "string") {
      const fontSize = +baseSize.replace("px", "");
      if (!isNaN(fontSize)) {
        return fontSize;
      } else {
        Log.warn("fontSize should be expressed as a number of pixels");
        return defaultStyles.fontSize;
      }
    }
    return defaultStyles.fontSize;
  }

  // Overridden in victory-core-native
  renderElements(props) {
    const textProps = {
      dx: this.dx, dy: this.dy, x: this.x, y: this.y,
      transform: this.transform, className: props.className
    };
    return (
      <text {...textProps}
        {...props.events}
      >
        {this.props.title && <title>{this.props.title}</title>}
        {this.props.desc && <desc>{this.props.desc}</desc>}
        {this.content.map((line, i) => {
          const style = this.style[i] || this.style[0];
          const lastStyle = this.style[i - 1] || this.style[0];
          const fontSize = (style.fontSize + lastStyle.fontSize) / 2;
          const textAnchor = style.textAnchor || this.textAnchor;
          const dy = i ? (this.lineHeight * fontSize) : undefined;
          return (
            <tspan key={i} x={props.x} dy={dy} dx={this.dx} style={style} textAnchor={textAnchor}>
              {line}
            </tspan>
          );
        })}
      </text>
    );
  }

  render() {
    const label = this.renderElements(this.props);
    return this.props.renderInPortal ? <VictoryPortal>{label}</VictoryPortal> : label;
  }
}
<commit_msg>Add support for inlining multiple text. Feature for #893.<commit_after>import React from "react";
import PropTypes from "prop-types";
import VictoryPortal from "../victory-portal/victory-portal";
import CustomPropTypes from "../victory-util/prop-types";
import Collection from "../victory-util/collection";
import Helpers from "../victory-util/helpers";
import LabelHelpers from "../victory-util/label-helpers";
import Style from "../victory-util/style";
import Log from "../victory-util/log";
import { assign, merge } from "lodash";

const defaultStyles = {
  fill: "#252525",
  fontSize: 14,
  fontFamily: "'Gill Sans', 'Gill Sans MT', 'Seravek', 'Trebuchet MS', sans-serif",
  stroke: "transparent"
};

export default class VictoryLabel extends React.Component {
  static displayName = "VictoryLabel";
  static role = "label";
  static propTypes = {
    active: PropTypes.bool,
    angle: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number
    ]),
    capHeight: PropTypes.oneOfType([
      PropTypes.string,
      CustomPropTypes.nonNegative,
      PropTypes.func
    ]),
    className: PropTypes.string,
    data: PropTypes.array,
    datum: PropTypes.any,
    desc: PropTypes.string,
    dx: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string,
      PropTypes.func
    ]),
    dy: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string,
      PropTypes.func
    ]),
    events: PropTypes.object,
    index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    inline: PropTypes.bool,
    labelPlacement: PropTypes.oneOf(["parallel", "perpendicular", "vertical"]),
    lineHeight: PropTypes.oneOfType([
      PropTypes.string,
      CustomPropTypes.nonNegative,
      PropTypes.func
    ]),
    origin: PropTypes.shape({ x: CustomPropTypes.nonNegative, y: CustomPropTypes.nonNegative }),
    polar: PropTypes.bool,
    renderInPortal: PropTypes.bool,
    scale: PropTypes.shape({ x: CustomPropTypes.scale, y: CustomPropTypes.scale }),
    style: PropTypes.oneOfType([
      PropTypes.object,
      PropTypes.array
    ]),
    text: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number,
      PropTypes.func,
      PropTypes.array
    ]),
    textAnchor: PropTypes.oneOfType([
      PropTypes.oneOf([
        "start",
        "middle",
        "end",
        "inherit"
      ]),
      PropTypes.func
    ]),
    title: PropTypes.string,
    transform: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object,
      PropTypes.func
    ]),
    verticalAnchor: PropTypes.oneOfType([
      PropTypes.oneOf([
        "start",
        "middle",
        "end"
      ]),
      PropTypes.func
    ]),
    x: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string
    ]),
    y: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string
    ])
  };

  static defaultProps = {
    capHeight: 0.71, // Magic number from d3.
    lineHeight: 1
  };

  componentWillMount() {
    this.cacheAttributes(this.calculateAttributes(this.props));
  }

  shouldComponentUpdate(nextProps) {
    const attrs = this.calculateAttributes(nextProps);
    const { style, dx, dy, x, y, content, lineHeight, textAnchor, transform } = attrs;
    const { angle, className, datum, active } = this.props;
    if (!Collection.allSetsEqual([
      [active, nextProps.active],
      [angle, nextProps.angle],
      [className, nextProps.className],
      [x, this.x],
      [y, this.y],
      [dx, this.dx],
      [dy, this.dy],
      [lineHeight, this.lineHeight],
      [textAnchor, this.textAnchor],
      [transform, this.transform],
      [content, this.content],
      [style, this.style],
      [datum, nextProps.datum]
    ])) {
      this.cacheAttributes(attrs);
      return true;
    }
    return false;
  }

  cacheAttributes(attrs) {
    const { style, dx, dy, x, y, content, textAnchor, transform, lineHeight } = attrs;
    this.style = style;
    this.dx = dx;
    this.dy = dy;
    this.x = x;
    this.y = y;
    this.content = content;
    this.textAnchor = textAnchor;
    this.lineHeight = lineHeight;
    this.transform = transform;
  }

  calculateAttributes(props) {
    const style = this.getStyles(props);
    const lineHeight = this.getHeight(props, "lineHeight");
    const textAnchor = props.textAnchor ?
      Helpers.evaluateProp(props.textAnchor, props.datum) : "start";
    const content = this.getContent(props);
    const dx = props.dx ? Helpers.evaluateProp(props.dx, props.datum) : 0;
    const dy = this.getDy(props, style, content, lineHeight);
    const transform = this.getTransform(props, style);
    const x = typeof props.x !== "undefined" ? props.x : this.getPosition(props, "x");
    const y = typeof props.y !== "undefined" ? props.y : this.getPosition(props, "y");
    return {
      style, dx, dy, content, lineHeight, textAnchor, transform, x, y
    };
  }

  getPosition(props, dimension) {
    if (!props.datum) {
      return 0;
    }
    const scaledPoint = Helpers.scalePoint(props, props.datum);
    return scaledPoint[dimension];
  }

  getStyle(props, style) {
    style = style ? merge({}, defaultStyles, style) : defaultStyles;
    const datum = props.datum || props.data;
    const baseStyles = Helpers.evaluateStyle(style, datum, props.active);
    return assign({}, baseStyles, { fontSize: this.getFontSize(baseStyles) });
  }

  getStyles(props) {
    return Array.isArray(props.style) ?
      props.style.map((style) => this.getStyle(props, style)) : [this.getStyle(props, props.style)];
  }

  getHeight(props, type) {
    const datum = props.datum || props.data;
    return Helpers.evaluateProp(props[type], datum, props.active);
  }

  getContent(props) {
    if (props.text === undefined || props.text === null) {
      return [" "];
    }
    const datum = props.datum || props.data;
    if (Array.isArray(props.text)) {
      return props.text.map((line) => Helpers.evaluateProp(line, datum, props.active));
    }
    const child = Helpers.evaluateProp(props.text, datum, props.active);
    return `${child}`.split("\n");
  }

  getDy(props, style, content, lineHeight) { //eslint-disable-line max-params
    style = Array.isArray(style) ? style[0] : style;
    const fontSize = style.fontSize;
    const datum = props.datum || props.data;
    const dy = props.dy ? Helpers.evaluateProp(props.dy, datum) : 0;
    const length = content.length;
    const capHeight = this.getHeight(props, "capHeight");
    const verticalAnchor = style.verticalAnchor || props.verticalAnchor;
    const anchor = verticalAnchor ?
      Helpers.evaluateProp(verticalAnchor, datum) : "middle";
    switch (anchor) {
    case "end":
      return dy + (capHeight / 2 + (0.5 - length) * lineHeight) * fontSize;
    case "middle":
      return dy + (capHeight / 2 + (0.5 - length / 2) * lineHeight) * fontSize;
    default:
      return dy + (capHeight / 2 + lineHeight / 2) * fontSize;
    }
  }

  getTransform(props, style) {
    const { datum, x, y, polar } = props;
    const defaultAngle = polar ? LabelHelpers.getPolarAngle(props) : 0;
    const angle = style.angle || props.angle || defaultAngle;
    const transform = props.transform || style.transform;
    const transformPart = transform && Helpers.evaluateProp(transform, datum);
    const rotatePart = angle && { rotate: [angle, x, y] };
    return transformPart || angle ?
      Style.toTransformString(transformPart, rotatePart) : undefined;
  }

  getFontSize(style) {
    const baseSize = style && style.fontSize;
    if (typeof baseSize === "number") {
      return baseSize;
    } else if (baseSize === undefined || baseSize === null) {
      return defaultStyles.fontSize;
    } else if (typeof baseSize === "string") {
      const fontSize = +baseSize.replace("px", "");
      if (!isNaN(fontSize)) {
        return fontSize;
      } else {
        Log.warn("fontSize should be expressed as a number of pixels");
        return defaultStyles.fontSize;
      }
    }
    return defaultStyles.fontSize;
  }

  // Overridden in victory-core-native
  renderElements(props) {
    const textProps = {
      dx: this.dx, dy: this.dy, x: this.x, y: this.y,
      transform: this.transform, className: props.className
    };
    return (
      <text {...textProps}
        {...props.events}
      >
        {this.props.title && <title>{this.props.title}</title>}
        {this.props.desc && <desc>{this.props.desc}</desc>}
        {this.content.map((line, i) => {
          const style = this.style[i] || this.style[0];
          const lastStyle = this.style[i - 1] || this.style[0];
          const fontSize = (style.fontSize + lastStyle.fontSize) / 2;
          const textAnchor = style.textAnchor || this.textAnchor;
          const dy = i && !props.inline ? (this.lineHeight * fontSize) : undefined;
          const x = !props.inline ? props.x : undefined;
          return (
            <tspan key={i} x={x} dy={dy} dx={this.dx} style={style} textAnchor={textAnchor}>
              {line}
            </tspan>
          );
        })}
      </text>
    );
  }

  render() {
    const label = this.renderElements(this.props);
    return this.props.renderInPortal ? <VictoryPortal>{label}</VictoryPortal> : label;
  }
}
<|endoftext|>


<commit_before>module.exports = {
  //---------------------------------------------------------------------
  // Created by General Wrex
  // Add URL to json object,
  // add path to the object you want from the json
  // set your variable type and name
  // example URL: https://api.github.com/repos/HellionCommunity/HellionExtendedServer/releases/latest
  // example Path: name
  // will return object.name
  // in this example the variable would contain "[DEV] Version 1.2.2.3"
  //---------------------------------------------------------------------

  //---------------------------------------------------------------------
  // Action Name
  //
  // This is the name of the action displayed in the editor.
  //---------------------------------------------------------------------

  name: "Parse From Stored Json",

  //---------------------------------------------------------------------
  // Action Section
  //
  // This is the section the action will fall into.
  //---------------------------------------------------------------------

  section: "JSON Things",

  //---------------------------------------------------------------------
  // DBM Mods Manager Variables (Optional but nice to have!)
  //
  // These are variables that DBM Mods Manager uses to show information
  // about the mods for people to see in the list.
  //---------------------------------------------------------------------

  // Who made the mod (If not set, defaults to "DBM Mods")
  author: "General Wrex, SeikiMatt, TheMonDon",

  // The version of the mod (Defaults to 1.0.0)
  version: "1.9.6",

  // A short description to show on the mod line for this mod (Must be on a single line)
  short_description: "Parse from Stored JSON.", //Added desc ~TheMonDon

  // If it depends on any other mods by name, ex: WrexMODS if the mod uses something from WrexMods
  depends_on_mods: ["WrexMODS"],

  //---------------------------------------------------------------------

  //---------------------------------------------------------------------
  // Action Subtitle
  //
  // This function generates the subtitle displayed next to the name.
  //---------------------------------------------------------------------

  subtitle: function(data) {
    return `${data.varName}`;
  },

  //---------------------------------------------------------------------
  // Action Storage Function
  //
  // Stores the relevant variable info for the editor.
  //---------------------------------------------------------------------

  variableStorage: function(data, varType) {
    const type = parseInt(data.storage);
    if (type !== varType) return;

    if (varType == typeof object) return [data.varName, "JSON Object"];
    else {
      return [data.varName, "JSON " + varType + " Value"];
    }
  },

  //---------------------------------------------------------------------
  // Action Fields
  //
  // These are the fields for the action. These fields are customized
  // by creating elements with corresponding IDs in the HTML. These
  // are also the names of the fields stored in the action's JSON data.
  //---------------------------------------------------------------------

  fields: ["behavior", "jsonObjectVarName", "path", "storage", "varName"],

  //---------------------------------------------------------------------
  // Command HTML
  //
  // This function returns a string containing the HTML used for
  // editing actions.
  //
  // The "isEvent" parameter will be true if this action is being used
  // for an event. Due to their nature, events lack certain information,
  // so edit the HTML to reflect this.
  //
  // The "data" parameter stores constants for select elements to use.
  // Each is an array: index 0 for commands, index 1 for events.
  // The names are: sendTargets, members, roles, channels,
  //                messages, servers, variables
  //---------------------------------------------------------------------

html: function(isEvent, data) {
return `

			Mod Info:

			Authors: ${this.author}

			Stored JSON Variable Name:
			<input id="jsonObjectVarName" class="round" type="text">

			JSON Path: (supports the usage of JSON Path (Regex))

			<input id="path" class="round";" type="text">

			Store In:
			<select id="storage" class="round" onchange="glob.variableChange(this, 'varNameContainer')">
			${data.variables[0]}
			</select>

			Variable Name:
			<input id="varName" class="round" type="text" style="width: 100%;">

			End Behavior:
			<select id="behavior" class="round";>
				<option value="0" selected>Call Next Action Automatically</option>
				<option value="1">Do Not Call Next Action</option>
			</select>

			Debug Mode:
			<select id="debugMode" class="round">
				<option value="0" selected>Disabled</option>
				<option value="1">Enabled</option>
			</select>

`;},

  //---------------------------------------------------------------------
  // Action Editor Init Code
  //
  // When the HTML is first applied to the action editor, this code
  // is also run. This helps add modifications or setup reactionary
  // functions for the DOM elements.
  //---------------------------------------------------------------------

  init: function() {
    const { glob, document } = this;
    glob.variableChange(document.getElementById("storage"), "varNameContainer");
  },

  //---------------------------------------------------------------------
  // Action Bot Function
  //
  // This is the function for the action within the Bot's Action class.
  // Keep in mind event calls won't have access to the "msg" parameter,
  // so be sure to provide checks for variable existance.
  //---------------------------------------------------------------------

  action: function(cache) {
    const WrexMODS = this.getWrexMods();
    const data = cache.actions[cache.index];
    let result;
    const varName = this.evalMessage(data.varName, cache);
    const storage = parseInt(data.storage);
    const jsonObjectVarName = this.evalMessage(data.jsonObjectVarName, cache);
    const path = this.evalMessage(data.path, cache);
    const jsonRaw = this.getVariable(storage, jsonObjectVarName, cache);
	const DEBUG = parseInt(data.debugMode);

    if (typeof jsonRaw !== "object") {
      var jsonData = JSON.parse(jsonRaw);
    } else {
      var jsonData = jsonRaw;
    }

    try {
      if (path && jsonData) {
        var outData = WrexMODS.jsonPath(jsonData, path);

        // if it dont work, try to go backwards one path
        if (outData == false) {
          outData = WrexMODS.jsonPath(jsonData, "$." + path);
        }

        // if it still dont work, try to go backwards two paths
        if (outData == false) {
          outData = WrexMODS.jsonPath(jsonData, "$.." + path);
        }

        if(DEBUG) console.log(outData);

        try {
          var test = JSON.parse(JSON.stringify(outData));
        } catch (error) {
          var errorJson = JSON.stringify({ error: error, success: false });
          this.storeValue(errorJson, storage, varName, cache);
          console.error(error.stack ? error.stack : error);
        }

        var outValue = eval(JSON.stringify(outData), cache);

        if (outData.success != null || outValue.success != null) {
          var errorJson = JSON.stringify({
            error: "error",
            statusCode: 0,
            success: false
          });
          this.storeValue(errorJson, storage, varName, cache);
          console.log("WebAPI Parser: Error Invalid JSON, is the Path set correctly? [" + path + "]");
        } else {
          if (outValue.success != null || !outValue) {
            var errorJson = JSON.stringify({
              error: error,
              statusCode: statusCode,
              success: false
            });
            this.storeValue(errorJson, storage, varName, cache);
            console.log("WebAPI Parser: Error Invalid JSON, is the Path set correctly? [" + path + "]");
          } else {
            this.storeValue(outValue, storage, varName, cache);
            if(DEBUG) console.log("WebAPI Parser: JSON Data values starting from [" + path + "] stored to: [" + varName + "]");
          }
        }
      }
    } catch (error) {
      var errorJson = JSON.stringify({
        error: error,
        statusCode: 0,
        success: false
      });
      this.storeValue(errorJson, storage, varName, cache);
      console.error("WebAPI Parser: Error: " + errorJson + " stored to: [" + varName + "]");
    }

    if (data.behavior === "0") {
      this.callNextAction(cache);
    }
  },

  //---------------------------------------------------------------------
  // Action Bot Mod
  //
  // Upon initialization of the bot, this code is run. Using the bot's
  // DBM namespace, one can add/modify existing functions if necessary.
  // In order to reduce conflictions between mods, be sure to alias
  // functions you wish to overwrite.
  //---------------------------------------------------------------------

  mod: function(DBM) {}
}; // End of module
<commit_msg>Fix misalignment \ rmv store in nothing<commit_after>module.exports = {
  //---------------------------------------------------------------------
  // Created by General Wrex
  // Add URL to json object,
  // add path to the object you want from the json
  // set your variable type and name
  // example URL: https://api.github.com/repos/HellionCommunity/HellionExtendedServer/releases/latest
  // example Path: name
  // will return object.name
  // in this example the variable would contain "[DEV] Version 1.2.2.3"
  //---------------------------------------------------------------------

  //---------------------------------------------------------------------
  // Action Name
  //
  // This is the name of the action displayed in the editor.
  //---------------------------------------------------------------------

  name: "Parse From Stored Json",

  //---------------------------------------------------------------------
  // Action Section
  //
  // This is the section the action will fall into.
  //---------------------------------------------------------------------

  section: "JSON Things",

  //---------------------------------------------------------------------
  // DBM Mods Manager Variables (Optional but nice to have!)
  //
  // These are variables that DBM Mods Manager uses to show information
  // about the mods for people to see in the list.
  //---------------------------------------------------------------------

  // Who made the mod (If not set, defaults to "DBM Mods")
  author: "General Wrex, SeikiMatt, TheMonDon",

  // The version of the mod (Defaults to 1.0.0)
  version: "1.9.6",

  // A short description to show on the mod line for this mod (Must be on a single line)
  short_description: "Parse from Stored JSON.", //Added desc ~TheMonDon

  // If it depends on any other mods by name, ex: WrexMODS if the mod uses something from WrexMods
  depends_on_mods: ["WrexMODS"],

  //---------------------------------------------------------------------

  //---------------------------------------------------------------------
  // Action Subtitle
  //
  // This function generates the subtitle displayed next to the name.
  //---------------------------------------------------------------------

  subtitle: function(data) {
    return `${data.varName}`;
  },

  //---------------------------------------------------------------------
  // Action Storage Function
  //
  // Stores the relevant variable info for the editor.
  //---------------------------------------------------------------------

  variableStorage: function(data, varType) {
    const type = parseInt(data.storage);
    if (type !== varType) return;

    if (varType == typeof object) return [data.varName, "JSON Object"];
    else {
      return [data.varName, "JSON " + varType + " Value"];
    }
  },

  //---------------------------------------------------------------------
  // Action Fields
  //
  // These are the fields for the action. These fields are customized
  // by creating elements with corresponding IDs in the HTML. These
  // are also the names of the fields stored in the action's JSON data.
  //---------------------------------------------------------------------

  fields: ["behavior", "jsonObjectVarName", "path", "storage", "varName"],

  //---------------------------------------------------------------------
  // Command HTML
  //
  // This function returns a string containing the HTML used for
  // editing actions.
  //
  // The "isEvent" parameter will be true if this action is being used
  // for an event. Due to their nature, events lack certain information,
  // so edit the HTML to reflect this.
  //
  // The "data" parameter stores constants for select elements to use.
  // Each is an array: index 0 for commands, index 1 for events.
  // The names are: sendTargets, members, roles, channels,
  //                messages, servers, variables
  //---------------------------------------------------------------------

html: function(isEvent, data) {
return `

			Mod Info:

			Authors: ${this.author}

				Stored JSON Variable Name:

			<input id="jsonObjectVarName" class="round" type="text">

			JSON Path: (supports the usage of JSON Path (Regex))

			<input id="path" class="round" ;" type="text">

				Store In:

			<select id="storage" class="round" onchange="glob.variableChange(this, 'varNameContainer')">
				${data.variables[1]}
			</select>

				Variable Name:

			<input id="varName" class="round" type="text">

				End Behavior:

			<select id="behavior" class="round" ;>
				<option value="0" selected>Call Next Action Automatically</option>
				<option value="1">Do Not Call Next Action</option>
			</select>

				Debug Mode:

			<select id="debugMode" class="round">
				<option value="0" selected>Disabled</option>
				<option value="1">Enabled</option>
			</select>

`;},

  //---------------------------------------------------------------------
  // Action Editor Init Code
  //
  // When the HTML is first applied to the action editor, this code
  // is also run. This helps add modifications or setup reactionary
  // functions for the DOM elements.
  //---------------------------------------------------------------------

  init: function() {
    const { glob, document } = this;
    glob.variableChange(document.getElementById("storage"), "varNameContainer");
  },

  //---------------------------------------------------------------------
  // Action Bot Function
  //
  // This is the function for the action within the Bot's Action class.
  // Keep in mind event calls won't have access to the "msg" parameter,
  // so be sure to provide checks for variable existance.
  //---------------------------------------------------------------------

  action: function(cache) {
    const WrexMODS = this.getWrexMods();
    const data = cache.actions[cache.index];
    let result;
    const varName = this.evalMessage(data.varName, cache);
    const storage = parseInt(data.storage);
    const jsonObjectVarName = this.evalMessage(data.jsonObjectVarName, cache);
    const path = this.evalMessage(data.path, cache);
    const jsonRaw = this.getVariable(storage, jsonObjectVarName, cache);
	const DEBUG = parseInt(data.debugMode);

    if (typeof jsonRaw !== "object") {
      var jsonData = JSON.parse(jsonRaw);
    } else {
      var jsonData = jsonRaw;
    }

    try {
      if (path && jsonData) {
        var outData = WrexMODS.jsonPath(jsonData, path);

        // if it dont work, try to go backwards one path
        if (outData == false) {
          outData = WrexMODS.jsonPath(jsonData, "$." + path);
        }

        // if it still dont work, try to go backwards two paths
        if (outData == false) {
          outData = WrexMODS.jsonPath(jsonData, "$.." + path);
        }

        if(DEBUG) console.log(outData);

        try {
          var test = JSON.parse(JSON.stringify(outData));
        } catch (error) {
          var errorJson = JSON.stringify({ error: error, success: false });
          this.storeValue(errorJson, storage, varName, cache);
          console.error(error.stack ? error.stack : error);
        }

        var outValue = eval(JSON.stringify(outData), cache);

        if (outData.success != null || outValue.success != null) {
          var errorJson = JSON.stringify({
            error: "error",
            statusCode: 0,
            success: false
          });
          this.storeValue(errorJson, storage, varName, cache);
          console.log("WebAPI Parser: Error Invalid JSON, is the Path set correctly? [" + path + "]");
        } else {
          if (outValue.success != null || !outValue) {
            var errorJson = JSON.stringify({
              error: error,
              statusCode: statusCode,
              success: false
            });
            this.storeValue(errorJson, storage, varName, cache);
            console.log("WebAPI Parser: Error Invalid JSON, is the Path set correctly? [" + path + "]");
          } else {
            this.storeValue(outValue, storage, varName, cache);
            if(DEBUG) console.log("WebAPI Parser: JSON Data values starting from [" + path + "] stored to: [" + varName + "]");
          }
        }
      }
    } catch (error) {
      var errorJson = JSON.stringify({
        error: error,
        statusCode: 0,
        success: false
      });
      this.storeValue(errorJson, storage, varName, cache);
      console.error("WebAPI Parser: Error: " + errorJson + " stored to: [" + varName + "]");
    }

    if (data.behavior === "0") {
      this.callNextAction(cache);
    }
  },

  //---------------------------------------------------------------------
  // Action Bot Mod
  //
  // Upon initialization of the bot, this code is run. Using the bot's
  // DBM namespace, one can add/modify existing functions if necessary.
  // In order to reduce conflictions between mods, be sure to alias
  // functions you wish to overwrite.
  //---------------------------------------------------------------------

  mod: function(DBM) {}
}; // End of module
<|endoftext|>