diff --git a/docs/actors.md b/docs/actors.md index 1e2974a2..4a8f319b 100644 --- a/docs/actors.md +++ b/docs/actors.md @@ -67,9 +67,9 @@ An actor is a script that **does not return a value**. It runs as an independent // worker.ce print("Worker started") -$receiver(function(msg, reply) { +$receiver(function(msg) { print("Received:", msg) - // Process message... + send(msg, {status: "ok"}) }) ``` @@ -83,106 +83,128 @@ $receiver(function(msg, reply) { Actors have access to special functions prefixed with `$`: -### $me +### $self -Reference to the current actor. +Reference to the current actor. This is a stone (immutable) actor object. ```javascript -print($me) // actor reference +print($self) // actor reference +print(is_actor($self)) // true +``` + +### $overling + +Reference to the parent actor that started this actor. `null` for the root actor. Child actors are automatically coupled to their overling — if the parent dies, the child dies too. + +```javascript +if ($overling != null) { + send($overling, {status: "ready"}) +} ``` ### $stop() -Stop the current actor. +Stop the current actor. When called with an actor argument, stops that underling (child) instead. ```javascript -$stop() +$stop() // stop self +$stop(child) // stop a child actor ``` -### $send(actor, message, callback) - -Send a message to another actor. - -```javascript -$send(other_actor, {type: "ping", data: 42}, function(reply) { - print("Got reply:", reply) -}) -``` - -Messages are automatically **splatted** — flattened to plain data without prototypes. - ### $start(callback, program) -Start a new actor from a script. +Start a new child actor from a script. The callback receives lifecycle events: + +- `{type: "greet", actor: }` — child started successfully +- `{type: "stop"}` — child stopped cleanly +- `{type: "disrupt", reason: ...}` — child crashed ```javascript -$start(function(new_actor) { - print("Started:", new_actor) +$start(function(event) { + if (event.type == 'greet') { + print("Child started:", event.actor) + send(event.actor, {task: "work"}) + } + if (event.type == 'stop') { + print("Child stopped") + } + if (event.type == 'disrupt') { + print("Child crashed:", event.reason) + } }, "worker") ``` ### $delay(callback, seconds) -Schedule a callback after a delay. +Schedule a callback after a delay. Returns a cancel function that can be called to prevent the callback from firing. ```javascript -$delay(function() { +var cancel = $delay(function() { print("5 seconds later") }, 5) + +// To cancel before it fires: +cancel() ``` ### $clock(callback) -Get called every frame/tick. +Get called every frame/tick. The callback receives the current time as a number. ```javascript -$clock(function(dt) { - // Called each tick with delta time +$clock(function(t) { + // called each tick with current time }) ``` ### $receiver(callback) -Set up a message receiver. +Set up a message receiver. The callback is called with the incoming message whenever another actor sends a message to this actor. + +To reply to a message, call `send(message, reply_data)` — the message object contains routing information that directs the reply back to the sender. ```javascript -$receiver(function(message, reply) { - // Handle incoming message - reply({status: "ok"}) +$receiver(function(message) { + // handle incoming message + send(message, {status: "ok"}) }) ``` ### $portal(callback, port) -Open a network port. +Open a network port to receive connections from remote actors. ```javascript $portal(function(connection) { - // Handle new connection + // handle new connection }, 8080) ``` ### $contact(callback, record) -Connect to a remote address. +Connect to a remote actor at a given address. ```javascript $contact(function(connection) { - // Connected + // connected }, {host: "example.com", port: 80}) ``` ### $time_limit(requestor, seconds) -Wrap a requestor with a timeout. See [Requestors](/docs/requestors/) for details. +Wrap a requestor with a timeout. Returns a new requestor that will cancel the original and call its callback with a failure if the time limit is exceeded. See [Requestors](/docs/requestors/) for details. ```javascript -$time_limit(my_requestor, 10) // 10 second timeout +var timed = $time_limit(my_requestor, 10) + +timed(function(result, reason) { + // reason will explain timeout if it fires +}, initial_value) ``` ### $couple(actor) -Couple the current actor to another actor. When the coupled actor dies, the current actor also dies. Coupling is automatic between an actor and its overling (parent). +Couple the current actor to another actor. When the coupled actor dies, the current actor also dies. Coupling is automatic between a child actor and its overling (parent). ```javascript $couple(other_actor) @@ -190,7 +212,7 @@ $couple(other_actor) ### $unneeded(callback, seconds) -Schedule the actor for removal after a specified time. +Schedule the actor for removal after a specified time. The callback fires when the time elapses. ```javascript $unneeded(function() { @@ -200,20 +222,76 @@ $unneeded(function() { ### $connection(callback, actor, config) -Get information about the connection to another actor, such as latency, bandwidth, and activity. +Get information about the connection to another actor. For local actors, returns `{type: "local"}`. For remote actors, returns connection details including latency, bandwidth, and activity. ```javascript $connection(function(info) { - print(info.latency) + if (info.type == "local") { + print("same machine") + } else { + print(info.latency) + } }, other_actor, {}) ``` +## Runtime Functions + +These functions are available in actors without the `$` prefix: + +### send(actor, message, callback) + +Send a message to another actor. The message must be an object record. + +The optional callback receives the reply when the recipient responds. + +```javascript +send(other_actor, {type: "ping"}, function(reply) { + print("Got reply:", reply) +}) +``` + +To reply to a received message, pass the message itself as the first argument — it contains routing information: + +```javascript +$receiver(function(message) { + send(message, {result: 42}) +}) +``` + +Messages are automatically flattened to plain data. + +### is_actor(value) + +Returns `true` if the value is an actor reference. + +```javascript +if (is_actor(some_value)) { + send(some_value, {ping: true}) +} +``` + +### log + +Logging functions: `log.console(msg)`, `log.error(msg)`, `log.system(msg)`. + +### use(path) + +Import a module. See [Module Resolution](#module-resolution) below. + +### args + +Array of command-line arguments passed to the actor. + +### sequence(), parallel(), race(), fallback() + +Requestor composition functions. See [Requestors](/docs/requestors/) for details. + ## Module Resolution When you call `use('name')`, ƿit searches: 1. **Current package** — files relative to package root -2. **Dependencies** — packages declared in `pit.toml` +2. **Dependencies** — packages declared in `cell.toml` 3. **Core** — built-in ƿit modules ```javascript @@ -234,8 +312,14 @@ var config = use('config') print("Starting application...") -$start(function(worker) { - $send(worker, {task: "process", data: [1, 2, 3]}) +$start(function(event) { + if (event.type == 'greet') { + send(event.actor, {task: "process", data: [1, 2, 3]}) + } + if (event.type == 'stop') { + print("Worker finished") + $stop() + } }, "worker") $delay(function() { @@ -246,11 +330,12 @@ $delay(function() { ```javascript // worker.ce - Worker actor -$receiver(function(msg, reply) { +$receiver(function(msg) { if (msg.task == "process") { - var result = array(msg.data, x => x * 2) - reply({result: result}) + var result = array(msg.data, function(x) { return x * 2 }) + send(msg, {result: result}) } + $stop() }) ``` diff --git a/docs/requestors.md b/docs/requestors.md index 2dc18ab2..6921a733 100644 --- a/docs/requestors.md +++ b/docs/requestors.md @@ -31,7 +31,7 @@ The cancel function, when called, should abort the in-progress work. ```javascript var fetch_data = function(callback, url) { $contact(function(connection) { - $send(connection, {get: url}, function(response) { + send(connection, {get: url}, function(response) { callback(response) }) }, {host: url, port: 80}) @@ -154,11 +154,11 @@ If the requestor does not complete within the time limit, it is cancelled and th ## Requestors and Actors -Requestors are particularly useful with actor messaging. Since `$send` is callback-based, it fits naturally: +Requestors are particularly useful with actor messaging. Since `send` is callback-based, it fits naturally: ```javascript var ask_worker = function(callback, task) { - $send(worker, task, function(reply) { + send(worker, task, function(reply) { callback(reply) }) } diff --git a/tests/actor_clock.ce b/tests/actor_clock.ce new file mode 100644 index 00000000..7c788c49 --- /dev/null +++ b/tests/actor_clock.ce @@ -0,0 +1,6 @@ +// Test: $clock fires with a time number +$clock(function(t) { + if (!is_number(t)) disrupt + if (t <= 0) disrupt + $stop() +}) diff --git a/tests/actor_connection.ce b/tests/actor_connection.ce new file mode 100644 index 00000000..4f7754b3 --- /dev/null +++ b/tests/actor_connection.ce @@ -0,0 +1,9 @@ +// Test: $connection reports local for a child actor +$start(function(event) { + if (event.type == 'greet') { + $connection(function(info) { + if (info.type != "local") disrupt + $stop() + }, event.actor, {}) + } +}, 'tests/actor_helper_echo') diff --git a/tests/actor_couple.ce b/tests/actor_couple.ce new file mode 100644 index 00000000..28bee718 --- /dev/null +++ b/tests/actor_couple.ce @@ -0,0 +1,8 @@ +// Test: $couple($self) is a no-op, $couple on a child doesn't disrupt +$couple($self) +$start(function(event) { + if (event.type == 'greet') { + $couple(event.actor) + $delay($stop, 0.1) + } +}, 'tests/actor_helper_echo') diff --git a/tests/actor_delay_cancel.ce b/tests/actor_delay_cancel.ce new file mode 100644 index 00000000..1bdbd250 --- /dev/null +++ b/tests/actor_delay_cancel.ce @@ -0,0 +1,10 @@ +// Test: $delay returns a cancel function that prevents the callback +var fired = false +var cancel = $delay(function() { + fired = true +}, 0.5) +cancel() +var _t = $delay(function() { + if (fired) disrupt + $stop() +}, 1) diff --git a/tests/actor_helper_echo.ce b/tests/actor_helper_echo.ce new file mode 100644 index 00000000..d1b2bc41 --- /dev/null +++ b/tests/actor_helper_echo.ce @@ -0,0 +1,5 @@ +// Helper actor that echoes messages back with {pong: true} +$receiver(function(msg) { + send(msg, {pong: true}) +}) +var _t = $delay($stop, 5) diff --git a/tests/actor_helper_report.ce b/tests/actor_helper_report.ce new file mode 100644 index 00000000..bea60a9a --- /dev/null +++ b/tests/actor_helper_report.ce @@ -0,0 +1,5 @@ +// Helper actor that reports $overling status +$receiver(function(msg) { + send(msg, {has_overling: $overling != null}) +}) +var _t = $delay($stop, 5) diff --git a/tests/actor_helper_stop.ce b/tests/actor_helper_stop.ce new file mode 100644 index 00000000..4728eab3 --- /dev/null +++ b/tests/actor_helper_stop.ce @@ -0,0 +1,2 @@ +// Helper actor that stops after 0.1 seconds +var _t = $delay($stop, 0.1) diff --git a/tests/actor_overling.ce b/tests/actor_overling.ce new file mode 100644 index 00000000..5397ef4e --- /dev/null +++ b/tests/actor_overling.ce @@ -0,0 +1,9 @@ +// Test: child actor reports $overling is not null +$start(function(event) { + if (event.type == 'greet') { + send(event.actor, {check: true}, function(reply) { + if (!reply.has_overling) disrupt + $stop() + }) + } +}, 'tests/actor_helper_report') diff --git a/tests/actor_receiver.ce b/tests/actor_receiver.ce new file mode 100644 index 00000000..dceb546b --- /dev/null +++ b/tests/actor_receiver.ce @@ -0,0 +1,6 @@ +// Test: $receiver fires when sending to self +$receiver(function(msg) { + if (!msg.test) disrupt + $stop() +}) +send($self, {test: true}) diff --git a/tests/actor_requestors.ce b/tests/actor_requestors.ce new file mode 100644 index 00000000..ddf2128f --- /dev/null +++ b/tests/actor_requestors.ce @@ -0,0 +1,30 @@ +// Test: sequence, parallel, and fallback requestor composition +var immediate = function(callback, value) { + callback(42) +} + +var add_one = function(callback, value) { + callback(value + 1) +} + +var broken = function(callback, value) { + callback(null, "broken") +} + +var _t = sequence([immediate, add_one])(function(result, reason) { + if (reason != null) disrupt + if (result != 43) disrupt + + parallel([immediate, immediate])(function(results, reason) { + if (reason != null) disrupt + if (length(results) != 2) disrupt + if (results[0] != 42) disrupt + if (results[1] != 42) disrupt + + fallback([broken, immediate])(function(result, reason) { + if (reason != null) disrupt + if (result != 42) disrupt + $stop() + }) + }) +}) diff --git a/tests/actor_self.ce b/tests/actor_self.ce new file mode 100644 index 00000000..fa410076 --- /dev/null +++ b/tests/actor_self.ce @@ -0,0 +1,5 @@ +// Test: $self and is_actor +if ($self == null) disrupt +if (!is_actor($self)) disrupt +if (!is_stone($self)) disrupt +$stop() diff --git a/tests/actor_send_reply.ce b/tests/actor_send_reply.ce new file mode 100644 index 00000000..c1257187 --- /dev/null +++ b/tests/actor_send_reply.ce @@ -0,0 +1,9 @@ +// Test: send with reply callback +$start(function(event) { + if (event.type == 'greet') { + send(event.actor, {ping: true}, function(reply) { + if (!reply.pong) disrupt + $stop() + }) + } +}, 'tests/actor_helper_echo') diff --git a/tests/actor_start.ce b/tests/actor_start.ce new file mode 100644 index 00000000..b665bf6b --- /dev/null +++ b/tests/actor_start.ce @@ -0,0 +1,13 @@ +// Test: $start lifecycle events (greet and stop) +var got_greet = false +$start(function(event) { + if (event.type == 'greet') { + if (!event.actor) disrupt + if (!is_actor(event.actor)) disrupt + got_greet = true + } + if (event.type == 'stop') { + if (!got_greet) disrupt + $stop() + } +}, 'tests/actor_helper_stop') diff --git a/tests/actor_time_limit.ce b/tests/actor_time_limit.ce new file mode 100644 index 00000000..552728ab --- /dev/null +++ b/tests/actor_time_limit.ce @@ -0,0 +1,10 @@ +// Test: $time_limit fires timeout for a requestor that never completes +var never_complete = function(callback, value) { + return null +} +var timed = $time_limit(never_complete, 0.5) +var _t = timed(function(val, reason) { + if (val != null) disrupt + if (reason == null) disrupt + $stop() +}) diff --git a/tests/actor_unneeded.ce b/tests/actor_unneeded.ce new file mode 100644 index 00000000..874aa609 --- /dev/null +++ b/tests/actor_unneeded.ce @@ -0,0 +1,2 @@ +// Test: $unneeded fires after the specified time +$unneeded($stop, 1) diff --git a/tests/decl_restrictions.ce b/tests/decl_restrictions.ce index c2ce2280..413c56fc 100644 --- a/tests/decl_restrictions.ce +++ b/tests/decl_restrictions.ce @@ -163,3 +163,4 @@ if (failed > 0) { print(" FAIL " + error_names[_j] + ": " + error_reasons[_j]) } } +$stop() diff --git a/tests/demo.ce b/tests/demo.ce index 90c55a40..dc9c5872 100644 --- a/tests/demo.ce +++ b/tests/demo.ce @@ -37,3 +37,4 @@ function test_nested() { test_nested() print("done") +$stop() diff --git a/tests/kill.ce b/tests/kill.ce index f99668b4..9b715da4 100644 --- a/tests/kill.ce +++ b/tests/kill.ce @@ -8,7 +8,7 @@ $start(e => { if (e.type == 'disrupt') { log.console(`underling successfully killed.`) - send($parent, { type: "test_result", passed: true }) + send($overling, {type: "test_result", passed: true}) $stop() } }, 'tests/hang_actor') diff --git a/tests/reply_actor.ce b/tests/reply_actor.ce index f7e21a3b..6309328b 100644 --- a/tests/reply_actor.ce +++ b/tests/reply_actor.ce @@ -1,2 +1,3 @@ // tests/reply_actor.ce - Simple child that just logs log.console("reply_actor: alive!") +$stop()