Source: serl.js

/** @module Serl
 *  @summary Currently the only source file.
 *
 *  @description 
 *  'export class/function' chosen over an (export default) expression for explicitness:
 *
 *  @todo Make an Atom() class datatype?
 *
 *  @todo Check each METHOD... should it be .STATIC or #INSTANCE? 
 *
 *  @todo Proxy the 'console' object so that we can toggle debug levels
 *
 *  @todo serl.js should be serl.mjs - but the lousy dev server doesn't serve the right
 *  mime type 
 *
 *  @todo https://docsify.js.org/#/quickstart ?
 *
 *  @todo export class Reference ()
 */

'use strict'

/** 
 *  @typedef {integer} module:Serl.NodeIndex The primary location of values of
 *  his type is at [Node.nodeMap.counter]{@link
 *  module:Serl.'Node#nodeMap'.counter}. These are then used as keys of 
 *  [Node.nodeMap]{@link module:Serl.'Node#nodeMap'}. They are also used as the
 *  first integer value in the string representation of a [Pid]{@link
 *  module:Serl.Pid}.
 *
 *  This custom type exists only in documentation, and must be manually enforced
 *  in code, by developers.
 */

/** @typedef {integer} module:Serl.ProcIndex The primary location of values of
 *  this type is at [Node.procMap.counter]{@link
 *  module:Serl.'Node#procMap'.counter}. These are then used as the second
 *  integer value in the string representation of a [Pid]{@link
 *  module:Serl.Pid}. ** UNLIKE [NodeIndex]{@link module:Serl.NodeIndex}, these
 *  are NOT keys of [Node.procMap]{@link module:Serl.'Node#procMap'}. **
 *
 *  This custom type exists only in documentation, and must be manually enforced
 *  in code, by developers.
 */

/**
 *  @class module:Serl.Node
 *
 *  @classdesc
 *  Constructor for objects analogous to an [OTP node]{@link
 *  https://erlang.org/doc/reference_manual/distributed.html#nodes}.
 *
 *  @param {String} given Following OTP convention, a [node name]{@link
 *  https://erlang.org/doc/reference_manual/distributed.html#nodes}, 
 *  names should be 'given@host'.
 *
 *  @property {Function} nodeIndexFromNodeName {@link
 *  module:Serl.Node.nodeIndexFromNodeName}
 *
 *  @property {Function} spawn {@link
 *  module:Serl.Node#spawn}
 *
 *  @property {module:Serl.Node#host} host Should mirror OTP convention, presumably refer
 *  to a hostfile.
 *
 *  @property {module:Serl.Node#name} name 
 *
 *  @property {Map} procMap 
 *
 *  @property {Map} nodeMap A map of connected nodes.
 *
 *  @todo Rename module from Serl to OTP? (Brand reinforcement; possibly
 *  conveying a sense of false legitimacy.)
 *
 *  @todo Distributed Erlang functionality (we don't need this yet) 
 *  https://erlang.org/doc/reference_manual/distributed.html
 *
 *  @property cookie todo/unimplemented
 *  @property hidden todo/unimplemented
 */
export class Node {

    constructor (given) {

        /** 
        *   @property module:Serl.Node#host 
        *   @type {String}
        *   @todo Assign from a hostfile.
        *   @description This custom type exists only in documentation, and must
        *   be manually enforced in code, by developers.
        *   
        */
        this.host   =   'placeholderHost'

        /** 
        *   @property module:Serl.Node#name 
        *   @type {String}
        *   @todo In OTP convention, this depends on -name vs -sname flags
        *   @description Has the form <code>given@host</code>, which is based on
        *   the given-name of each call to <code>Node(given)</code>. Analogous
        *   to an OTP [node name]{@link
        *   https://erlang.org/doc/reference_manual/distributed.html#nodes}
        *
        *   This custom type exists only in documentation, and must
        *   be manually enforced in code, by developers.
        *   
        */
        this.name   =   `${given}@${this.host}`   

        /** 
        *   @namespace module:Serl.'Node#procMap'
        *
        *   @description A map of a node's local processes (any instances of
        *   [Proc]{@link module:Serl.Node.Proc} which were created by calling
        *   [spawn/n]{@link module:Serl.Node.spawn} on that node.
        *   Here we use a Map, because we anticipate frequent
        *   lookups based on given [Pid]{@link module:Serl.Pid}s. We also expect
        *   'frequent additions and removals of key-value pairs'
        *
        *   @property {module:Serl.Pid} @keys Keys in the Map should be
        *   instances of this class.
        *
        *   @property {module:Serl.Proc} @values Values in the Map should be
        *   instances of this class.
        *
        *   @property {module:Serl.ProcIndex} counter {@link
        *   module:Serl.'Node#procMap'.counter}
        *
        *   @todo update docs to remind developers to enforce
        *   nodeIndex = parseInt(nodeIndex).
        *
        *   @todo Should ProcMap have its own class? 
        */
        this.procMap    =   new Map( [ ] )  
        
        /**
        *   @property module:Serl.'Node#procMap'.counter
        *
        *   @memberof module:Serl.'Node#procMap'
        *
        *   @description This helps us avoid duplicate process-indices on the
        *   local node, when generating a Pid object. We want to use Map here,
        *   because we expect 'frequent additions and removals of key-value
        *   pairs'
        * 
        *   Each node must track all its previously spawned processes, and so we
        *   want an incrementing [Node.procMap.counter]{@link
        *   module:Serl.'Node#procMap'.counter}, to ensure that for the lifetime
        *   of a node, N, all of N's local processes have a unique ProcIndex. 
        * 
        *   In the current implementation, the upper limit seems to simply be
        *   the size of Node.procMap.counter. (In Erlang, the equivalent number
        *   is stored in 18 to 28 bits, after which numbers are reused...
        *   however in JavaScript, it seems we have almost 53 bits...)
        * 
        */
        this.procMap.counter = 0

        /** 
        *   @namespace module:Serl.'Node#nodeMap'
        *
        *   @description A map of a node's known (currently connected, and
        *   previously connected)) nodes, including itself. 
        *   Here we use a Map, because we anticipate frequent
        *   lookups based on given [NodeIndex]{@link module:Serl.NodeIndex}s. We
        *   also expect 'frequent additions and removals of key-value pairs'
        *   
        *   @property {module:Serl.NodeIndex} @keys Keys in the Map should be
        *   of this type.
        *
        *   @property { described } @values Values in the Map should be
        *    of the form <code>{ name: [NodeName]{@link module:Serl.Node#name}
        *   }</code>. 

        *
        *   @property {Serl.NodeIndex} counter {@link
        *   module:Serl.'Node#nodeMap'.counter}
        *
        *   @todo Add nodeMap.counter to make this consistent with
        *   procMap.counter; update docs to remind developers to enforce
        *   nodeIndex = parseInt(nodeIndex).
        *
        *   @todo Values should have their own class.
        *
        *   @todo Should NodeMap have its own class? 
        */
        this.nodeMap    =   new Map(    [   [0, { name:this.name }] ] )

        /**
        *   @property module:Serl.'Node#nodeMap'.counter
        *
        *   @memberof module:Serl.'Node#nodeMap'
        *
        *   @description This helps us avoid duplicate {@link Serl.NodeIndex}s on the
        *   local node, when generating a [Pid]{@link module:Serl.Pid}. 
        *
        *   Each node must track all previously known nodes, and so we want an
        *   incrementing counter, to ensure that for any single
        *   lifetime of a node, N, every other node that ever connects to N, has a
        *   unique NodeIndex from N's point of view. 
        * 
        *   In the current implementation, the upper limit of trackable nodes connected
        *   to N in one lifetime seems to simply be the size of Node.nodeMap.counter.
        *   Here we impose a convention where the local node is index=0
        *
        */
        this.nodeMap.counter = 0

    }

    /**
     *  @method module:Serl.Node.nodeIndexFromNodeName
     *  @description 
     *  Utility function, returning a <code>nodeIndex</code>, integers used as
     *  keys in {@link module:Serl.Node#nodeMap}.
     *  
     *  @param {module:Serl.Node} node An instance of the Node class.
     *  @param {module:Serl.Node#name} nodeName
     *  @returns {integer}
     */
    static nodeIndexFromNodeName (node, nodeName) {
        return Array.from( node.nodeMap.keys() )
                    .find( (key) => node.nodeMap.get(key).name == nodeName )
    }


    /**
     *  @method module:Serl.Node#spawn
     *  @description Implements various arities of spawn/n.
     *  Spawns an instance of [Proc]{@link module:Serl.Proc} on some a certain
     *  instance of [Node]{@link module:Serl.Node}. The keyword
     *  <code>this</code> in any <code>fun</code> passed to spawn/n will refer
     *  to the spawned <code>proc</code>.
     *
     *  <h5>spawn/1</h5>
     *  Spawns a Proc object on the parent node, which applies
     *  <code>fun</code> to an empty array <code>[]</code>. See
     *  <a href="http://erlang.org/doc/man/erlang.html#spawn-1">OTP docs</a>.
     *  <h6>Parameters:</h6>
     *
     *  | Name  | Type | Description |
     *  |-------|------|-------------|
     *  | fun | Function |The function which will run in this process. 
     *
     *  <h5>spawn/2</h5>
     *  (coming soon)
     *
     *  <h5>spawn/3</h5>
     *  Spawns a Proc object on the parent node, which applies a function (given and
     *  accessed via a module) to a list of arguments, in the manner of:
     *
     *  ```
     *  module[funName]( ... funArgs )</code>
     *  ```
     *  See 
     *  <a href="http://erlang.org/doc/man/erlang.html#spawn-3">OTP docs</a>.
     *  <h6>Parameters:</h6>
     *
     *  | Name  | Type | Description |
     *  |-------|------|-------------|
     *  | module | Object | An object representing a code Module, with callable methods.
     *  | funName | String | The string name of a method of <code>module</code>. 
     *  | funArgs | Array | An array of arguments to pass to <code>module[funName]</code> when the latter is called.
     *
     *  <h5>spawn/4</h5>
     *  (coming soon)
     *
     *  @todo implement the 'undefined function' error
     *  @todo refer to 'dynamic module loading' and module objects later 
     *  @todo When <code>fun</code> returns, Proc should exit formally.
     *  @todo Enforce passing objects by value (copy on call).
     *  
     */

    spawn () {
        let nodeName, procIndex, newProc, module, fun, funArgs
        switch (arguments.length) {
            case 0:
                throw Error( 'Node.spawn/0 called; no implementation.' )
                break

            case 1:

                if ( typeof arguments[0] != 'function' ) { 
                    throw Error( 'Node.spawn/1 called with a non-function' ) 
                }

                nodeName    = this.name
                procIndex   = ++this.procMap.counter
                fun         = arguments[0]

                newProc     = new Proc ( nodeName, procIndex, this )
                newProc.fun = fun

                this.procMap.set ( newProc.pid, newProc )
                newProc.fun([])
                break

            case 2:
                throw Error( 'Node.spawn/2 called; no implementation.' )
                break
            case 3:
                //  http://erlang.org/doc/man/erlang.html#spawn-3

                if ( typeof arguments[0] != 'object' ) { 
                    throw Error( `Node.spawn/3, arguments[0] called with a
                        non-object (expecting the module-object)` ) 
                }

                if ( typeof arguments[1] != 'string' ) { 
                    throw Error( `Node.spawn/3, arguments[1] called with a
                        non-string (expecting the method name as a string` ) 
                }

                if ( ! Array.isArray( arguments[2] ) ) { 
                    throw Error( `Node.spawn/3, arguments[2] called with a
                        non-array (expecting arguments for fun as an array)` ) 
                }

                nodeName        = this.name
                procIndex       = ++this.procMap.counter
                module          = arguments[0]
                fun             = module[arguments[1]]
                funArgs    = arguments[2]

                newProc     = new Proc ( nodeName, procIndex, this )
                newProc.fun = fun

                this.procMap.set ( newProc.pid, newProc )
                newProc.fun ( ... funArgs )
                break
            case 4:
                throw Error( 'Node.spawn/4 called; no implementation.' )
                break
            case 5:
                throw Error( 'Node.spawn/5 called; no implementation.' )
                break
            default:
                throw Error( 'Node.spawn/(>5) called; no implementation.' )
        }

        return newProc.pid

    } // method Node.spawn

} // class Node

/**
 *  @class module:Serl.Proc
 *
 *  @classdesc
 *  Constructor for objects analogous to an [OTP process]{@link
 *  https://erlang.org/doc/getting_started/conc_prog.html#processes}.
 *
 *  @param {module:Serl.Node#name} nodeName
 *
 *  @param {module:Serl.ProcIndex} procIndex 
 *
 *  @param {module:Serl.Node} localNode The [node]{@link module:Serl.Node} which
 *  spawns the new proc, then stores it in its [node.procMap]{@link
 *  module:Serl.Node#procMap}. 
 *
 *  This procMap should be the ONLY direct reference to the process, otherwise
 *  interactions with this process should only occur via send/receive i.e.
 *  message-passing.
 *
 *  @property {Function} toString {@link module:Serl.Proc#toString}
 *
 *  @property {Function} defaultMailHandler {@link module:Serl.Proc#defaultMailHandler}
 *
 *  @property {Function} send {@link module:Serl.Proc#send}
 *
 *  @property {Function} receive {@link module:Serl.Proc#receive}
 *
 *  @property {module:Serl.Node} node Reference to argument passed in
 *  parameter#3; doing this is questionable. To be reviewed. 
 *
 *  @property {module:Serl.NodeIndex} nodeIndex 
 *
 *  @property {module:Serl.Pid} pid Unique identifier for this proc, on this
 *  Node.
 *
 *  @property {Array} mailbox A stack for messages received by this proc.
 *
 *  @property {Function} mailHandler A method which determines how the process
 *  handles messages it receives. This may include storing them in the mailbox,
 *  checking messages already in the mailbox, executing other logic, or simply
 *  ignoring them.
 *
 *  @todo Dependence on localNode is questionable; review.
 *
 *  @todo Do we need to validate argument types here?
 *
 *  @todo Would performance improve if these were static methods? 
 *
 *  @todo 'registered processes'
 *
 */
export class Proc {

    constructor (nodeName, procIndex, localNode) {

        /**
        *   @type {module:Serl.Node}
        *   @todo Review, do we want this here?
        */
        this.node           = localNode 

        this.nodeIndex      = Node.nodeIndexFromNodeName ( localNode, nodeName)
        this.pid            = new Pid (this.nodeIndex, procIndex)
        this.mailbox        = []
        this.mailHandler    = this.defaultMailHandler
    }

    /**
     *  @method module:Serl.Proc#toString
     *  @description Overridden [toString]{@link
     *  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString}
     *  . Output is of the form:
     *
     *  ```
     *  [object Proc<0.1>]
     *  ```
     *  @todo should this be made into a static method?
     */
    toString () {
        return `[object Proc<${this.pid.nodeIndex}.${this.pid.procIndex}>]`
    }

    /**
     *  @method module:Serl.Proc#defaultMailHandler 
     *  @description When the next message come in, they will be held in
     *  [this.mailbox]{@link module:Serl.Proc#mailbox} in their order of
     *  arrival. No attempt is made to match them to any further logical branches
     *  
     */
    defaultMailHandler ( msg ) {
        //console.log( `    defaultMailHandler received a message` )
        this.mailbox.push ( msg )
    }

    /**
     *  @method module:Serl.Proc#send 
     *  @description Sends a message from this process to another process.
     *
     *  Ultimately executes:
     *
     *  ```
     *  destinationProcess.mailHandler ( validMsg )
     *  ```
     *   
     *  @param {module:Serl.Pid} dest A Pid object which addresses the local or
     *  remote process.
     *   
     *  @param {*} msg The message to be sent to the destination process.
     *  
     *  @todo Validation/Proxy/Reassignment: ensure that nothing is passed by reference.
     *  @todo Validation of <code>dest</code>? 
     *  
     */
    send ( dest, msg ) {

        switch (arguments.length) {
            case 2:
                if ( ! ( dest instanceof Pid ) ) {
                    throw Error (`Proc.send/2 called, first argument was not an
                        instance of Pid`)
                }

                //console.log(`    ${this}.send/2: supposed to send a message to ${dest}`)

                this.node.procMap.get ( dest ).mailHandler ( msg )
                 
                break
            case 3:
                throw Error( 'Proc.send/3 called; no implementation.' )
                break
            default:
                throw Error( 'Proc.send/<1 or >3 called; no implementation.' )
                break
        }

    }

    /**
     *  @method module:Serl.Proc#receive 
     *  @description 
     *  
     *  See [OTP docs]{@link
     *  http://erlang.org/doc/reference_manual/expressions.html#receive}.
     *  
     *  In the body of a function passed to [spawn/n]{@link
     *  module:Serl.Node#spawn}, usage would be in the example below, where
     *  <code>this</code> refers to the [proc]{@link module:Serl.Proc} which was
     *  spawned.
     *
     *  @see
     *  How does a process handle received messages?<br>
     *  Text:           {@link https://erlangbyexample.org/send-receive}<br>
     *  Illustrated:    {@link https://learnyousomeerlang.com/more-on-multiprocessing}
     *
     *  <pre>
     *
     *  When a process receives a message, the message is
     *  appended to the mailbox.
     *
     *  The receive block will try each message in the mailbox (one
     *  by one), against that block's sequence of patterns, until
     *  one of the messages matches a pattern.
     *
     *  When there is match, the message gets removed from
     *  the mailbox and the logic corresponding to the matched pattern
     *  will get executed. If there is no match,
     *  then that message remains in the mailbox, and the following
     *  message gets tried sequentially,
     *  against all of the receive block's patterns.
     *
     *  If no messages in the mailbox match any pattern, the process,
     *  having tried all messages, and having exhausted all receive
     *  patterns, will get suspended until a new message arrives,
     *  and the message processing logic starts all over,
     *  beginning with the first message in the mailbox.
     *  </pre>
     *
     *  @example let awaited = await this.receive( branches )
     *
     *  @todo extend example to include entire call to spawn/n 
     *  @todo typecheck 'branches'? Should be iterable. Currently expects
     *  @todo type: [ [ 'function', 'function'] ]
     *  @todo Perhaps allow type: [ 'function', 'function' ] ?
     *  @todo Perhaps allow type: 'function' where this is just the branch?
     *
     */
    receive ( branches ) {

        //console.log (`NEWS, ${this}.RECEIVE(): called`)

        let prom = new Promise ( (resolve, reject) => {

                // console.log (`NEWS, promiseExec(): called`)

                this.mailHandler = m => {

                    this.mailbox.push ( m )
                        //  New messages are not always evaluated first.

                    //  Oldest messages are always evaluated first.
                    let messageMatched  =   false
                    let messageIndex    =   0

                    check_entire_mailbox: for ( const msg of this.mailbox ) {
                        match_message_to_reaction: for ( const b of branches ) {

                            // Essential framework conventions
                            let match   = b[0]
                            let branch  = b[1]

                            if ( match ( msg ) ) {
                                //console.log (`    customised mailHandler() MATCHED a message`)

                                messageMatched = true
                                this.mailbox.splice ( messageIndex, 1 )

                                this.mailHandler = this.defaultMailHandler
                                    // This must be done before resolve() so that
                                    // control is passed back to the proc's fn
                                    // body only after the proc.mailHandler has been
                                    // modified to perform safekeeping.

                                let returnedByBranch = branch ( msg )
                                
                                //console.log(
                                //`    returnedByBranch: [[${returnedByBranch}]],
                                //typeof ${typeof returnedByBranch}`)

                                resolve (returnedByBranch)
                                    // Promise Resolved
                                
                                break check_entire_mailbox
                            }
                            //console.log (`    customised mailHandler() tried to match a
                            //    message; failed`)
                        }
                        messageIndex ++ 
                    }
                }
                
                if ( this.mailbox.length ) {
                    this.mailHandler ( this.mailbox.pop() )
                }
                // this.mailHandler has now been customised; if there are any
                // messages in the mailbox, pop the last one, m, then call
                // this.mailHandler on it... ( which pushes m in at the top of
                // the stack, then starts checking through messages from the
                // bottom of the stack for matches in the mailHandler logic.)
                // TODO: review - really not sure if this is a performance leak.
        } )

        //console.log (`NEWS, RECEIVE(): will now return... `)
        return prom
    }
} 
    

/**
 *  @class module:Serl.Pid
 *  @classdesc
 *  Constructor for objects analogous to an [OTP Pid term]{@link
 *  http://erlang.org/doc/reference_manual/data_types.html#pid}.
 *
 *  @todo Do we need to validate argument types in the constructor?
 *
 */
export class Pid {

    constructor (nodeIndex, procIndex) {
        this.nodeIndex  = nodeIndex
        this.procIndex  = procIndex
    }

    /**
     *  @method module:Serl.Pid#toString
     *  @description Overridden [toString]{@link
     *  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString}
     *  . Output is of the form:
     *
     *  ```
     *  [object Pid<0.1>]
     *  ```
     *  @todo should this be made into a static method?
     *  
     */
    toString () {
        return `[object Pid<${this.nodeIndex}.${this.procIndex}>]`
    }
} 

/**
 *  @function module:Serl.recurse
 *  @description Utility function that helps reduce boilerplate in spawn/n for
 *  recursing functions. 
 *  @todo Make 'recurse' work with all arities of spawn/n
 */
export function recurse ( fun, funArgs ){
    // 'function' in expression needed, for 'this' in body

    let utilRecursive =   async ( _fun, _funArgs ) => { 
        // 'async' in expression needed, for 'await' in body

        let utilRecursiveAwaited    =   await _fun.apply ( this, _funArgs ) 
            // value of 'this' is inherited from surrounding scope;
            //
            // So, if 'recurse' is spawn/n-ed on a new Proc, then the context is
            // proc.recurse, and so 'this' would point to 'proc'. (see the
            // source for Node.spawn/n

        // TODO: utilRecursiveAwaited's return value is not used, but 
        // it could be used to replace the default _funArgs

        utilRecursive ( _fun, _funArgs ) 
    } 
    utilRecursive ( fun, funArgs ) 
}