signal.js

import {createSlot} from './slot'

/**
 * A signal instance that returns the current value when called as a function.
 * @namespace Signal
 * @type {Function}
 * @returns {any}
 */
const signalPrototype = {
  /**
   * Add listener to signal
   * @memberof Signal#
   * @param {Function} listener The listener to be executed when the signal dispatches
   * @param {boolean} [once=false] Executes the listener only once when set to true
   * @param {boolean} [immediate=false] Executes the listener immediately with current state
   * @returns {Slot}
   */
  add(listener, once=false, immediate=false) {
    if (typeof listener!=='function') throw 'listener is a required param of add() and should be a Function.'
    let slot
    if (!this.has(listener)) {
      slot = createSlot(listener, this, once)
      this._slots.push(slot)
    }
    immediate&&listener(...this._values)
    return slot
  },
  /**
   * Add listener to signal
   * @memberof Signal#
   * @param {Function} listener The listener to be executed when the signal dispatches
   * @returns {Slot}
   */
  addOnce(listener) {
    return this.add(listener, true)
  },
  /**
   * Remove all signal listeners
   * @memberof Signal#
   * @returns {Signal}
   */
  clear() {
    this._slots.forEach(slot => slot._signal = null)
    this._slots.splice(0, Number.MAX_SAFE_INTEGER)
    return this
  },
  /**
   * Dispatch the signal
   * The `values` parameter can be empty, a single value, or an array of values. The listener is executed with these values as parameters.
   * @memberof Signal#
   * @param {any[]} values
   * @returns {Signal}
   */
  dispatch(...values) {
    this._values.splice(0, Number.MAX_SAFE_INTEGER, ...values)
    this._slots.forEach(slot => {
      slot._listener(...values)
      slot.once&&slot.remove()
    })
    return this
  },
  /**
   * Test if listener was added
   * @memberof Signal#
   * @returns {boolean}
   */
  has(listener){
    return this._slots.find(slot=>slot._listener===listener)!==undefined
  },
  /**
   * The current state
   * @memberof Signal#
   * @returns {any[]}
   */
  get state(){
    return this._values
  },
  /**
   * The current state as a string
   * @memberof Signal#
   * @returns {string}
   */
  toString(){
    return this()?.toString()
  }
}

/**
 * Factory method to create a signal
 * @param {any[]} [values] Values to initialise the signals state with
 * @returns {Signal}
 */
export function createSignal(...values) {
  const signalProperties = {
    /**
     * @memberof Signal#
     * @type {any}
     * @private
     */
    _values: {
      writable: false, value: values
    },
    /**
     * @memberof Signal#
     * @type {Slot[]}
     * @private
     */
    _slots: {
      writable: false, value: []
    },
    /**
     The number of listeners added
     * @memberof Signal#
     * @type {number}
     * @readonly
     */
    length: {
      get: function(){ return this._slots.length }
    }
  }
  const inst = Object.create(signalPrototype, signalProperties)
  return Object.defineProperties(() => {
    const {_values} = inst
    return _values.length===1?_values[0]:_values
  }, Object.keys({...signalPrototype, ...signalProperties}).reduce((acc, key) => {
    acc[key] = {get: () => inst[key]}
    return acc
  },{}))
}