Home Reference Source Test

src/events.js

import {
  dataView,
  isArray,
  isFunction,
  isInt,
  isString,
} from './common/utils'

import {
  prepareAddress,
  prepareRegExPattern,
} from './common/helpers'

import Bundle from './bundle'
import Message from './message'
import Packet from './packet'

/**
 * Default options
 * @private
 */
const defaultOptions = {
  discardLateMessages: false,
}

/**
 * EventHandler to notify listeners on matching OSC messages and
 * status changes of plugins
 */
export default class EventHandler {
  /**
   * Create an EventHandler instance
   * @param {object} options Custom options
   */
  constructor(options) {
    /**
     * @type {object} options
     * @private
     */
    this.options = { ...defaultOptions, ...options }
    /**
     * @type {array} addressHandlers
     * @private
     */
    this.addressHandlers = []
    /**
     * @type {object} eventHandlers
     * @private
     */
    this.eventHandlers = {
      open: [],
      error: [],
      close: [],
    }
    /**
     * @type {number} uuid
     * @private
     */
    this.uuid = 0
  }

  /**
   * Internally used method to dispatch OSC Packets. Extracts
   * given Timetags and dispatches them accordingly
   * @param {Packet} packet
   * @param {*} [rinfo] Remote address info
   * @return {boolean} Success state
   * @private
   */
  dispatch(packet, rinfo) {
    if (!(packet instanceof Packet)) {
      throw new Error('OSC EventHander dispatch() accepts only arguments of type Packet')
    }

    if (!packet.value) {
      throw new Error('OSC EventHander dispatch() can\'t read empty Packets')
    }

    if (packet.value instanceof Bundle) {
      const bundle = packet.value

      return bundle.bundleElements.forEach((bundleItem) => {
        if (bundleItem instanceof Bundle) {
          if (bundle.timetag.value.timestamp() < bundleItem.timetag.value.timestamp()) {
            throw new Error('OSC Bundle timestamp is older than the timestamp of enclosed Bundles')
          }
          return this.dispatch(new Packet(bundleItem))
        } else if (bundleItem instanceof Message) {
          const message = bundleItem
          return this.notify(
            message.address,
            message,
            bundle.timetag.value.timestamp(),
            rinfo,
          )
        }

        throw new Error('OSC EventHander dispatch() can\'t dispatch unknown Packet value')
      })
    } else if (packet.value instanceof Message) {
      const message = packet.value
      return this.notify(message.address, message, 0, rinfo)
    }

    throw new Error('OSC EventHander dispatch() can\'t dispatch unknown Packet value')
  }

  /**
   * Internally used method to invoke listener callbacks. Uses regular
   * expression pattern matching for OSC addresses
   * @param {string} name OSC address or event name
   * @param {*} [data] The data of the event
   * @param {*} [rinfo] Remote address info
   * @return {boolean} Success state
   * @private
   */
  call(name, data, rinfo) {
    let success = false

    // call event handlers
    if (isString(name) && name in this.eventHandlers) {
      this.eventHandlers[name].forEach((handler) => {
        handler.callback(data, rinfo)
        success = true
      })

      return success
    }

    // call address handlers
    const handlerKeys = Object.keys(this.addressHandlers)
    const handlers = this.addressHandlers

    handlerKeys.forEach((key) => {
      let foundMatch = false

      const regex = new RegExp(prepareRegExPattern(prepareAddress(name)), 'g')
      const test = regex.test(key)

      // found a matching address in our callback handlers
      if (test && key.length === regex.lastIndex) {
        foundMatch = true
      }

      if (!foundMatch) {
        // try matching address from callback handlers (when given)
        const reverseRegex = new RegExp(prepareRegExPattern(prepareAddress(key)), 'g')
        const reverseTest = reverseRegex.test(name)

        if (reverseTest && name.length === reverseRegex.lastIndex) {
          foundMatch = true
        }
      }

      if (foundMatch) {
        handlers[key].forEach((handler) => {
          handler.callback(data, rinfo)
          success = true
        })
      }
    })

    return success
  }

  /**
   * Notify the EventHandler of incoming OSC messages or status
   * changes (*open*, *close*, *error*). Handles OSC address patterns
   * and executes timed messages. Use binary arrays when
   * handling directly incoming network data. Packet's or Messages can
   * also be used
   * @param {...*} args
   * The OSC address pattern / event name as string}. For convenience and
   * Plugin API communication you can also use Message or Packet instances
   * or ArrayBuffer, Buffer instances (low-level access). The latter will
   * automatically be unpacked
   * When using a string you can also pass on data as a second argument
   * (any type). All regarding listeners will be notified with this data.
   * As a third argument you can define a javascript timestamp (number or
   * Date instance) for timed notification of the listeners.
   * @return {boolean} Success state of notification
   *
   * @example
   * const socket = dgram.createSocket('udp4')
   * socket.on('message', (message) => {
   *   this.notify(message)
   * })
   *
   * @example
   * this.notify('error', error.message)
   *
   * @example
   * const message = new OSC.Message('/test/path', 55)
   * this.notify(message)
   *
   * @example
   * const message = new OSC.Message('/test/path', 55)
   * // override timestamp
   * this.notify(message.address, message, Date.now() + 5000)
   */
  notify(...args) {
    if (args.length === 0) {
      throw new Error('OSC EventHandler can not be called without any argument')
    }

    try {
      // check for incoming dispatchable OSC data
      if (args[0] instanceof Packet) {
        return this.dispatch(args[0], args[1])
      } else if (args[0] instanceof Bundle || args[0] instanceof Message) {
        return this.dispatch(new Packet(args[0]), args[1])
      } else if (!isString(args[0])) {
        const packet = new Packet()
        packet.unpack(dataView(args[0]))
        return this.dispatch(packet, args[1])
      }

      const name = args[0]

      // data argument
      let data = null

      if (args.length > 1) {
        data = args[1]
      }

      // timestamp argument
      let timestamp = null

      if (args.length > 2) {
        if (isInt(args[2])) {
          timestamp = args[2]
        } else if (args[2] instanceof Date) {
          timestamp = args[2].getTime()
        } else {
          throw new Error('OSC EventHandler timestamp has to be a number or Date')
        }
      }

      // remote address info
      let rinfo = null

      if (args.length >= 3) {
        rinfo = args[3]
      }

      // notify now or later
      if (timestamp) {
        const now = Date.now()

        // is message outdated?
        if (now > timestamp) {
          if (!this.options.discardLateMessages) {
            return this.call(name, data, rinfo)
          }
        }

        // notify later
        const that = this

        setTimeout(() => {
          that.call(name, data, rinfo)
        }, timestamp - now)

        return true
      }

      return this.call(name, data, rinfo)
    } catch (error) {
      this.notify('error', error)
      return false
    }
  }

  /**
   * Subscribe to a new address or event you want to listen to
   * @param {string} name The OSC address or event name
   * @param {function} callback Callback function on notification
   * @return {number} Subscription identifier (needed to unsubscribe)
   */
  on(name, callback) {
    if (!(isString(name) || isArray(name))) {
      throw new Error('OSC EventHandler accepts only strings or arrays for address patterns')
    }

    if (!isFunction(callback)) {
      throw new Error('OSC EventHandler callback has to be a function')
    }

    // get next id
    this.uuid += 1

    // prepare handler
    const handler = {
      id: this.uuid,
      callback,
    }

    // register event listener
    if (isString(name) && name in this.eventHandlers) {
      this.eventHandlers[name].push(handler)
      return this.uuid
    }

    // register address listener
    const address = prepareAddress(name)

    if (!(address in this.addressHandlers)) {
      this.addressHandlers[address] = []
    }

    this.addressHandlers[address].push(handler)

    return this.uuid
  }

  /**
   * Unsubscribe listener from event notification or address handler
   * @param {string} name The OSC address or event name
   * @param {number} subscriptionId Subscription id to identify the handler
   * @return {boolean} Success state
   */
  off(name, subscriptionId) {
    if (!(isString(name) || isArray(name))) {
      throw new Error('OSC EventHandler accepts only strings or arrays for address patterns')
    }

    if (!isInt(subscriptionId)) {
      throw new Error('OSC EventHandler subscription id has to be a number')
    }

    let key
    let haystack

    // event or address listener
    if (isString(name) && name in this.eventHandlers) {
      key = name
      haystack = this.eventHandlers
    } else {
      key = prepareAddress(name)
      haystack = this.addressHandlers
    }

    // remove the entry
    if (key in haystack) {
      return haystack[key].some((item, index) => {
        if (item.id === subscriptionId) {
          haystack[key].splice(index, 1)
          return true
        }

        return false
      })
    }

    return false
  }
}