'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 }