Some checks failed
Build and Deploy / package-dist (push) Blocked by required conditions
Build and Deploy / deploy-itch (push) Blocked by required conditions
Build and Deploy / deploy-gitea (push) Blocked by required conditions
Build and Deploy / build-linux (push) Successful in 1m16s
Build and Deploy / build-windows (CLANG64) (push) Failing after 10m57s
903 lines
23 KiB
JavaScript
903 lines
23 KiB
JavaScript
(function engine() {
|
|
prosperon.DOC = Symbol('+documentation+') // Symbol for documentation references
|
|
|
|
prosperon.id = 'newguy';
|
|
|
|
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 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)
|
|
})
|
|
|
|
var use_cache = {}
|
|
|
|
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')
|
|
var tracy = use_embed('tracy')
|
|
|
|
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({})
|
|
|
|
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()
|
|
|
|
return `[${prosperon.id.substring(0,6)}] [${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
|
|
|
|
if (typeof msg === "object") msg = JSON.stringify(msg, null, 2)
|
|
|
|
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)
|
|
}
|
|
|
|
console.spam = function(msg) {
|
|
pprint(msg, 0)
|
|
}
|
|
console.debug = function(msg) {
|
|
pprint(msg, 1)
|
|
}
|
|
console.info = function(msg) {
|
|
pprint(msg, 2)
|
|
}
|
|
console.warn = function(msg) {
|
|
pprint(msg, 3)
|
|
}
|
|
console.log = function(msg) {
|
|
pprint(msg, 2)
|
|
}
|
|
console.error = function(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(e) {
|
|
pprint(e, 5)
|
|
os.quit()
|
|
}
|
|
console.assert = function(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
|
|
}
|
|
|
|
globalThis.use = function use(file) {
|
|
if (use_cache[file]) return use_cache[file]
|
|
var mod = script_fn(file)
|
|
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 {}
|
|
var parts = content.split(/\n\s*---\s*\n/)
|
|
if (parts.length === 1) {
|
|
var part = parts[0]
|
|
if (part.match(/return\s+[^;]+;?\s*$/))
|
|
return { module: part }
|
|
return { program: part }
|
|
}
|
|
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, callback) {
|
|
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
|
|
})
|
|
|
|
if (callback) callback(underling, { message:"created" })
|
|
|
|
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)()
|
|
|
|
var enet = use('enet')
|
|
var util = use('util')
|
|
var math = use('math')
|
|
var crypto = use('crypto')
|
|
|
|
var HEADER = Symbol()
|
|
var ACTORDATA = Symbol()
|
|
var PEER = Symbol()
|
|
|
|
var ar = 1 // seconds before reclamation
|
|
|
|
function print_actor()
|
|
{
|
|
return json.encode(this[ACTORDATA], 1);
|
|
}
|
|
|
|
function json_actor() {
|
|
var ret = this[ACTORDATA]
|
|
|
|
// The idea here is that, if we're serializing an actor, it's being sent over the wire
|
|
if (!ret.portal)
|
|
ret.portal = local_portal
|
|
|
|
return ret
|
|
}
|
|
|
|
function create_actor(data) {
|
|
return {
|
|
[ACTORDATA]: data,
|
|
toString: print_actor,
|
|
toJSON: json_actor
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
When an actor object like $_ is passed to another machine or function, it appears like this
|
|
[ACTORDATA]: {
|
|
id: local to the machine. Currently, the local port it is listening on
|
|
address: the IP address of the portal that connects it, if set
|
|
port: the public port of the portal that connects it, if set
|
|
}
|
|
*/
|
|
|
|
var $_ = {
|
|
toString: print_actor,
|
|
toJSON: json_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) {
|
|
if (actor[ACTORDATA]) return true
|
|
return false
|
|
}
|
|
|
|
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 (os.mailbox_exist(actor[ACTORDATA].id)) {
|
|
callback({type:"local"})
|
|
return
|
|
}
|
|
throw new Error(`Could not get connection information for ${actor}`)
|
|
}
|
|
$_.connection[prosperon.DOC] = "takes a callback function, an actor object, and a configuration record..."
|
|
|
|
var peers = {}
|
|
var peer_queue = new WeakMap()
|
|
var portal = undefined
|
|
var portal_fn
|
|
var local_portal
|
|
|
|
// The portal function is the only way an actor object can leave the machine; so on its way out, it needs tagged with the portal data
|
|
$_.portal = function(fn, port) {
|
|
if (portal) throw new Error(`Already started a portal listening on ${portal.port}`)
|
|
console.log(`starting a portal on port ${port}`)
|
|
if (!port) throw new Error("Requires a valid port.")
|
|
portal = enet.create_host({address: "any", port})
|
|
local_portal = `localhost:${port}`
|
|
portal_fn = fn
|
|
}
|
|
$_.portal[prosperon.DOC] = "starts a public address that performs introduction services..."
|
|
|
|
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(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":
|
|
handle_message(e.data)
|
|
break
|
|
}
|
|
}
|
|
|
|
var contactor = undefined
|
|
$_.contact = function(callback, record) {
|
|
var sendto = { [ACTORDATA]: {address: record.address, port: record.port} }
|
|
$_.send(create_actor({
|
|
address: record.address,
|
|
port: record.port
|
|
}), record, callback)
|
|
}
|
|
$_.contact[prosperon.DOC] = "The contact function sends a message to a portal..."
|
|
|
|
$_.receiver = function(fn) { receive_fn = fn }
|
|
$_.receiver[prosperon.DOC] = "registers a function that will receive all messages..."
|
|
|
|
$_.start = function(cb, prg, arg) {
|
|
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)
|
|
os.createthread(argv)
|
|
}
|
|
$_.start[prosperon.DOC] = "The start function creates a new actor..."
|
|
|
|
$_.stop = function(actor) {
|
|
if (!actor) destroyself()
|
|
if (!underlings.has(actor[ACTORDATA].id))
|
|
throw new Error('Can only call stop on an underling or self.')
|
|
|
|
actor_send(actor, {type:"stop", id: prosperon.id})
|
|
}
|
|
$_.stop[prosperon.DOC] = "The stop function stops an underling."
|
|
|
|
var unneeded_fn = $_.stop
|
|
var unneeded_time = ar
|
|
|
|
$_.unneeded = function(fn, seconds = ar) {
|
|
if (typeof fn !== 'function') throw new Error('Must supply a function to unneeded.')
|
|
unneeded_fn = fn
|
|
unneeded_time = seconds
|
|
}
|
|
$_.unneeded[prosperon.DOC] = "registers a function that is called when the actor..."
|
|
|
|
var timers = []
|
|
var timer_id = 0
|
|
|
|
$_.delay = function(fn, seconds) {
|
|
timers[timer_id++] = {seconds, fn}
|
|
return function() { delete timers[timer_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_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 (receive_fn && actor[ACTORDATA].id === prosperon.id) {
|
|
receive_fn(message.data)
|
|
return
|
|
}
|
|
|
|
if (actor[ACTORDATA].id && os.mailbox_exist(actor[ACTORDATA].id)) {
|
|
os.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(message)
|
|
return
|
|
}
|
|
|
|
throw new Error(`Unable to send message to actor ${json.encode(actor)}`)
|
|
}
|
|
|
|
var replies = {}
|
|
var reply_cc = new WeakMap()
|
|
var contact_replies = new WeakMap()
|
|
|
|
$_.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]) { // This means it's a response message
|
|
var header = actor[HEADER]
|
|
console.log(`replying to a message: ${json.encode(header)}`)
|
|
// Only set id if ACTORDATA exists, otherwise create it
|
|
if (!actor[ACTORDATA]) actor[ACTORDATA] = {}
|
|
actor[ACTORDATA].id = header.replycc
|
|
send.return = header.reply
|
|
}
|
|
|
|
if (reply) {
|
|
var id = util.guid()
|
|
replies[id] = reply
|
|
send.reply = id
|
|
send.replycc = prosperon.id
|
|
}
|
|
|
|
actor_send(actor, send)
|
|
}
|
|
$_.send[prosperon.DOC] = "sends a message to another actor..."
|
|
|
|
var cmd = use('cmd')
|
|
cmd.process(prosperon.argv)
|
|
|
|
if (!prosperon.args.id) prosperon.id = util.guid()
|
|
else prosperon.id = prosperon.args.id
|
|
|
|
$_[ACTORDATA] = {id: prosperon.id}
|
|
$_.toJSON = json_actor
|
|
$_.toSring = print_actor
|
|
|
|
if (prosperon.args.overling) overling = { [ACTORDATA]: {id: prosperon.args.overling} }
|
|
if (prosperon.args.root) root = { [ACTORDATA]: {id: prosperon.args.root} }
|
|
else root = { [ACTORDATA]: {id: prosperon.id} }
|
|
|
|
os.mailbox_start(prosperon.id)
|
|
|
|
if (overling) actor_send(overling, {type:'greet', id: prosperon.id})
|
|
if (prosperon.args.program) actor.spawn(prosperon.args.program)
|
|
|
|
var unneeded_timer = $_.delay($_.stop, ar)
|
|
|
|
function destroyself() {
|
|
if (overling) actor_send(overling, { type: "stopped", id: prosperon.id })
|
|
os.exit(0)
|
|
}
|
|
|
|
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) {
|
|
os.mailbox_push(msg.target, msg)
|
|
return
|
|
}
|
|
}
|
|
unneeded_timer()
|
|
switch (msg.type) {
|
|
case "user":
|
|
console.log(`handling message ${json.encode(msg)}`)
|
|
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
|
|
}
|
|
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 letter = msg.data
|
|
letter[HEADER] = msg
|
|
delete msg.data
|
|
portal_fn(letter)
|
|
}
|
|
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]
|
|
console.log(`got a greet from message ${json.encode(msg)}`)
|
|
if (greeter) greeter({type: "actor_started", actor: create_actor(msg)})
|
|
}
|
|
unneeded_timer = $_.delay(unneeded_fn, unneeded_time)
|
|
}
|
|
|
|
var hang = 0.01
|
|
var last_t = os.now()
|
|
|
|
while (1) {
|
|
if (portal) portal.service(handle_host, hang)
|
|
if (contactor) contactor.service(handle_host, hang)
|
|
os.mailbox_service(prosperon.id, handle_message)
|
|
var elapsed = os.now() - last_t
|
|
last_t = os.now()
|
|
for (var i in timers) {
|
|
var t = timers[i]
|
|
t.seconds -= elapsed
|
|
if (t.seconds <= 0) {
|
|
t.fn()
|
|
delete timers[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
})()
|