Files
cell/scripts/core/engine.js
John Alanbrook 216ada5568
Some checks failed
Build and Deploy / build-macos (push) Failing after 4s
Build and Deploy / build-linux (push) Failing after 1m33s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
attempt fix local network
2025-05-21 15:40:22 -05:00

1234 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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 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')
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()
var ACTORDATA = Symbol()
function create_actor(id = util.guid()) {
return {id}
}
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 "id" in actor || ACTORDATA in actor
}
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.id]
if (peer) {
callback(peer_connection(peer))
return
}
if (os.mailbox_exist(actor.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..."
// 1) id → peer (live ENet connection)
const peer_by_id = Object.create(null)
// 2) id → {address,port} (last seen endpoint)
const id_address = Object.create(null)
// 3) address:port → peer (fast lookup for incoming events)
const peers = Object.create(null)
var peer_queue = new WeakMap()
var portal, portal_fn
var local_address, local_port
var service_delay = 0.01 // how often to ping enet
function route_set(id, peer){ peer_by_id[id] = peer }
function route_hint(id, address, port){ id_address[id] = {address,port} }
function route_peerString(peer){ return `${peer.address}:${peer.port}` }
$_.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})
local_address = 'localhost'
local_port = port
portal_fn = fn
console.log(`Portal initialized with actor ID: ${$_.id}`)
}
$_.portal[prosperon.DOC] = "starts a public address that performs introduction services..."
function handle_host(e) {
switch (e.type) {
case "connect":
// Store peer information for future routing
var key = route_peerString(e.peer)
peers[key] = e.peer
console.log(`Connected to peer: ${e.peer.address}:${e.peer.port}`)
// Check if we have queued messages for this peer
var queue = peer_queue.get(e.peer)
if (queue && queue.length > 0) {
console.log(`Sending ${queue.length} queued messages to newly connected peer`)
for (var msg of queue) {
try {
e.peer.send(nota.encode(msg))
console.log(`Sent queued message: ${json.encode(msg)}`)
} catch (err) {
console.error(`Failed to send queued message: ${err.message}`)
}
}
// Clear the queue after sending
peer_queue.delete(e.peer)
}
break
case "disconnect":
// Clean up peer references
var key = route_peerString(e.peer)
console.log(`Peer disconnected: ${key}`)
// Remove from peers map
delete peers[key]
// Remove from queue
peer_queue.delete(e.peer)
// Remove from any ID-based routing
for (var id in peer_by_id) {
if (peer_by_id[id] === e.peer) {
console.log(`Removing route for actor ${id}`)
delete peer_by_id[id]
}
}
break
case "receive":
// Decode the message
try {
var msg = nota.decode(e.data)
console.log(`Received message from ${e.peer.address}:${e.peer.port}:`, json.encode(msg))
// Update routing information based on message contents
function touch(obj){
if (!obj || typeof obj !== 'object') return
if (typeof obj.id === 'string'){
// Set up routing for this actor ID
route_set(obj.id, e.peer)
// Add address hint if we don't have one
if (!id_address[obj.id]) {
route_hint(obj.id, e.peer.address, e.peer.port)
console.log(`Added route hint for ${obj.id}: ${e.peer.address}:${e.peer.port}`)
}
}
// Recursively touch all properties
for (const k in obj) {
if (obj.hasOwnProperty(k)) touch(obj[k])
}
}
// Extract routing information
touch(msg)
// Process the message
handle_message(msg)
} catch (err) {
console.error(`Failed to decode or process received message: ${err.message}`)
}
break
}
}
var contactor = undefined
$_.contact = function(callback, record) {
console.log("Contact function called with:", json.encode(record));
// Create a pseudo-actor for the initial contact
var sendto = {}
sendto[ACTORDATA] = record
// Ensure we send a properly formatted contact message
var contactMsg = {
type: "contact",
data: record
};
// Wrap the original callback to handle the actor reference properly
function wrappedCallback(response) {
console.log("Contact wrapped callback received:", json.encode(response));
// First param should be the actor, second param is reason/error
if (response && typeof response === 'object') {
if ('id' in response) {
// This is an actor reference, pass it directly
console.log("Identified actor reference in response");
callback(response, null);
} else if (response.data && typeof response.data === 'object' && 'id' in response.data) {
// The actor reference is in the data field
console.log("Identified actor reference in response.data");
callback(response.data, null);
} else {
// No actor reference found, must be an error
console.log("No actor reference found in response");
callback(null, response);
}
} else {
// Error case or unexpected response format
console.log("Unexpected response format");
callback(null, response);
}
}
// Send the contact message with callback for response
$_.send(sendto, contactMsg, wrappedCallback);
}
$_.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) {
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)
os.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.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) {
os.unneeded(fn, seconds)
}
$_.unneeded[prosperon.DOC] = "registers a function that is called when the actor..."
$_.delay = function(fn, seconds) {
var id = os.delay(fn, seconds)
return function() { os.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.id}`)
couplings.add(actor.id)
}
$_.couple[prosperon.DOC] = "causes this actor to stop when another actor stops."
function actor_prep(actor, send) {
message_queue.push({actor,send});
}
function ensure_route(actor){
// Extract the actor ID
const id = actor.id
// First check if we already have a peer for this actor
if (peer_by_id[id]) {
console.log(`Found existing peer for actor ${id}`)
return peer_by_id[id]
}
// Check if we have a hint for this actor's address
const hint = id_address[id]
if (hint) {
console.log(`Found address hint for ${id}: ${hint.address}:${hint.port}`)
// Create host if needed
if (!contactor && !portal) {
console.log("Creating new contactor host")
contactor = enet.create_host()
}
// Connect to the peer
const host = contactor || portal
console.log(`Connecting to ${hint.address}:${hint.port} via ${contactor ? 'contactor' : 'portal'}`)
const p = host.connect(hint.address, hint.port)
// Initialize queue for this peer
peer_queue.set(p, [])
return p
}
// Check if we have connection data for this actor
var cnn = actor[ACTORDATA]
if (cnn) {
console.log(`Using ACTORDATA for connection: ${cnn.address}:${cnn.port}`)
// Create host if needed
if (!contactor && !portal) {
console.log("Creating new contactor host")
contactor = enet.create_host()
}
// Connect to the peer
const host = contactor || portal
console.log(`Connecting to ${cnn.address}:${cnn.port} via ${contactor ? 'contactor' : 'portal'}`)
var p = host.connect(cnn.address, cnn.port)
// Initialize queue for this peer
peer_queue.set(p, [])
return p
}
console.log(`No route found for actor ${id}`)
return null
}
function actor_send(actor, send){
if (!$_.is_actor(actor)) throw Error('bad actor: ' + json.encode(actor))
if (actor.id===prosperon.id) { // message to self
console.log("actor_send: message to self")
if(receive_fn)
receive_fn(send.data);
return
}
if (os.mailbox_exist(actor.id)){ // message to local mailbox
os.mailbox_push(actor.id, send);
return
}
const peer = ensure_route(actor)
if (peer){
console.log("actor_send: sending via peer route", peer.address + ":" + peer.port)
peer_queue.get(peer)?.push(send) || peer.send(nota.encode(send))
return
}
// fallback forward via parent header if present
if (actor[HEADER] && actor[HEADER].replycc){
console.log("actor_send: forwarding via parent header")
const fwd = {type:'forward', forward_to:actor.id, payload:send}
actor_send(actor[HEADER].replycc, fwd); return
}
console.error("actor_send: no route to actor", actor.id)
throw Error(`no route to actor ${actor.id}`)
}
// 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 = []
// Process all queued messages
while (message_queue.length > 0) {
var {actor,send} = message_queue.shift()
// console.log("Processing queued message:", json.encode(send))
try {
actor_send(actor, send)
// console.log("Message sent successfully")
} catch (err) {
console.error("Failed to send message:", err.message)
errors.push(err)
}
}
// Report any send errors
if (errors.length) {
console.error(`${errors.length} messages failed to send`)
for (var i = 0; i < Math.min(errors.length, 3); i++) {
console.error(`Error ${i+1}: ${errors[i].message}`)
}
}
}
var replies = {}
$_.send = function(actor, message, reply) {
if (typeof message !== 'object')
throw new Error('Message must be an object')
// If message already has type, respect it, otherwise wrap it as user message
var send = message.type ? message : {type:"user", data: message}
// Make sure contact messages have 'data' property
if (message.type === 'contact' && !send.data) {
console.log("Fixing contact message structure")
send.data = message.data || {}
}
if (actor[HEADER] && actor[HEADER].replycc) { // in this case, it's not a true actor, but a message we're responding to
console.log("Send: responding to message with header", json.encode(actor[HEADER]))
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
// When replying to a contact message, ensure proper data structure
if (header.type === "contact" && send.type === "user") {
// Make sure we're sending the actor ID in a proper format for contact responses
send.data = send.data || $_;
}
}
if (reply) {
var id = util.guid()
replies[id] = reply
send.reply = id
send.replycc = $_
console.log("Send: added reply callback with id", id)
}
// Instead of sending immediately, queue it
// console.log("Send: queuing message", json.encode(send))
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
os.register_actor(prosperon.id, function(msg) {
try {
handle_message(msg)
send_messages()
} catch (err) {
message_queue = []
throw err
}
}, prosperon.args.main)
$_.id = prosperon.id
if (prosperon.args.overling) overling = create_actor(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';
console.log(`running ${prosperon.args.program}`)
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));
os.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) {
// console.log("Handling message:", json.encode(msg));
if (msg.target) {
if (msg.target !== prosperon.id) {
console.log(`Forwarding message to ${msg.target}`);
os.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) {
console.error(`Could not find return function for message ${msg.return}`);
return;
}
// Handle contact response specially - first parameter is the actor reference
if (letter && letter[HEADER] && letter[HEADER].type === "user" && letter[HEADER].return) {
// For contact responses, we need to extract the actor reference
// letter.data should contain the actor object from the portal
if (letter.data && typeof letter.data === 'object' && letter.data.id) {
console.log("Processing contact response with actor data:", json.encode(letter.data));
fn(letter.data); // This is the actor reference
} else {
console.log("Processing contact response with letter as actor:", json.encode(letter));
fn(letter); // Fallback to the whole message
}
} else {
fn(letter);
}
delete replies[msg.return];
return;
}
if (receive_fn) receive_fn(letter);
break;
case "stop":
if (overling && msg.id !== overling.id)
throw new Error(`Got a message from an actor ${msg.id} to stop...`);
destroyself();
break;
case "contact":
if (portal_fn) {
console.log("Portal received contact message");
var letter2 = msg.data;
letter2[HEADER] = msg;
delete msg.data;
console.log("Portal handling contact message:", json.encode(letter2));
portal_fn(letter2);
} else {
console.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) {
console.log("Greeting actor with id:", msg.id);
greeter({type: "actor_started", actor: create_actor(msg)});
}
break;
case "ping":
// Keep-alive ping, no action needed
break;
default:
// console.log("Default message handler for type:", msg.type);
if (receive_fn) receive_fn(msg);
break;
}
};
function enet_check()
{
if (portal) portal.service(handle_host)
if (contactor) contactor.service(handle_host)
send_messages();
// Send keep-alive ping to all active peers to prevent timeout
for (var peerKey in peers) {
var peer = peers[peerKey]
if (peer && peer.state === 1) { // ENET_PEER_STATE_CONNECTED
try {
peer.send(nota.encode({type: 'ping'}))
} catch (e) {
console.debug(`Failed to send ping to ${peerKey}:`, e)
}
}
}
$_.delay(enet_check, service_delay);
}
send_messages();
enet_check();
})()