1010 lines
27 KiB
JavaScript
1010 lines
27 KiB
JavaScript
(function engine() {
|
|
prosperon.DOC = Symbol('+documentation+') // Symbol for documentation references
|
|
|
|
var listeners = new Map()
|
|
|
|
prosperon.on = function(type, callback) {
|
|
if (!listeners.has(type)) listeners.set(type, [])
|
|
listeners.get(type).push(callback)
|
|
|
|
return function() {
|
|
var arr = listeners.get(type)
|
|
if (!arr) return
|
|
var idx = arr.indexOf(callback)
|
|
if (idx >= 0) arr.splice(idx,1)
|
|
}
|
|
}
|
|
|
|
prosperon.dispatch = function(type, data) {
|
|
var arr = listeners.get(type)
|
|
if (!arr) return
|
|
for (var callback of arr) callback(data)
|
|
}
|
|
|
|
var os = use_embed('os')
|
|
var actor_mod = use_embed('actor')
|
|
var tracy = use_embed('tracy')
|
|
|
|
os.trace = true;
|
|
|
|
if (os.trace)
|
|
tracy.level(1);
|
|
|
|
var js = use_embed('js')
|
|
|
|
prosperon.on('SIGINT', function() {
|
|
os.exit(1)
|
|
})
|
|
|
|
prosperon.on('SIGABRT', function() {
|
|
console.error(new Error('SIGABRT'))
|
|
os.exit(1)
|
|
})
|
|
|
|
prosperon.on('SIGSEGV', function() {
|
|
console.error(new Error('SIGSEGV'))
|
|
os.exit(1)
|
|
})
|
|
|
|
Object.defineProperty(Function.prototype, "hashify", {
|
|
value: function () {
|
|
var hash = new Map()
|
|
var fn = this
|
|
function hashified(...args) {
|
|
var key = args[0]
|
|
if (!hash.has(key)) hash.set(key, fn(...args))
|
|
return hash.get(key)
|
|
}
|
|
return hashified
|
|
},
|
|
})
|
|
|
|
var io = use_embed('io')
|
|
|
|
globalThis.console = use_embed('console')
|
|
|
|
var RESPATH = 'scripts/modules/resources.js'
|
|
var canonical = io.realdir(RESPATH) + 'resources.js'
|
|
var content = io.slurp(RESPATH)
|
|
var resources = js.eval(RESPATH, `(function setup_resources(){${content}})`).call({})
|
|
|
|
var use_cache = {}
|
|
|
|
use_cache[resources.canonical('resources.js')] = resources
|
|
|
|
function print_api(obj) {
|
|
for (var prop in obj) {
|
|
if (!obj.hasOwnProperty(prop)) continue
|
|
var val = obj[prop]
|
|
console.log(prop)
|
|
if (typeof val === 'function') {
|
|
var m = val.toString().match(/\(([^)]*)\)/)
|
|
if (m) console.log(' function: ' + prop + '(' + m[1].trim() + ')')
|
|
}
|
|
}
|
|
}
|
|
|
|
prosperon.PATH = [
|
|
"/",
|
|
"scripts/modules/",
|
|
"scripts/modules/ext/",
|
|
]
|
|
|
|
// path is the path of a module or script to resolve
|
|
var script_fn = function script_fn(path) {
|
|
var parsed = {}
|
|
var file = resources.find_script(path)
|
|
|
|
if (!file) {
|
|
parsed.module_ret = bare_load(path)
|
|
if (!parsed.module_ret) throw new Error(`Module ${path} could not be created`)
|
|
return parsed
|
|
}
|
|
|
|
var content = io.slurp(file)
|
|
var parsed = parse_file(content, file)
|
|
var module_name = file.name()
|
|
parsed.module_ret = bare_load(path)
|
|
parsed.module_ret ??= {}
|
|
|
|
if (parsed.module) {
|
|
var mod_script = `(function setup_${module_name}_module(){ var self = this; var $ = this; var exports = {}; var module = {exports: exports}; var define = undefined; ${parsed.module}})`
|
|
var module_fn = js.eval(file, mod_script)
|
|
parsed.module_ret = module_fn.call(parsed.module_ret)
|
|
if (parsed.module_ret === undefined || parsed.module_ret === null)
|
|
throw new Error(`Module ${module_name} must return a value`)
|
|
parsed.module_fn = module_fn
|
|
}
|
|
|
|
parsed.program ??= ""
|
|
var prog_script = `(function use_${module_name}() { var self = this; var $ = this.__proto__; ${parsed.program}})`
|
|
parsed.prog_fn = js.eval(file, prog_script)
|
|
|
|
return parsed
|
|
}.hashify()
|
|
|
|
function bare_load(file) {
|
|
try {
|
|
return use_embed(file)
|
|
} catch (e) { }
|
|
try {
|
|
return use_dyn(file + so_ext)
|
|
} catch(e) { }
|
|
return undefined
|
|
}
|
|
|
|
|
|
|
|
var res_cache = {}
|
|
|
|
function console_rec(category, priority, line, file, msg) {
|
|
var now = time.now()
|
|
|
|
var id = prosperon.name ? prosperon.name : prosperon.id
|
|
id = id.substring(0,6)
|
|
|
|
return `[${id}] [${time.text(now, "mb d yyyy h:nn:ss")}] ${file}:${line}: [${category} ${priority}]: ${msg}\n`
|
|
}
|
|
|
|
io.mkdir('.prosperon')
|
|
var logfile //= io.open('.prosperon/log.txt')
|
|
|
|
function pprint(msg, lvl = 0) {
|
|
if (!logfile) return
|
|
|
|
var file = "nofile"
|
|
var line = 0
|
|
|
|
var caller = new Error().stack.split("\n")[2]
|
|
if (caller) {
|
|
var md = caller.match(/\((.*)\:/)
|
|
var m = md ? md[1] : "SCRIPT"
|
|
if (m) file = m
|
|
md = caller.match(/\:(\d*)\)/)
|
|
m = md ? md[1] : 0
|
|
if (m) line = m
|
|
}
|
|
var fmt = console_rec("script", lvl, line, file, msg)
|
|
console.print(fmt)
|
|
|
|
if (logfile)
|
|
logfile.write(fmt)
|
|
|
|
if (tracy) tracy.message(fmt)
|
|
}
|
|
|
|
function format_args(...args) {
|
|
return args.map(arg => {
|
|
if (typeof arg === 'object' && arg !== null) {
|
|
try {
|
|
return json.encode(arg)
|
|
} catch (e) {
|
|
return String(arg)
|
|
}
|
|
}
|
|
return String(arg)
|
|
}).join(' ')
|
|
}
|
|
|
|
console.spam = function spam(...args) {
|
|
pprint(format_args(...args), 0)
|
|
}
|
|
console.debug = function debug(...args) {
|
|
pprint(format_args(...args), 1)
|
|
}
|
|
console.info = function info(...args) {
|
|
pprint(format_args(...args), 2)
|
|
}
|
|
console.warn = function warn(...args) {
|
|
pprint(format_args(...args), 3)
|
|
}
|
|
console.log = function log(...args) {
|
|
pprint(format_args(...args), 2)
|
|
}
|
|
console.error = function error(e) {
|
|
if (!e)
|
|
e = new Error()
|
|
|
|
if (e instanceof Error)
|
|
pprint(`${e.name} : ${e.message}
|
|
${e.stack}`, 4)
|
|
else
|
|
pprint(e,4)
|
|
}
|
|
console.panic = function panic(e) {
|
|
pprint(e, 5)
|
|
os.quit()
|
|
}
|
|
console.assert = function assert(op, str = `assertion failed [value '${op}']`) {
|
|
if (!op) console.panic(str)
|
|
}
|
|
|
|
//os.on('uncaught_exception', function(e) { console.error(e); })
|
|
|
|
console[prosperon.DOC] = {
|
|
doc: "The console object provides various logging, debugging, and output methods.",
|
|
spam: "Output a spam-level message for very verbose logging.",
|
|
debug: "Output a debug-level message.",
|
|
info: "Output info level message.",
|
|
warn: "Output warn level message.",
|
|
error: "Output error level message, and print stacktrace.",
|
|
panic: "Output a panic-level message and exit the program.",
|
|
assert: "If the condition is false, print an error and panic.",
|
|
critical: "Output critical level message, and exit game immediately.",
|
|
write: "Write raw text to console.",
|
|
say: "Write raw text to console, plus a newline.",
|
|
log: "Output directly to in game console.",
|
|
level: "Set level to output logging to console.",
|
|
stack: "Output a stacktrace to console.",
|
|
clear: "Clear console."
|
|
}
|
|
|
|
var BASEPATH = 'scripts/core/base.js'
|
|
var script = io.slurp(BASEPATH)
|
|
var fnname = "base"
|
|
script = `(function ${fnname}() { ${script}; })`
|
|
js.eval(BASEPATH, script)()
|
|
|
|
function add_timer(obj, fn, seconds) {
|
|
var timers = obj[TIMERS]
|
|
|
|
var stop = function () {
|
|
if (!timer) return
|
|
timers.delete(stop)
|
|
timer.fn = undefined
|
|
timer = undefined
|
|
}
|
|
|
|
function execute() {
|
|
if (fn) timer.remain = fn(stop.seconds)
|
|
if (!timer) return
|
|
if (!timer.remain) stop()
|
|
else stop.seconds = timer.remain
|
|
}
|
|
|
|
// var timer = os.make_timer(execute)
|
|
timer.remain = seconds
|
|
|
|
stop.remain = seconds
|
|
stop.seconds = seconds
|
|
|
|
timers.push(stop)
|
|
return stop
|
|
}
|
|
|
|
var DEAD = Symbol()
|
|
var GARBAGE = Symbol()
|
|
var FILE = Symbol()
|
|
var TIMERS = Symbol()
|
|
var REGGIES = Symbol()
|
|
var UNDERLINGS = Symbol()
|
|
var OVERLING = Symbol()
|
|
|
|
var actor = {}
|
|
globalThis.actor = actor
|
|
|
|
var so_ext
|
|
switch(os.platform()) {
|
|
case 'Windows':
|
|
so_ext = '.dll'
|
|
break
|
|
default:
|
|
so_ext = '.so'
|
|
break
|
|
}
|
|
|
|
var use_cache = {}
|
|
var inProgress = {}
|
|
var loadingStack = []
|
|
|
|
globalThis.use = function use(file) {
|
|
// If we've already begun loading this file in this chain, show the cycle
|
|
if (loadingStack.includes(file)) {
|
|
// Find where in the stack this file first appeared
|
|
let cycleIndex = loadingStack.indexOf(file)
|
|
// Extract just the modules in the cycle
|
|
let cyclePath = loadingStack.slice(cycleIndex).concat(file)
|
|
|
|
throw new Error(
|
|
`Circular dependency detected while loading "${file}".\n` +
|
|
`Module chain: ${loadingStack.join(" -> ")}\n` +
|
|
`Cycle specifically: ${cyclePath.join(" -> ")}`
|
|
)
|
|
}
|
|
|
|
// Already fully loaded? Return it
|
|
if (use_cache[file]) {
|
|
return use_cache[file]
|
|
}
|
|
|
|
// If it's loading (but not on the stack), mark it as a new chain entry
|
|
// (This is optional if you just rely on loadingStack.
|
|
// But if you'd like a simple “already loading” check, keep 'inProgress'.)
|
|
if (inProgress[file]) {
|
|
throw new Error(`Circular dependency detected while loading "${file}"`)
|
|
}
|
|
inProgress[file] = true
|
|
|
|
// Push onto loading stack for chain tracking
|
|
loadingStack.push(file)
|
|
|
|
// Actually load the module
|
|
var mod = script_fn(file)
|
|
|
|
// Done loading, remove from the chain and mark as loaded
|
|
loadingStack.pop()
|
|
delete inProgress[file]
|
|
|
|
// Cache and return
|
|
use_cache[file] = mod.module_ret
|
|
return use_cache[file]
|
|
}
|
|
|
|
globalThis.json = use('json')
|
|
var time = use('time')
|
|
|
|
function parse_file(content, file) {
|
|
if (!content) return {}
|
|
if (!/^\s*---\s*$/m.test(content)) {
|
|
var part = content.trim()
|
|
if (part.match(/return\s+[^;]+;?\s*$/)) {
|
|
return { module: part }
|
|
}
|
|
return { program: part }
|
|
}
|
|
var parts = content.split(/\n\s*---\s*\n/)
|
|
var module = parts[0]
|
|
if (!/\breturn\b/.test(module))
|
|
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.`)
|
|
|
|
try {
|
|
new Function(module)()
|
|
} catch (e) {
|
|
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.\n` + e.message)
|
|
}
|
|
|
|
var pad = '\n'.repeat(module.split('\n').length + 4)
|
|
return {
|
|
module,
|
|
program: pad + parts[1]
|
|
}
|
|
}
|
|
|
|
globalThis.Register = {
|
|
registries: [],
|
|
|
|
add_cb(name) {
|
|
var n = {}
|
|
var fns = []
|
|
|
|
n.register = function (fn, oname) {
|
|
if (typeof fn !== 'function') return
|
|
|
|
var dofn = function (...args) {
|
|
fn(...args)
|
|
}
|
|
Object.defineProperty(dofn, 'name', {value:`do_${oname}`})
|
|
|
|
var left = 0
|
|
var right = fns.length - 1
|
|
dofn.layer = fn.layer
|
|
dofn.layer ??= 0
|
|
|
|
while (left <= right) {
|
|
var mid = Math.floor((left + right) / 2)
|
|
if (fns[mid] === dofn.layer) {
|
|
left = mid
|
|
break
|
|
} else if (fns[mid].layer < dofn.layer) left = mid + 1
|
|
else right = mid - 1
|
|
}
|
|
|
|
fns.splice(left, 0, dofn)
|
|
return function () {
|
|
fns.delete(dofn)
|
|
}
|
|
}
|
|
|
|
prosperon[name] = function (...args) {
|
|
fns.forEach(fn => {
|
|
fn(...args)
|
|
})
|
|
}
|
|
Object.defineProperty(prosperon[name], 'name', {value:name})
|
|
prosperon[name].fns = fns
|
|
|
|
n.clear = function () {
|
|
fns = []
|
|
}
|
|
|
|
Register[name] = n
|
|
Register.registries[name] = n
|
|
return n
|
|
},
|
|
}
|
|
|
|
Register.pull_registers = function pull_registers(obj) {
|
|
var reggies = []
|
|
for (var reg in Register.registries) {
|
|
if (typeof obj[reg] === "function")
|
|
reggies.push(reg)
|
|
}
|
|
return reggies
|
|
}
|
|
|
|
Register.register_obj = function register_obj(obj, reg) {
|
|
var fn = obj[reg].bind(obj)
|
|
fn.layer = obj[reg].layer
|
|
var name = obj.ur ? obj.ur.name : obj.toString()
|
|
obj[TIMERS].push(Register.registries[reg].register(fn, name))
|
|
if (!obj[reg].name) Object.defineProperty(obj[reg], 'name', {value:`${obj._file}_${reg}`})
|
|
}
|
|
|
|
Register.check_registers = function check_registers(obj) {
|
|
if (obj[REGGIES]) {
|
|
if (obj[REGGIES].length == 0) return
|
|
for (var reg of obj[REGGIES])
|
|
Register.register_obj(obj,reg)
|
|
return
|
|
}
|
|
for (var reg in Register.registries) {
|
|
if (typeof obj[reg] === "function")
|
|
Register.register_obj(obj,reg)
|
|
}
|
|
}
|
|
|
|
Register.add_cb("appupdate")
|
|
Register.add_cb("update").doc = "Called once per frame."
|
|
Register.add_cb("physupdate")
|
|
Register.add_cb("gui")
|
|
Register.add_cb("hud")
|
|
Register.add_cb("draw")
|
|
Register.add_cb("imgui")
|
|
Register.add_cb("app")
|
|
|
|
function cant_kill() {
|
|
throw Error("Can't kill an object in its spawning code. Move the kill command to awake.")
|
|
}
|
|
|
|
actor.toString = function() { return this[FILE] }
|
|
|
|
actor.spawn = function spawn(script, config) {
|
|
if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor")
|
|
var prog
|
|
if (!script) {
|
|
prog = {}
|
|
prog.module_ret = {}
|
|
prog.prog_fn = function() {}
|
|
} else {
|
|
prog = script_fn(script)
|
|
if (!prog.prog_fn) throw new Error(`Script ${script} is not an actor script or has no actor component`)
|
|
}
|
|
|
|
var underling
|
|
prog.module_ret.__proto__ = actor
|
|
underling = Object.create(prog.module_ret)
|
|
underling[OVERLING] = this
|
|
underling[FILE] = script
|
|
underling[TIMERS] = []
|
|
underling[UNDERLINGS] = new Set()
|
|
|
|
Object.defineProperty(underling, 'overling', {
|
|
get() { return this[OVERLING] },
|
|
enumerable:true,
|
|
configurable:false
|
|
})
|
|
|
|
Object.defineProperty(underling, 'underlings', {
|
|
get() { return new Set(this[UNDERLINGS]) },
|
|
enumerable:true,
|
|
configurable:false
|
|
})
|
|
|
|
Object.defineProperty(underling, 'spawn', {
|
|
value: actor.spawn,
|
|
writable:false,
|
|
enumerable:true,
|
|
configurable:false
|
|
})
|
|
|
|
Object.defineProperty(underling, 'kill', {
|
|
value: actor.kill,
|
|
writable:false,
|
|
enumerable:true,
|
|
configurable:false
|
|
})
|
|
|
|
Object.defineProperty(underling, 'delay', {
|
|
value: actor.delay,
|
|
writable:false,
|
|
enumerable:true,
|
|
configurable:false
|
|
})
|
|
|
|
try {
|
|
prog.prog_fn.call(underling)
|
|
} catch(e) { throw e; }
|
|
|
|
if (underling[DEAD]) return undefined;
|
|
|
|
if (typeof config === 'object') Object.assign(underling, config)
|
|
|
|
if (!underling[REGGIES])
|
|
underling.__proto__[REGGIES] = Register.pull_registers(underling)
|
|
|
|
Register.check_registers(underling)
|
|
if (underling.awake) underling.awake()
|
|
|
|
this[UNDERLINGS].add(underling)
|
|
if (underling.tag) act.tag_add(underling.tag, underling)
|
|
|
|
underling[GARBAGE] = underling.garbage
|
|
return underling
|
|
}
|
|
|
|
|
|
|
|
actor.clear = function actor_clear() {
|
|
this[UNDERLINGS].forEach(p => {
|
|
p.kill()
|
|
})
|
|
this[UNDERLINGS].clear()
|
|
}
|
|
|
|
var input = use('input')
|
|
|
|
actor.kill = function kill() {
|
|
if (this[DEAD]) return
|
|
this[DEAD] = true
|
|
this[TIMERS].slice().forEach(t => t())
|
|
delete this[TIMERS]
|
|
input.do_uncontrol(this)
|
|
|
|
this.clear()
|
|
this[OVERLING][UNDERLINGS].delete(this)
|
|
delete this[UNDERLINGS]
|
|
|
|
if (typeof this.garbage === "function") this.garbage()
|
|
if (typeof this.then === "function") this.then()
|
|
|
|
act.tag_clear_guid(this)
|
|
}
|
|
actor.kill.doc = `Remove this actor and all its underlings from existence.`
|
|
|
|
actor.delay = function(fn, seconds) {
|
|
if (this[DEAD]) return
|
|
add_timer(this, fn, seconds)
|
|
}
|
|
actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.`
|
|
|
|
|
|
|
|
var act = use('actor')
|
|
actor[UNDERLINGS] = new Set()
|
|
|
|
globalThis.mixin("color")
|
|
|
|
var DOCPATH = 'scripts/core/doc.js'
|
|
var script = io.slurp(DOCPATH)
|
|
var fnname = "doc"
|
|
script = `(function ${fnname}() { ${script}; })`
|
|
//js.eval(DOCPATH, script)()
|
|
|
|
/*
|
|
When handling a message, the message appears like this:
|
|
{
|
|
type: type of message
|
|
- contact: used for contact messages
|
|
- stop: used to issue stop command
|
|
- etc
|
|
reply: ID this message will respond to (callback saved on the actor)
|
|
replycc: the actor that is waiting for the reply
|
|
target: ID of the actor that's supposed to receive the message. Only added to non direct sends (out of portals)
|
|
return: reply ID so the replycc actor can know what callback to send the message to
|
|
|
|
data: the actual content of the message
|
|
}
|
|
|
|
actors look like
|
|
{
|
|
id: the GUID of this actor
|
|
address: the IP this actor can be found at
|
|
port: the port of the IP the actor can be found at
|
|
}
|
|
*/
|
|
|
|
var enet = use('enet')
|
|
var util = use('util')
|
|
var math = use('math')
|
|
var crypto = use('crypto')
|
|
var nota = use('nota')
|
|
|
|
var dying = false
|
|
|
|
var HEADER = Symbol()
|
|
|
|
function create_actor(__ACTORDATA__ = {id:util.guid()}) {
|
|
return { __ACTORDATA__ }
|
|
}
|
|
|
|
var $_ = create_actor()
|
|
|
|
$_.random = crypto.random
|
|
$_.random[prosperon.DOC] = "returns a number between 0 and 1. There is a 50% chance that the result is less than 0.5."
|
|
|
|
$_.clock = function(fn) { return os.now() }
|
|
$_.clock[prosperon.DOC] = "takes a function input value that will eventually be called with the current time in number form."
|
|
|
|
var underlings = new Set()
|
|
var overling = undefined
|
|
var root = undefined
|
|
|
|
globalThis.$_ = $_
|
|
|
|
var receive_fn = undefined
|
|
var greeters = {}
|
|
|
|
$_.is_actor = function(actor) {
|
|
return actor.__ACTORDATA__
|
|
}
|
|
|
|
function peer_connection(peer) {
|
|
return {
|
|
latency: peer.rtt,
|
|
bandwidth: {
|
|
incoming: peer.incoming_bandwidth,
|
|
outgoing: peer.outgoing_bandwidth
|
|
},
|
|
activity: {
|
|
last_sent: peer.last_send_time,
|
|
last_received: peer.last_receive_time
|
|
},
|
|
mtu: peer.mtu,
|
|
data: {
|
|
incoming_total: peer.incoming_data_total,
|
|
outgoing_total: peer.outgoing_data_total,
|
|
reliable_in_transit: peer.reliable_data_in_transit
|
|
},
|
|
latency_variance: peer.rtt_variance,
|
|
packet_loss: peer.packet_loss,
|
|
state: peer.state
|
|
}
|
|
}
|
|
|
|
$_.connection = function(callback, actor, config) {
|
|
var peer = peers[actor.__ACTORDATA__.id]
|
|
if (peer) {
|
|
callback(peer_connection(peer))
|
|
return
|
|
}
|
|
if (actor_mod.mailbox_exist(actor.__ACTORDATA__.id)) {
|
|
callback({type:"local"})
|
|
return
|
|
}
|
|
throw new Error(`Could not get connection information for ${actor}`)
|
|
}
|
|
$_.connection[prosperon.DOC] = "The connection function takes a callback function, an actor object, and a configuration record for getting information about the status of a connection to the actor. The configuration record is used to request the sort of information that needs to be communicated. This can include latency, bandwidth, activity, congestion, cost, partitions. The callback is given a record containing the requested information."
|
|
|
|
var peers = {}
|
|
var id_address = {}
|
|
var peer_queue = new WeakMap()
|
|
var portal = undefined
|
|
var portal_fn = undefined
|
|
|
|
var service_delay = 0.01
|
|
|
|
$_.portal = function(fn, port) {
|
|
if (portal) throw new Error(`Already started a portal listening on ${portal.port}`)
|
|
if (!port) throw new Error("Requires a valid port.")
|
|
console.log(`starting a portal on port ${port}`)
|
|
portal = enet.create_host({address: "any", port})
|
|
portal_fn = fn
|
|
}
|
|
$_.portal[prosperon.DOC] = "A portal is a special actor with a public address that performs introduction services. It listens on a specified port for contacts by external actors that need to acquire an actor object. The function will receive the record containing the request. The record can have a reply sent through it. A portal can respond by beginning a new actor, or finding an existing actor, or by forwarding the contact message to another actor. This is how distributed Misty networks are bootstrapped. The portal function returns null."
|
|
|
|
function handle_host(e) {
|
|
switch (e.type) {
|
|
case "connect":
|
|
console.log(`connected a new peer: ${e.peer.address}:${e.peer.port}`)
|
|
peers[`${e.peer.address}:${e.peer.port}`] = e.peer
|
|
var queue = peer_queue.get(e.peer)
|
|
if (queue) {
|
|
for (var msg of queue) e.peer.send(nota.encode(msg))
|
|
console.log(`sent ${json.encode(msg)} out of queue`)
|
|
peer_queue.delete(e.peer)
|
|
}
|
|
break
|
|
case "disconnect":
|
|
peer_queue.delete(e.peer)
|
|
for (var id in peers) if (peers[id] === e.peer) delete peers[id]
|
|
console.log('portal got disconnect')
|
|
break
|
|
case "receive":
|
|
var data = nota.decode(e.data)
|
|
console.log(`got message ${json.encode(data)} over the wire`)
|
|
if (data.replycc && !data.replycc.address) {
|
|
data.replycc.__ACTORDATA__.address = e.peer.address
|
|
data.replycc.__ACTORDATA__.port = e.peer.port
|
|
}
|
|
// Also populate address/port for any actor objects in the message data
|
|
function populate_actor_addresses(obj) {
|
|
if (typeof obj !== 'object' || obj === null) return
|
|
if (obj.__ACTORDATA__ && !obj.__ACTORDATA__.address) {
|
|
obj.__ACTORDATA__.address = e.peer.address
|
|
obj.__ACTORDATA__.port = e.peer.port
|
|
}
|
|
for (var key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
populate_actor_addresses(obj[key])
|
|
}
|
|
}
|
|
}
|
|
if (data.data) populate_actor_addresses(data.data)
|
|
console.log(`turned it into ${json.encode(data)} over the wire`)
|
|
handle_message(data)
|
|
break
|
|
}
|
|
}
|
|
|
|
var contactor = undefined
|
|
$_.contact = function(callback, record) {
|
|
$_.send(create_actor(record), record, callback)
|
|
}
|
|
|
|
$_.contact[prosperon.DOC] = `The contact function sends a message to a portal on another machine to obtain an actor object.
|
|
The callback is a function with a actor input and a reason input. If successful, actor is bound to an actor object. If not successful, actor is null and reason may contain an explanation.`
|
|
|
|
$_.receiver = function(fn) {
|
|
receive_fn = fn
|
|
}
|
|
$_.receiver[prosperon.DOC] = "registers a function that will receive all messages..."
|
|
|
|
$_.start = function(cb, prg, arg) {
|
|
if (dying) {
|
|
console.warn(`Cannot start an underling in the same turn as we're stopping`)
|
|
return
|
|
}
|
|
var id = util.guid()
|
|
greeters[id] = cb
|
|
var argv = ["./prosperon", "spawn", "--id", id, "--overling", prosperon.id, "--root", root]
|
|
if (prg) argv = argv.concat(['--program', prg])
|
|
if (arg) argv = argv.concat(cmd.encode(arg))
|
|
underlings.add(id)
|
|
actor_mod.createactor(argv)
|
|
}
|
|
$_.start[prosperon.DOC] = "The start function creates a new actor..."
|
|
|
|
$_.stop = function(actor) {
|
|
if (!actor) {
|
|
destroyself()
|
|
return
|
|
}
|
|
if (!$_.is_actor(actor))
|
|
throw new Error('Can only call stop on an actor.')
|
|
if (!underlings.has(actor.__ACTORDATA__.id))
|
|
throw new Error('Can only call stop on an underling or self.')
|
|
|
|
actor_prep(actor, {type:"stop", id: prosperon.id})
|
|
}
|
|
$_.stop[prosperon.DOC] = "The stop function stops an underling."
|
|
|
|
$_.unneeded = function(fn, seconds) {
|
|
actor_mod.unneeded(fn, seconds)
|
|
}
|
|
$_.unneeded[prosperon.DOC] = "registers a function that is called when the actor..."
|
|
|
|
$_.delay = function(fn, seconds) {
|
|
var id = actor_mod.delay(fn, seconds)
|
|
return function() { actor_mod.removetimer(id) }
|
|
}
|
|
$_.delay[prosperon.DOC] = "used to schedule the invocation of a function..."
|
|
|
|
var couplings = new Set()
|
|
$_.couple = function(actor) {
|
|
console.log(`coupled to ${actor.__ACTORDATA__.id}`)
|
|
couplings.add(actor.__ACTORDATA__.id)
|
|
}
|
|
$_.couple[prosperon.DOC] = "causes this actor to stop when another actor stops."
|
|
|
|
function actor_prep(actor, send) {
|
|
message_queue.push({actor,send});
|
|
}
|
|
|
|
function actor_send(actor, message) {
|
|
if (!$_.is_actor(actor)) throw new Error(`Must send to an actor object. Attempted send to ${json.encode(actor)}`)
|
|
if (typeof message !== 'object') throw new Error('Must send an object record.')
|
|
|
|
if (actor.__ACTORDATA__.id === prosperon.id) {
|
|
if (receive_fn) receive_fn(message.data)
|
|
return
|
|
}
|
|
if (actor.__ACTORDATA__.id && actor_mod.mailbox_exist(actor.__ACTORDATA__.id)) {
|
|
actor_mod.mailbox_push(actor.__ACTORDATA__.id, message)
|
|
return
|
|
}
|
|
if (actor.__ACTORDATA__.address) {
|
|
if (actor.__ACTORDATA__.id) message.target = actor.__ACTORDATA__.id
|
|
else message.type = "contact"
|
|
var peer = peers[actor.__ACTORDATA__.address + ":" + actor.__ACTORDATA__.port]
|
|
if (!peer) {
|
|
if (!contactor && !portal) {
|
|
console.log(`creating a contactor ...`)
|
|
contactor = enet.create_host()
|
|
}
|
|
peer = (contactor || portal).connect(actor.__ACTORDATA__.address, actor.__ACTORDATA__.port)
|
|
peer_queue.set(peer, [message])
|
|
} else {
|
|
peer.send(nota.encode(message))
|
|
}
|
|
return
|
|
}
|
|
throw new Error(`Unable to send message to actor ${json.encode(actor)}`)
|
|
}
|
|
|
|
// Holds all messages queued during the current turn.
|
|
var message_queue = []
|
|
|
|
function send_messages() {
|
|
// Attempt to flush the queued messages. If one fails, keep going anyway.
|
|
var errors = []
|
|
while (message_queue.length > 0) {
|
|
var item = message_queue.shift()
|
|
var actor = item.actor
|
|
var send = item.send
|
|
try {
|
|
actor_send(actor, send)
|
|
} catch (err) {
|
|
errors.push(err)
|
|
}
|
|
}
|
|
if (errors.length > 0) {
|
|
console.error("Some messages failed to send:", errors)
|
|
for (var i of errors) console.error(i)
|
|
}
|
|
}
|
|
|
|
var replies = {}
|
|
|
|
$_.send = function(actor, message, reply) {
|
|
if (typeof message !== 'object')
|
|
throw new Error('Message must be an object')
|
|
var send = {type:"user", data: message}
|
|
|
|
if (actor[HEADER] && actor[HEADER].replycc) {
|
|
var header = actor[HEADER]
|
|
if (!header.replycc || !$_.is_actor(header.replycc))
|
|
throw new Error(`Supplied actor had a return, but it's not a valid actor! ${json.encode(actor[HEADER])}`)
|
|
|
|
actor = header.replycc
|
|
send.return = header.reply
|
|
}
|
|
|
|
if (reply) {
|
|
var id = util.guid()
|
|
replies[id] = reply
|
|
send.reply = id
|
|
send.replycc = $_
|
|
}
|
|
|
|
// Instead of sending immediately, queue it
|
|
actor_prep(actor,send);
|
|
}
|
|
$_.send[prosperon.DOC] = "sends a message to another actor..."
|
|
|
|
$_.blast = $_.send;
|
|
|
|
var cmd = use('cmd')
|
|
cmd.process(prosperon.argv.slice())
|
|
|
|
if (!prosperon.args.id) prosperon.id = util.guid()
|
|
else prosperon.id = prosperon.args.id
|
|
|
|
$_.__ACTORDATA__.id = prosperon.id
|
|
|
|
actor_mod.register_actor(prosperon.id, function(msg) {
|
|
try {
|
|
handle_message(msg)
|
|
send_messages()
|
|
} catch (err) {
|
|
message_queue = []
|
|
throw err
|
|
}
|
|
}, prosperon.args.main)
|
|
|
|
if (prosperon.args.overling) overling = create_actor({id:prosperon.args.overling})
|
|
|
|
if (prosperon.args.root) root = json.decode(prosperon.args.root)
|
|
else root = $_
|
|
|
|
if (overling) actor_prep(overling, {type:'greet', id: prosperon.id})
|
|
|
|
if (!prosperon.args.program)
|
|
os.exit(1)
|
|
|
|
if (typeof prosperon.args.program !== 'string')
|
|
prosperon.args.program = 'main.js';
|
|
|
|
actor.spawn(prosperon.args.program)
|
|
|
|
function destroyself() {
|
|
console.log(`Got the message to destroy self.`)
|
|
dying = true
|
|
for (var i of underlings)
|
|
$_.stop(create_actor({id:i}))
|
|
|
|
actor_mod.destroy()
|
|
}
|
|
|
|
function handle_actor_disconnect(id) {
|
|
var greeter = greeters[id]
|
|
if (greeter) {
|
|
greeter({type: "stopped", id})
|
|
delete greeters[id]
|
|
}
|
|
console.log(`actor ${id} disconnected`)
|
|
if (couplings.has(id)) $_.stop()
|
|
delete peers[id]
|
|
}
|
|
|
|
function handle_message(msg) {
|
|
if (msg.target) {
|
|
if (msg.target !== prosperon.id) {
|
|
actor_mod.mailbox_push(msg.target, msg)
|
|
return
|
|
}
|
|
}
|
|
switch (msg.type) {
|
|
case "user":
|
|
var letter = msg.data
|
|
delete msg.data
|
|
letter[HEADER] = msg
|
|
if (msg.return) {
|
|
console.log(`Received a message for the return id ${msg.return}`)
|
|
var fn = replies[msg.return]
|
|
if (!fn) throw new Error(`Could not find return function for message ${msg.return}`)
|
|
fn(letter)
|
|
delete replies[msg.return]
|
|
return
|
|
}
|
|
if (receive_fn) receive_fn(letter)
|
|
break
|
|
case "stop":
|
|
if (msg.id !== overling.__ACTORDATA__.id)
|
|
throw new Error(`Got a message from an actor ${msg.id} to stop...`)
|
|
destroyself()
|
|
break
|
|
case "contact":
|
|
if (portal_fn) {
|
|
var letter2 = msg.data
|
|
letter2[HEADER] = msg
|
|
delete msg.data
|
|
portal_fn(letter2)
|
|
} else throw new Error('Got a contact message, but no portal is established.')
|
|
break
|
|
case "stopped":
|
|
handle_actor_disconnect(msg.id)
|
|
break
|
|
case "greet":
|
|
var greeter = greeters[msg.id]
|
|
if (greeter) greeter({type: "actor_started", actor: create_actor(msg)})
|
|
break;
|
|
default:
|
|
if (receive_fn) receive_fn(msg)
|
|
break;
|
|
}
|
|
};
|
|
|
|
function enet_check()
|
|
{
|
|
if (portal) portal.service(handle_host)
|
|
if (contactor) contactor.service(handle_host)
|
|
|
|
send_messages();
|
|
|
|
$_.delay(enet_check, service_delay);
|
|
}
|
|
|
|
send_messages();
|
|
enet_check();
|
|
|
|
})() |