Object Run
#[run] does for a whole object what #[main] does for a document: self.run() executes every #[run]-tagged function and field on that object, treating it as a workflow rather than something you call one function at a time.
Basic Run
Every #[run] function on the object executes when you call run():
Workflow: {
out: []
#[run]
fn step_one() { self.out.push_back('one'); }
#[run]
fn step_two() { self.out.push_back('two'); }
}
#[main]
fn main() {
self.Workflow.run();
pln(self.Workflow.out);
}Ordering with #[run(N)]
A number controls execution order — regardless of the order things are declared in the document:
Workflow: {
out: []
#[run(3)]
fn third() { self.out.push_back(3); }
#[run(1)]
fn first() { self.out.push_back(1); }
#[run(2)]
fn second() { self.out.push_back(2); }
}
#[main]
fn main() {
self.Workflow.run();
pln(self.Workflow.out);
}Running Nested Objects
If a child object is itself #[run]-tagged, running the parent recurses into it — each step in a multi-stage workflow can be its own object:
Workflow: {
#[run(1)]
step_one: {
#[run]
fn execute() { pln('step 1'); }
}
#[run(2)]
step_two: {
#[run]
fn execute() { pln('step 2'); }
}
}
#[main]
fn main() {
self.Workflow.run();
}Passing Arguments
#[run({'args': [...]})] passes arguments through to the function it's attached to:
Workflow: {
#[run({'args': [42]})]
fn setup(val: int) {
self.configured = val;
}
}
#[main]
fn main() {
self.Workflow.run();
pln(self.Workflow.configured);
}Running a List
#[run] on a list field runs every element — an arrow function, an object with its own #[run] members, whatever's in there:
Workflow: {
done: false
sub_done: false
#[run]
steps: [
() => { self.done = true; },
{ #[run] fn inner() { super.sub_done = true; } }
]
}
#[main]
fn main() {
self.Workflow.run();
pln(self.Workflow.done, self.Workflow.sub_done);
}A Realistic Shape
None of these mechanics need a network to demonstrate, but the pattern they're building toward usually involves one — fetch something, validate what comes back, and record the result, all in one self-contained object:
#[type]
Task: {
ok: false
result: null
config: {
endpoint: "https://myendpoint"
schema: { /* a schema to validate the result against */ }
}
#[run]
fn exec() {
const res = await Http.fetch(self.config.endpoint);
const result = new {};
parse(res.remove("text"), result, "json");
self.ok = self.config.schema.schemafy(result);
self.result = result;
}
}
This one isn't runnable here — Http isn't a library this sandbox has enabled — but it's the shape #[run] is really for: a task object that knows how to execute itself, called the same way (task.run()) whether it's this or one of the five-line examples above it.
Async Run
Obj.run(..) is syncronous on purpose, but it is common to have async pipelines. With Async, there are many ways one can go about this.
This example may give you a few ideas:
#[type]
/// Setup a type that handles async the way you'd like.
/// Common to put helpers in a Job type that can resolve to the Workflow parent, etc.
Workflow: {
list jobs: []
list handles: []
#[run(1)]
fn run_jobs() {
for (const job in self.jobs) {
// in real-life I'd switch on type and add fn (async gets pushed to handles, etc.)
if (job.parent() != self) job.move(self);
job.run();
}
}
#[run(2)]
fn await_handles() {
pln('awaiting handles', self.handles.len());
await self.handles;
self.handles = []; // reset handles for any #[run(3)] and beyond
}
}
#[type]
Job: {
text: 'default'
#[run] fn running() { pln(self.text); }
}
#[type]
AsyncJob: {
#[run]
fn running() {
const handles = super.handles ?? [];
handles.push_back(async { sleep(10ms); pln('Running an Async Job!'); });
}
}
#[main]
fn main() {
const flow = new Workflow {};
flow.jobs.push_back(new Job { text: 'First Job' });
flow.jobs.push_back(new AsyncJob {});
flow.run();
pln('Done Running');
}