/** @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 )
}