1074 lines
27 KiB
Plaintext
1074 lines
27 KiB
Plaintext
(function engine() {
|
|
var _cell = globalThis.cell
|
|
delete globalThis.cell
|
|
var ACTORDATA = _cell.hidden.actorsym
|
|
var SYSYM = '__SYSTEM__'
|
|
|
|
var hidden = _cell.hidden
|
|
|
|
var os = hidden.os;
|
|
_cell.os = null
|
|
|
|
var dylib_ext
|
|
|
|
_cell.id ??= "newguy"
|
|
|
|
switch(os.platform()) {
|
|
case 'Windows': dylib_ext = '.dll'; break;
|
|
case 'macOS': dylib_ext = '.dylib'; break;
|
|
case 'Linux': dylib_ext = '.so'; break;
|
|
}
|
|
|
|
var MOD_EXT = '.cm'
|
|
var ACTOR_EXT = '.ce'
|
|
|
|
var load_internal = os.load_internal
|
|
function use_embed(name) {
|
|
return load_internal(`js_${name}_use`)
|
|
}
|
|
|
|
globalThis.meme = function(obj, ...mixins) {
|
|
var result = {
|
|
__proto__: obj
|
|
}
|
|
|
|
array.for(mixins, mix => {
|
|
if (isa(mix, object)) {
|
|
for (var key in mix) {
|
|
result[key] = mix[key]
|
|
}
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
globalThis.logical = function(val1)
|
|
{
|
|
if (val1 == 0 || val1 == false || val1 == "false" || val1 == null)
|
|
return false;
|
|
if (val1 == 1 || val1 == true || val1 == "true")
|
|
return true;
|
|
return null;
|
|
}
|
|
|
|
var utf8 = use_embed('utf8')
|
|
var js = use_embed('js')
|
|
var fd = use_embed('fd')
|
|
|
|
// Get the shop path from HOME environment
|
|
var home = os.getenv('HOME') || os.getenv('USERPROFILE')
|
|
if (!home) {
|
|
throw new Error('Could not determine home directory')
|
|
}
|
|
var shop_path = home + '/.cell'
|
|
var packages_path = shop_path + '/packages'
|
|
var core_path = packages_path + '/core'
|
|
|
|
if (!fd.is_dir(core_path)) {
|
|
throw new Error('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.')
|
|
}
|
|
|
|
var use_cache = {}
|
|
use_cache['core/os'] = os
|
|
|
|
var _Symbol = Symbol
|
|
|
|
globalThis.key = function()
|
|
{
|
|
return _Symbol()
|
|
}
|
|
|
|
// Load a core module from the file system
|
|
function use_core(path) {
|
|
var cache_key = 'core/' + path
|
|
if (use_cache[cache_key])
|
|
return use_cache[cache_key];
|
|
|
|
var sym = use_embed(path.replace('/','_'))
|
|
|
|
// Core scripts are in packages/core/
|
|
var file_path = core_path + '/' + path + MOD_EXT
|
|
|
|
if (fd.is_file(file_path)) {
|
|
var script_blob = fd.slurp(file_path)
|
|
var script = utf8.decode(script_blob)
|
|
var mod = `(function setup_module(use){${script}})`
|
|
var fn = js.eval('core:' + path, mod)
|
|
var result = fn.call(sym, use_core);
|
|
use_cache[cache_key] = result;
|
|
return result;
|
|
}
|
|
|
|
use_cache[cache_key] = sym;
|
|
return sym;
|
|
}
|
|
|
|
var blob = use_core('blob')
|
|
var blob_stone = blob.prototype.stone
|
|
var blob_stonep = blob.prototype.stonep;
|
|
delete blob.prototype.stone;
|
|
delete blob.prototype.stonep;
|
|
|
|
// Capture Object and Array methods before they're deleted
|
|
var _Object = Object
|
|
var _ObjectKeys = Object.keys
|
|
var _ObjectFreeze = Object.freeze
|
|
var _ObjectIsFrozen = Object.isFrozen
|
|
var _ObjectDefineProperty = Object.defineProperty
|
|
var _ObjectGetPrototypeOf = Object.getPrototypeOf
|
|
var _ObjectCreate = meme
|
|
var _ArrayIsArray = Array.isArray
|
|
|
|
Object.prototype.toString = function()
|
|
{
|
|
return json.encode(this)
|
|
}
|
|
|
|
function deepFreeze(object) {
|
|
if (object instanceof blob)
|
|
blob_stone.call(object);
|
|
|
|
var propNames = _ObjectKeys(object);
|
|
|
|
for (var name of propNames) {
|
|
var value = object[name];
|
|
|
|
if ((value && typeof value == "object") || typeof value == "function")
|
|
deepFreeze(value);
|
|
}
|
|
|
|
return _ObjectFreeze(object);
|
|
}
|
|
|
|
globalThis.actor = function()
|
|
{
|
|
|
|
}
|
|
|
|
globalThis.stone = deepFreeze
|
|
stone.p = function(object)
|
|
{
|
|
if (object instanceof blob)
|
|
return blob_stonep.call(object)
|
|
|
|
return _ObjectIsFrozen(object)
|
|
}
|
|
|
|
var actor_mod = use_core('actor')
|
|
var wota = use_core('wota')
|
|
var nota = use_core('nota')
|
|
|
|
// Load internal modules for global functions
|
|
globalThis.text = use_core('internal/text')
|
|
globalThis.number = use_core('internal/number')
|
|
globalThis.array = use_core('internal/array')
|
|
globalThis.object = use_core('internal/object')
|
|
globalThis.fn = use_core('internal/fn')
|
|
|
|
// Global utility functions (use already-captured references)
|
|
var _isArray = _ArrayIsArray
|
|
var _keys = _ObjectKeys
|
|
var _getPrototypeOf = _ObjectGetPrototypeOf
|
|
var _create = _ObjectCreate
|
|
|
|
globalThis.length = function(value) {
|
|
if (value == null) return null
|
|
|
|
// For functions, return arity
|
|
if (typeof value == 'function') return value.length
|
|
|
|
// For strings, return codepoint count
|
|
if (typeof value == 'string') return value.length
|
|
|
|
// For arrays, return element count
|
|
if (_isArray(value)) return value.length
|
|
|
|
// For blobs, return bit count
|
|
if (value instanceof blob && typeof value.length == 'number') return value.length
|
|
|
|
// For records with length field
|
|
if (typeof value == 'object' && value != null) {
|
|
if ('length' in value) {
|
|
var len = value.length
|
|
if (typeof len == 'function') return len.call(value)
|
|
if (typeof len == 'number') return len
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
globalThis.reverse = function(value) {
|
|
if (_isArray(value)) {
|
|
var result = []
|
|
for (var i = value.length - 1; i >= 0; i--) {
|
|
result.push(value[i])
|
|
}
|
|
return result
|
|
}
|
|
|
|
// For blobs, would need blob module support
|
|
if (isa(value, blob)) {
|
|
// Simplified: return null for now, would need proper blob reversal
|
|
return null
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
globalThis.isa = function(value, master) {
|
|
if (master == null) return false
|
|
|
|
// isa(value, array) - check if object has all keys
|
|
if (_isArray(master)) {
|
|
if (typeof value != 'object' || value == null) return false
|
|
for (var i = 0; i < master.length; i++) {
|
|
if (!(master[i] in value)) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isa(value, function) - check if function.prototype is in chain
|
|
if (typeof master == 'function') {
|
|
// Special type checks
|
|
if (master == stone) return _ObjectIsFrozen(value) || typeof value != 'object'
|
|
if (master == number) return typeof value == 'number'
|
|
if (master == text) return typeof value == 'string'
|
|
if (master == logical) return typeof value == 'boolean'
|
|
if (master == array) return _isArray(value)
|
|
if (master == object) return typeof value == 'object' && value != null && !_isArray(value)
|
|
if (master == fn) return typeof value == 'function'
|
|
if (master == actor) return isa(value, object) && value[ACTORDATA]
|
|
|
|
// Check prototype chain
|
|
if (master.prototype) {
|
|
var proto = _getPrototypeOf(value)
|
|
while (proto != null) {
|
|
if (proto == master.prototype) return true
|
|
proto = _getPrototypeOf(proto)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isa(object, master_object) - check prototype chain
|
|
if (typeof master == 'object') {
|
|
var proto = _getPrototypeOf(value)
|
|
while (proto != null) {
|
|
if (proto == master) return true
|
|
proto = _getPrototypeOf(proto)
|
|
}
|
|
return false
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
globalThis.proto = function(obj) {
|
|
if (!isa(obj, object)) return null
|
|
var p = _getPrototypeOf(obj)
|
|
if (p == _Object.prototype) return null
|
|
return p
|
|
}
|
|
|
|
globalThis.splat = function(obj) {
|
|
if (typeof obj != 'object' || obj == null) return null
|
|
|
|
var result = {}
|
|
var current = obj
|
|
|
|
// Walk prototype chain and collect text keys
|
|
while (current != null) {
|
|
var keys = _keys(current)
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var k = keys[i]
|
|
if (!(k in result)) {
|
|
var val = current[k]
|
|
// Only include serializable types
|
|
if (typeof val == 'object' || typeof val == 'number' ||
|
|
typeof val == 'string' || typeof val == 'boolean') {
|
|
result[k] = val
|
|
}
|
|
}
|
|
}
|
|
current = _getPrototypeOf(current)
|
|
}
|
|
|
|
// Call to_data if present
|
|
if (typeof obj.to_data == 'function') {
|
|
var extra = obj.to_data(result)
|
|
if (typeof extra == 'object' && extra != null) {
|
|
var extraKeys = _keys(extra)
|
|
for (var i = 0; i < extraKeys.length; i++) {
|
|
result[extraKeys[i]] = extra[extraKeys[i]]
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
var ENETSERVICE = 0.1
|
|
var REPLYTIMEOUT = 60 // seconds before replies are ignored
|
|
|
|
globalThis.pi = 3.14159265358979323846264338327950288419716939937510
|
|
|
|
function caller_data(depth = 0)
|
|
{
|
|
var file = "nofile"
|
|
var line = 0
|
|
|
|
var caller = new Error().stack.split("\n")[1+depth]
|
|
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
|
|
}
|
|
|
|
return {file,line}
|
|
}
|
|
|
|
function console_rec(line, file, msg) {
|
|
return `[${_cell.id.slice(0,5)}] [${file}:${line}]: ${msg}\n`
|
|
// time: [${time.text("mb d yyyy h:nn:ss")}]
|
|
}
|
|
|
|
globalThis.log = {}
|
|
log.console = function(msg)
|
|
{
|
|
var caller = caller_data(1)
|
|
os.print(console_rec(caller.line, caller.file, msg))
|
|
}
|
|
|
|
log.error = function(msg = new Error())
|
|
{
|
|
var caller = caller_data(1)
|
|
|
|
if (msg instanceof Error)
|
|
msg = msg.name + ": " + msg.message + "\n" + msg.stack
|
|
|
|
os.print(console_rec(caller.line,caller.file,msg))
|
|
}
|
|
|
|
log.system = function(msg) {
|
|
msg = "[SYSTEM] " + msg
|
|
log.console(msg)
|
|
}
|
|
|
|
function disrupt(err)
|
|
{
|
|
if (overling) {
|
|
if (err) {
|
|
// with an err, this is a forceful disrupt
|
|
var reason = (err instanceof Error) ? err.stack : err
|
|
report_to_overling({type:'disrupt', reason})
|
|
} else
|
|
report_to_overling({type:'stop'})
|
|
}
|
|
|
|
if (underlings) {
|
|
for (var id of underlings) {
|
|
log.console(`calling on ${id} to disrupt too`)
|
|
$_.stop(create_actor({id}))
|
|
}
|
|
}
|
|
|
|
if (err) {
|
|
log.console(err);
|
|
if (err.stack)
|
|
log.console(err.stack)
|
|
}
|
|
|
|
actor_mod.disrupt()
|
|
}
|
|
|
|
actor_mod.on_exception(disrupt)
|
|
|
|
_cell.args = _cell.hidden.init
|
|
_cell.args ??= {}
|
|
_cell.id ??= "newguy"
|
|
|
|
function create_actor(desc = {id:guid()}) {
|
|
var actor = {}
|
|
actor[ACTORDATA] = desc
|
|
return actor
|
|
}
|
|
|
|
var $_ = {}
|
|
$_.self = create_actor()
|
|
|
|
os.use_cache = use_cache
|
|
os.global_shop_path = shop_path
|
|
os.$_ = $_
|
|
|
|
var shop = use_core('internal/shop')
|
|
|
|
var json = use_core('json')
|
|
var time = use_core('time')
|
|
|
|
var pronto = use_core('pronto')
|
|
globalThis.fallback = pronto.fallback
|
|
globalThis.parallel = pronto.parallel
|
|
globalThis.race = pronto.race
|
|
globalThis.sequence = pronto.sequence
|
|
|
|
$_.time_limit = function(requestor, seconds)
|
|
{
|
|
if (!pronto.is_requestor(requestor))
|
|
throw new Error('time_limit: first argument must be a requestor');
|
|
if (!isa(seconds, number) || seconds <= 0)
|
|
throw new Error('time_limit: seconds must be a positive number');
|
|
|
|
return function time_limit_requestor(callback, value) {
|
|
pronto.check_callback(callback, 'time_limit')
|
|
var finished = false
|
|
var requestor_cancel = null
|
|
var timer_cancel = null
|
|
|
|
function cancel(reason) {
|
|
if (finished) return
|
|
finished = true
|
|
if (timer_cancel) {
|
|
timer_cancel()
|
|
timer_cancel = null
|
|
}
|
|
if (requestor_cancel) {
|
|
try { pronto.requestor_cancel(reason) } catch (_) {}
|
|
requestor_cancel = null
|
|
}
|
|
}
|
|
|
|
timer_cancel = $_.delay(function() {
|
|
if (finished) return
|
|
def reason = make_reason(factory, 'Timeout.', seconds)
|
|
if (requestor_cancel) {
|
|
try { requestor_cancel(reason) } catch (_) {}
|
|
requestor_cancel = null
|
|
}
|
|
finished = true
|
|
callback(null, reason)
|
|
}, seconds)
|
|
|
|
try {
|
|
requestor_cancel = requestor(function(val, reason) {
|
|
if (finished) return
|
|
finished = true
|
|
if (timer_cancel) {
|
|
timer_cancel()
|
|
timer_cancel = null
|
|
}
|
|
callback(val, reason)
|
|
}, value)
|
|
} catch (ex) {
|
|
cancel(ex)
|
|
callback(null, ex)
|
|
}
|
|
|
|
return function(reason) {
|
|
if (requestor_cancel) {
|
|
try { requestor_cancel(reason) } catch (_) {}
|
|
requestor_cancel = null
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var config = {
|
|
ar_timer: 60,
|
|
actor_memory:0,
|
|
net_service:0.1,
|
|
reply_timeout:60,
|
|
main: true
|
|
}
|
|
|
|
_cell.config = config
|
|
|
|
ENETSERVICE = config.net_service
|
|
REPLYTIMEOUT = config.reply_timeout
|
|
|
|
/*
|
|
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
|
|
}
|
|
*/
|
|
|
|
function guid(bits = 256)
|
|
{
|
|
var guid = new blob(bits, os.random)
|
|
stone(guid)
|
|
return text(guid,'h')
|
|
}
|
|
|
|
var _Symbol = Symbol
|
|
|
|
var HEADER = _Symbol()
|
|
|
|
// takes a function input value that will eventually be called with the current time in number form.
|
|
$_.clock = function(fn) {
|
|
actor_mod.clock(_ => {
|
|
fn(time.number())
|
|
send_messages()
|
|
})
|
|
}
|
|
|
|
var underlings = new Set() // this is more like "all actors that are notified when we die"
|
|
var overling = null
|
|
var root = null
|
|
|
|
var receive_fn = null
|
|
var greeters = {} // Router functions for when messages are received for a specific 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
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
$_.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
|
|
}
|
|
|
|
callback()
|
|
}
|
|
|
|
var peers = {}
|
|
var id_address = {}
|
|
var peer_queue = {}
|
|
var portal = null
|
|
var portal_fn = null
|
|
|
|
// takes a function input value that will eventually be called with the current time in number form.
|
|
$_.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.")
|
|
log.system(`starting a portal on port ${port}`)
|
|
portal = enet.create_host({address: "any", port})
|
|
portal_fn = fn
|
|
}
|
|
|
|
function handle_host(e) {
|
|
switch (e.type) {
|
|
case "connect":
|
|
log.system(`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))
|
|
log.system(`sent ${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]
|
|
log.system('portal got disconnect from ' + e.peer.address + ":" + e.peer.port)
|
|
break
|
|
case "receive":
|
|
var data = nota.decode(e.data)
|
|
if (data.replycc && !data.replycc.address) {
|
|
data.replycc[ACTORDATA].address = e.peer.address
|
|
data.replycc[ACTORDATA].port = e.peer.port
|
|
}
|
|
function populate_actor_addresses(obj) {
|
|
if (!isa(obj, object)) 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 (object.has(obj, key)) {
|
|
populate_actor_addresses(obj[key])
|
|
}
|
|
}
|
|
}
|
|
if (data.data) populate_actor_addresses(data.data)
|
|
turn(data)
|
|
break
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
$_.contact = function(callback, record) {
|
|
send(create_actor(record), record, callback)
|
|
}
|
|
|
|
// registers a function that will receive all messages...
|
|
$_.receiver = function receiver(fn) {
|
|
receive_fn = fn
|
|
}
|
|
|
|
$_.start = function start(cb, program, ...args) {
|
|
if (!program) return
|
|
|
|
var id = guid()
|
|
if (args.length == 1 && Array.isArray(args[0])) args = args[0]
|
|
var startup = {
|
|
id,
|
|
overling: $_.self,
|
|
root,
|
|
arg: args,
|
|
program,
|
|
}
|
|
greeters[id] = cb
|
|
message_queue.push({ startup })
|
|
}
|
|
|
|
// stops an underling or self.
|
|
$_.stop = function stop(actor) {
|
|
if (!actor) {
|
|
need_stop = true
|
|
return
|
|
}
|
|
if (!isa(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.')
|
|
|
|
sys_msg(actor, {kind:"stop"})
|
|
}
|
|
|
|
// schedules the removal of an actor after a specified amount of time.
|
|
$_.unneeded = function unneeded(fn, seconds) {
|
|
actor_mod.unneeded(fn, seconds)
|
|
}
|
|
|
|
// schedules the invocation of a function after a specified amount of time.
|
|
$_.delay = function delay(fn, seconds = 0) {
|
|
if (seconds <= 0) {
|
|
$_.clock(fn)
|
|
return
|
|
}
|
|
|
|
function delay_turn() {
|
|
fn()
|
|
send_messages()
|
|
}
|
|
var id = actor_mod.delay(delay_turn, seconds)
|
|
return function() { actor_mod.removetimer(id) }
|
|
}
|
|
|
|
var enet = use_core('enet')
|
|
|
|
// causes this actor to stop when another actor stops.
|
|
var couplings = new Set()
|
|
$_.couple = function couple(actor) {
|
|
if (actor == $_.self) return // can't couple to self
|
|
couplings.add(actor[ACTORDATA].id)
|
|
sys_msg(actor, {kind:'couple', from: $_.self})
|
|
log.system(`coupled to ${actor}`)
|
|
}
|
|
|
|
function actor_prep(actor, send) {
|
|
message_queue.push({actor,send});
|
|
}
|
|
|
|
// Send a message immediately without queuing
|
|
function actor_send_immediate(actor, send) {
|
|
try {
|
|
actor_send(actor, send);
|
|
} catch (err) {
|
|
log.error("Failed to send immediate message:", err);
|
|
}
|
|
}
|
|
|
|
function actor_send(actor, message) {
|
|
if (actor[HEADER] && !actor[HEADER].replycc) // attempting to respond to a message but sender is not expecting; silently drop
|
|
return
|
|
|
|
if (!isa(actor, actor) && !isa(actor.replycc, actor)) throw new Error(`Must send to an actor object. Attempted send to ${actor}`)
|
|
|
|
if (typeof message != 'object') throw new Error('Must send an object record.')
|
|
|
|
// message to self
|
|
if (actor[ACTORDATA].id == _cell.id) {
|
|
if (receive_fn) receive_fn(message.data)
|
|
return
|
|
}
|
|
|
|
// message to actor in same flock
|
|
if (actor[ACTORDATA].id && actor_mod.mailbox_exist(actor[ACTORDATA].id)) {
|
|
var wota_blob = wota.encode(message)
|
|
// log.console(`sending wota blob of ${wota_blob.length/8} bytes`)
|
|
actor_mod.mailbox_push(actor[ACTORDATA].id, wota_blob)
|
|
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 (!portal) {
|
|
log.system(`creating a contactor ...`)
|
|
portal = enet.create_host({address:"any"})
|
|
log.system(`allowing contact to port ${portal.port}`)
|
|
}
|
|
log.system(`no peer! connecting to ${actor[ACTORDATA].address}:${actor[ACTORDATA].port}`)
|
|
peer = portal.connect(actor[ACTORDATA].address, actor[ACTORDATA].port)
|
|
peer_queue.set(peer, [message])
|
|
} else {
|
|
peer.send(nota.encode(message))
|
|
}
|
|
return
|
|
}
|
|
log.system(`Unable to send message to actor ${actor[ACTORDATA]}`)
|
|
}
|
|
|
|
// Holds all messages queued during the current turn.
|
|
var message_queue = []
|
|
|
|
var need_stop = false
|
|
|
|
function send_messages() {
|
|
// if we've been flagged to stop, bail out before doing anything
|
|
if (need_stop) {
|
|
disrupt()
|
|
message_queue.length = 0
|
|
return
|
|
}
|
|
|
|
for (var msg of message_queue) {
|
|
if (msg.startup) {
|
|
// now is the time to actually spin up the actor
|
|
actor_mod.createactor(msg.startup)
|
|
} else {
|
|
actor_send(msg.actor, msg.send)
|
|
}
|
|
}
|
|
|
|
message_queue.length = 0
|
|
}
|
|
|
|
var replies = {}
|
|
|
|
globalThis.send = function send(actor, message, reply) {
|
|
if (typeof actor != 'object')
|
|
throw new Error(`Must send to an actor object. Provided: ${actor}`);
|
|
|
|
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 || !isa(header.replycc, actor))
|
|
throw new Error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
|
|
|
|
actor = header.replycc
|
|
send.return = header.reply
|
|
}
|
|
|
|
if (reply) {
|
|
var id = guid()
|
|
replies[id] = reply
|
|
$_.delay(_ => {
|
|
if (replies[id]) {
|
|
replies[id](null, "timeout")
|
|
delete replies[id]
|
|
}
|
|
}, REPLYTIMEOUT)
|
|
send.reply = id
|
|
send.replycc = $_.self
|
|
}
|
|
|
|
// Instead of sending immediately, queue it
|
|
actor_prep(actor,send);
|
|
}
|
|
|
|
stone(send)
|
|
|
|
if (!_cell.args.id) _cell.id = guid()
|
|
else _cell.id = _cell.args.id
|
|
|
|
$_.self[ACTORDATA].id = _cell.id
|
|
|
|
// Actor's timeslice for processing a single message
|
|
function turn(msg)
|
|
{
|
|
var mes = wota.decode(msg)
|
|
handle_message(mes)
|
|
send_messages()
|
|
}
|
|
|
|
//log.console(`FIXME: need to get main from config, not just set to true`)
|
|
//log.console(`FIXME: remove global access (ie globalThis.use)`)
|
|
//log.console(`FIXME: add freeze/unfreeze at this level, so we can do it (but scripts cannot)`)
|
|
actor_mod.register_actor(_cell.id, turn, true, config.ar_timer)
|
|
|
|
if (config.actor_memory)
|
|
js.mem_limit(config.actor_memory)
|
|
|
|
if (config.stack_max)
|
|
js.max_stacksize(config.system.stack_max);
|
|
|
|
overling = _cell.args.overling
|
|
$_.overling = overling
|
|
|
|
root = _cell.args.root
|
|
root ??= $_.self
|
|
|
|
if (overling) {
|
|
$_.couple(overling) // auto couple to overling
|
|
|
|
report_to_overling({type:'greet', actor: $_.self})
|
|
}
|
|
|
|
// sys messages are always dispatched immediately
|
|
function sys_msg(actor, msg)
|
|
{
|
|
actor_send(actor, {[SYSYM]:msg})
|
|
}
|
|
|
|
// messages sent to here get put into the cb provided to start
|
|
function report_to_overling(msg)
|
|
{
|
|
if (!overling) return
|
|
sys_msg(overling, {kind:'underling', message:msg, from: $_.self})
|
|
}
|
|
|
|
// Determine the program to run from command line
|
|
var program = _cell.args.program
|
|
|
|
if (!program) {
|
|
log.error('No program specified. Usage: cell <program.ce> [args...]')
|
|
os.exit(1)
|
|
}
|
|
|
|
function handle_actor_disconnect(id) {
|
|
var greeter = greeters[id]
|
|
if (greeter) {
|
|
greeter({type: "stopped", id})
|
|
delete greeters[id]
|
|
}
|
|
log.system(`actor ${id} disconnected`)
|
|
if (couplings.has(id)) disrupt("coupled actor died") // couplings now disrupts instead of stop
|
|
}
|
|
|
|
function handle_sysym(msg)
|
|
{
|
|
switch(msg.kind) {
|
|
case 'stop':
|
|
disrupt("got stop message")
|
|
break
|
|
case 'underling':
|
|
var from = msg.from
|
|
var greeter = greeters[from[ACTORDATA].id]
|
|
if (greeter) greeter(msg.message)
|
|
if (msg.message.type == 'disrupt')
|
|
underlings.delete(from[ACTORDATA].id)
|
|
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 'couple': // from must be notified when we die
|
|
var from = msg.from
|
|
underlings.add(from[ACTORDATA].id)
|
|
log.system(`actor ${from} is coupled to me`)
|
|
break
|
|
}
|
|
}
|
|
|
|
function handle_message(msg) {
|
|
if (msg[SYSYM]) {
|
|
handle_sysym(msg[SYSYM], msg.from)
|
|
return
|
|
}
|
|
|
|
switch (msg.type) {
|
|
case "user":
|
|
var letter = msg.data // what the sender really sent
|
|
_ObjectDefineProperty(letter, HEADER, {
|
|
value: msg, enumerable: false
|
|
})
|
|
_ObjectDefineProperty(letter, ACTORDATA, { // this is so is_actor == true
|
|
value: { reply: msg.reply }, enumerable: false
|
|
})
|
|
|
|
if (msg.return) {
|
|
var fn = replies[msg.return]
|
|
if (fn) fn(letter)
|
|
delete replies[msg.return]
|
|
return
|
|
}
|
|
|
|
if (receive_fn) receive_fn(letter)
|
|
return
|
|
case "stopped":
|
|
handle_actor_disconnect(msg.id)
|
|
break
|
|
}
|
|
};
|
|
|
|
function enet_check()
|
|
{
|
|
if (portal) portal.service(handle_host)
|
|
|
|
$_.delay(enet_check, ENETSERVICE);
|
|
}
|
|
|
|
// enet_check();
|
|
|
|
// Finally, run the program
|
|
actor_mod.setname(_cell.args.program)
|
|
|
|
var prog = _cell.args.program
|
|
|
|
var package = use_core('package')
|
|
|
|
var locator = shop.resolve_locator(_cell.args.program + ".ce", null)
|
|
|
|
if (!locator) {
|
|
var pkg = package.find_package_dir(_cell.args.program + ".ce")
|
|
locator = shop.resolve_locator(_cell.args.program + ".ce", pkg)
|
|
}
|
|
|
|
if (!locator)
|
|
throw new Error(`Main program ${_cell.args.program} could not be found`)
|
|
|
|
// Hide JavaScript built-ins - make them inaccessible
|
|
// Store references we need internally before deleting
|
|
var _Object = Object
|
|
var _Array = Array
|
|
var _String = String
|
|
var _Number = Number
|
|
var _Boolean = Boolean
|
|
var _Math = Math
|
|
var _Function = Function
|
|
|
|
var _Error = Error
|
|
var _JSON = JSON
|
|
|
|
// juicing these before Math is gone
|
|
|
|
use_core('math/radians')
|
|
use_core('math/cycles')
|
|
use_core('math/degrees')
|
|
|
|
// Delete from globalThis
|
|
delete globalThis.Object
|
|
delete globalThis.Math
|
|
delete globalThis.Number
|
|
delete globalThis.String
|
|
delete globalThis.Array
|
|
delete globalThis.Boolean
|
|
delete globalThis.Date
|
|
delete globalThis.Function
|
|
delete globalThis.Reflect
|
|
delete globalThis.Proxy
|
|
delete globalThis.WeakMap
|
|
delete globalThis.WeakSet
|
|
delete globalThis.WeakRef
|
|
delete globalThis.BigInt
|
|
delete globalThis.Symbol
|
|
//delete globalThis.Map
|
|
//delete globalThis.Set
|
|
delete globalThis.Promise
|
|
delete globalThis.ArrayBuffer
|
|
delete globalThis.DataView
|
|
delete globalThis.Int8Array
|
|
delete globalThis.Uint8Array
|
|
delete globalThis.Uint8ClampedArray
|
|
delete globalThis.Int16Array
|
|
delete globalThis.Uint16Array
|
|
delete globalThis.Int32Array
|
|
delete globalThis.Uint32Array
|
|
delete globalThis.Float32Array
|
|
delete globalThis.Float64Array
|
|
delete globalThis.BigInt64Array
|
|
delete globalThis.BigUint64Array
|
|
delete globalThis.eval
|
|
delete globalThis.parseInt
|
|
delete globalThis.parseFloat
|
|
delete globalThis.isNaN
|
|
delete globalThis.isFinite
|
|
delete globalThis.decodeURI
|
|
delete globalThis.decodeURIComponent
|
|
delete globalThis.encodeURI
|
|
delete globalThis.encodeURIComponent
|
|
delete globalThis.escape
|
|
delete globalThis.unescape
|
|
delete globalThis.Intl
|
|
delete globalThis.RegExp
|
|
|
|
_ObjectFreeze(globalThis)
|
|
|
|
$_.clock(_ => {
|
|
// Get capabilities for the main program
|
|
var file_info = shop.file_info ? shop.file_info(locator.path) : null
|
|
var inject = shop.script_inject_for ? shop.script_inject_for(file_info) : []
|
|
|
|
// Build values array for injection
|
|
var vals = []
|
|
for (var i = 0; i < inject.length; i++) {
|
|
var key = inject[i]
|
|
if (key && key[0] == '$') key = key.substring(1)
|
|
if (key == 'fd') vals.push(fd)
|
|
else vals.push($_[key])
|
|
}
|
|
|
|
// Create use function bound to the program's package
|
|
var pkg = file_info ? file_info.package : null
|
|
var use_fn = function(path) { return shop.use(path, pkg) }
|
|
|
|
// Call with signature: setup_module(args, use, ...capabilities)
|
|
// The script wrapper builds $_ from the injected capabilities for backward compatibility
|
|
var val = locator.symbol.call(null, _cell.args.arg, use_fn, ...vals)
|
|
|
|
if (val)
|
|
throw new Error('Program must not return anything');
|
|
})
|
|
|
|
})() |