Source: main.js

'use strict'
import kindOf from 'kind-of'
import dayjs from 'dayjs'
import temporal from 'temporal'
const MAX_ARRAY_INDEX = 2 ** 32 - 1
const MIN_NEGA_INDEX = -(MAX_ARRAY_INDEX + 1)
/**
 * @classdesc
 * @augments Array
 * arrays of __functions only__
 */
class Farr extends Array {
  /**
   * after - schedule the next terminal command to occur after __t__ milliseconds
   * - chainable
   * @method
   * @memberof Farr#
   * @name after
   * @param {(String|Number)} [t] the millisecond delay after which the next terminal command should occur...if a non-Number object is provided, it will be cast to Number
   * @return {Farr} this instance (Proxy)
   * @tutorial after
   */
  #after = (t) => {
    if (['string'].includes(kindOf(t))) {
      t = Number(t)
    }
    // console.log(t)
    if (typeof t === 'number') {
      this.#commands.t0 = () => {
        return (t > 0) ? t : Farr.baseCommands.t0
      }
    }

    return this.#P
  }
  #premapper = null
  #generatedS = Symbol('generated')
  #isGenerated = f => typeof f === 'function' && f[this.#generatedS] === true
  #funcWrapper = u => {
    // premap the value if premapper set
    const v = this.#premapper ? this.#premapper(u) : u
    // wrap any non-function values
    return typeof v === 'function' ? v : Object.defineProperty(() => v, this.#generatedS, {
      value: true,
      configurable: false,
      enumerable: false,
      writable: false
    })
  }
  /**
   * premap - set a function that maps any value given to it
   * - chainable
   * @method
   * @memberof Farr#
   * @name premap
   * @param {(function)} [f] the mapping function. called with the value, should return something
   * @return {Farr} this instance (Proxy)
   * @tutorial premap
   */
  #premap = (f) => {
    this.#premapper = (typeof f === 'function') ? f : null
    return this.#P
  }
  /**
   * at - schedule the next terminal command to occur at date/time __t__
   * - chainable
   * @method
   * @memberof Farr#
   * @name at
   * @param {(String|Number|date|dayjs)} [t] the date/time at which the next terminal command should occur...if a non-dayjs object is provided, it will be cast to dayjs
   * @return {Farr} this instance (Proxy)
   * @tutorial at
   */
  #at = (t) => {
    if (['string', 'number', 'date'].includes(kindOf(t))) {
      t = dayjs(t)
    }
    if (t instanceof dayjs) {
      this.#commands.t0 = () => {
        const now = dayjs()
        return (t.isAfter(now)) ? t - dayjs() : Farr.baseCommands.t0
      }
    }
    // console.log(this.#commands);
    return this.#P
  }
  // the current commands for the next terminal function call
  #commands = Object.assign({}, Farr.baseCommands)
  /**
   * get this.#commands as a JSON object]
   *
   * @method
   * @memberof Farr#
   * @name commandJson
   */
  #commandJson = () => {
    const o = Object.assign({}, this.#commands)
    for (let k in o) {
      if (o.hasOwnProperty(k)) {
        o[k] = (typeof o[k] === 'function') ? o[k]() : o[k]
      }
      return JSON.stringify(o)
    }
  }
  // constrain a given array index
  #constrainIndex = i => (Math.sign(i) === -1) ? this.length + Number(i) : i
  /**
   * reset the current this.#commands
   *
   * @method
   * @memberof Farr#
   * @name clearCommands
   */
  #clearCommands = () => {
    Object.assign(this.#commands, Farr.baseCommands)
  }
  /**
   * halt current activities, including
   * 1. temporal tasks, and
   * 2. any timers
   *
   * @method
   * @memberof Farr#
   * @name halt
   */
  #halt = () => {
    temporal.stop()
    this.#timerPool.forEach(timer => clearTimeout(timer))
  }
  // controls to be exposed
  #controls = new Map([
    ['clearCommands', this.#clearCommands.bind(this)],
    ['commandJson', this.#commandJson.bind(this)],
    ['halt', this.#halt.bind(this)]
  ])
  /**
   * nCycles - schedule the next terminal command to occur __n__ times
   * - chainable
   * @method
   * @memberof Farr#
   * @name nCycles
   * @param {Number} [n=1] the number of times the next terminal command should occur...
   * @return {Farr} this instance (Proxy)
   * @tutorial nCycles
   */
  #nCycles = (n) => {
    n = (kindOf(n) === 'number') ? n : 1
    this.#commands.nCycles = n
    return this.#P
  }
  // keys for parsing non terminal (chainable) commands
  #nonterminals = new Map([
    ['after', this.#after],
    ['at', this.#at],
    ['nCycles', this.#nCycles],
    ['premap', this.#premap]
  ])
  // a Proxy to this
  #P = null
  // keys for parsing terminal commands
  #terminals = new Map([
    ['all', this.all],
    ['cascade', this.cascade],
    ['periodic', this.periodic]
  ])
  // the timers created by this instance
  #timerPool = new Set()
  // get a task-wrapped function corresponding to those in this.#terminals
  #wrappedTerminal = (terminal) => {
    const { t0, nCycles } = this.#commands
    const clearCommands = this.#clearCommands
    const dt = (typeof t0 === 'function') ? t0() : null
    const f = terminal.bind(this)
    return async (arg) => {
      clearCommands()
      const cycle = async () => {
        // const C = new Array(nCycles).fill(f.bind(this))
        let s = arg
        for (let i = 0; i < nCycles; i++) {
          if (i === 0) {
            s = (s) ? await f(s) : await f()
          } else {
            s = await f({ s })
          }
        }
        return s
      }
      return (dt) ? new Promise((resolve) => {
        const timeout = setTimeout(() => {
          // console.log(`@t${bigint()}, timeout`)
          resolve(cycle(timeout))
          clearTimeout(timeout)
          this.#timerPool.delete(timeout)
        }, dt)
        this.#timerPool.add(timeout)
        // console.log(`@t${bigint()}, timeout`)
      }) : Promise.resolve(cycle())
    }
  }
  #getters = new Map([
    /**
     * get generated - given index n in this instance, get a boolean telling whether corresponding element was generated by this instance
     * @method
     * @memberof Farr#
     * @param {(number)} n index to check
     * @return {(boolean|Array<boolean>)}
     */
    ['generated', (n) => Farr.isSafeIndex(n) ? this.#isGenerated(this[n]) : Array.from(this.map((f) => this.#isGenerated(f)))],
    /**
     * get givenFunc - given index n in this instance, get a boolean telling whether corresponding element was not generated by this instance
     * @method
     * @memberof Farr#
     * @param {(number)} n index to check
     * @return {(boolean|Array<boolean>)}
     */
    ['givenFunc', (n) => {
      const nn = this.generated(n)
      const inv = (a) => !a
      return Array.isArray(nn) ? nn.map(inv) : inv(nn)
    }]
  ])
  #exposeGetters = () => {
    const v = Object.fromEntries([...this.#getters].map((ar) => [ar[0], {
      enumerable: false,
      get: () => ar[1]
    }]))
    return Object.defineProperties(this, v)
  }

  /**
   * constructor - create a Farr instance
   *
   * @param  {(Array|number)} [arr] if an Array, its elements will be used to populate the new instance. if a number, it sets the instance's length -- Array
   * @return {Array} this instance (Proxy)
   * @tutorial constructor
   */
  constructor (arr) {
    super()
    this.#exposeGetters()
    this.#P = new Proxy(this, {
      set (target, prop, value) {
        if (Farr.isSafeIndex(prop)) {
          prop = target.#constrainIndex(prop)
          return Reflect.set(target, prop, target.#funcWrapper(value))
        }
        return Reflect.set(...arguments)
      },
      get (target, prop, receiver) {
        if (Farr.isSafeIndex(prop)) {
          prop = target.#constrainIndex(prop)
        } else if (kindOf(prop) === 'string') {
          if (target.#nonterminals.has(prop)) {
            return (...args) => target.#nonterminals.get(prop)(...args)
          } else if (target.#terminals.has(prop)) {
            const terminal = target.#terminals.get(prop)
            return target.#wrappedTerminal(terminal)
          } else if (target.#controls.has(prop)) {
            return target.#controls.get(prop)
          }
        }
        return Reflect.get(target, prop, receiver)
      }
    })
    if (Array.isArray(arr)) {
      this.#P.push(...arr)
    } else if (Number.isInteger(arr)) {
      this.length = arr
    }
    return this.#P
  }

  /**
   * async all -  A unary asynchronous instance method that
   * - calls functions in order,
   * - resolves when all the functions called return, and
   * - accepts a parameter __arg__ that contains
   *     1. a starting value __arg.s__ to be passed to all functions

   * -
   * @async
   * @param  {object} [arg = {s: undefined}] argument
   * @return {Promise} result of Promise.all call on this' function elements
   * @tutorial all
   */
  async all (arg = { s: undefined }) {
    let { s } = arg
    const getS = (i) => (Array.isArray(s) && s.length >= this.length) ? s[i] : s
    return Promise.all(this.map((f, i) => f(getS(i))))
  }

  /**
   * async cascade -  A unary asynchronous instance method that
   * - calls functions in order,
   * - accepts a parameter __arg__ that contains
   *     1. a starting value __arg.s__ to be passed to the first function
   * - mutates the given start value
   * @async
   * @param  {object} [arg = {s: undefined}] argument
   * @return {Promise} result of Promise.all call on this' function elements
   * @tutorial cascade
   */
  async cascade (arg = { s: undefined }) {
    let { s } = arg
    for await (let f of this) {
      s = await f(s)
    }
    return s
  }

  /**
   * get head - get the first element
   *
   * @return {function}  the 0th element
   */
  get head () {
    return this[0]
  }

  /**
   * async periodic -  A unary asynchronous instance method that
   * - calls functions in order,
   * - accepts a parameter __arg__ that contains
   *     1. a starting value __arg.s__ to be passed to all functions
   *     2. a millisecond interval value __arg.delay__
   *
   * @async
   * @param  {object} [arg = {delay: 233, s: undefined}] argument
   * @return {Promise} eventual result of call on this' function elements
   * @tutorial periodic
   */
  async periodic (arg = { delay: 233, s: undefined }) {
    const { s, delay } = arg
    const { length } = this
    const results = new Array(length)
    const tasks = new Array(length)

    for (let i = 0; i < length; i++) {
      const f = this[i]
      tasks[i] = {
        delay,
        task: () => {
          results[i] = Promise.resolve(f(s))
            .catch(err => err)
        }
      }
    }
    // console.log(tasks)
    // const queue = await this.temporal.queue(tasks)
    await this.temporal.queue(tasks)
    // console.log(queue)
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(Promise.all(results)), delay * length)
    })
  }

  /**
   * get tail - get the last element
   *
   * @return {function}  the last element
   */
  get tail () {
    return this[-1]
  }

  /**
   * get temporal - get the temporal instance used by this instance
   *
   * @return {object} the temporal instance
   */
  get temporal () {
    return temporal
  }
}

Object.defineProperties(Farr, {
  /**
   * the default commands
   * @memberof Farr
   * @name baseCommands
   * @static
   */
  baseCommands: {
    value: Object.freeze({
      t0: 0,
      nCycles: 1
    }),
    enumerable: true,
    writable: false,
    configurable: false
  },
  /**
   * determine whether a number __d__ can be used as an array index
   * @method
   * @name isSafeIndex
   * @param  {Number} d the number to test
   * @return {Boolean}   true if d is a usable array index
   * @memberof Farr
   * @static
   */
  isSafeIndex: {
    value: (d) => {
      return (typeof d !== 'symbol') && Number.isInteger(+d) && MIN_NEGA_INDEX <= d && d <= MAX_ARRAY_INDEX
    },
    enumerable: true,
    writable: false,
    configurable: false
  },
  /**
   * array containing the string keys of the non terminal functions
   *
   * @name nonTerminalKeys
   * @memberof Farr
   * @static
   */
  nonTerminalKeys: {
    value: ['after', 'at', 'nCycles', 'premap'],
    enumerable: true,
    writable: false,
    configurable: false
  }
})

export { Farr }