1use super::{WorkContext, WorkHost, WorkOptions, WorkRun};
10use crate::core::{Promise, PromiseRejection, PromiseState, Value};
11use crate::lang::protocol::INamespaced;
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::rc::Rc;
15
16const VERSION_KEY: &str = "work/plan-version";
17const OP_KEY: &str = "work/op";
18const TARGET_KEY: &str = "work/target";
19const CHILDREN_KEY: &str = "work/children";
20const CHILD_KEY: &str = "work/child";
21const SOURCE_KEY: &str = "work/source";
22const CONTINUATION_KEY: &str = "work/continuation-target";
23const CLEANUP_KEY: &str = "work/cleanup";
24const INITIAL_KEY: &str = "work/initial";
25const REDUCER_KEY: &str = "work/reducer";
26const SELECTOR_KEY: &str = "work/selector";
27const CHOICES_KEY: &str = "work/choices";
28const WAIT_KEY: &str = "work/wait";
29const MAXIMUM_DEPTH_KEY: &str = "work/maximum-depth";
30const NODES_KEY: &str = "work/nodes";
31const ORDER_KEY: &str = "work/order";
32const PROCESS_KEY: &str = "work/process";
33
34fn key(name: &str) -> Value {
35 Value::Keyword(name.into())
36}
37
38fn map(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
39 Value::Map(
40 entries
41 .into_iter()
42 .map(|(name, value)| (key(name), value))
43 .collect(),
44 )
45}
46
47fn field(value: &Value, name: &str) -> Option<Value> {
48 match value {
49 Value::Map(fields) => fields.get(&key(name)).cloned(),
50 Value::OrderedMap(fields) => fields.get(&key(name)).cloned(),
51 Value::SortedMap(fields) => fields.get(&key(name)).cloned(),
52 _ => None,
53 }
54}
55
56fn vector(value: Value, name: &str) -> Result<Vec<Value>, String> {
57 match value {
58 Value::Vector(values) => Ok(values.into_iter().collect()),
59 Value::Tuple(values) => Ok(values.into_iter().collect()),
60 _ => Err(format!("work/plan-invalid: {name} must be a vector")),
61 }
62}
63
64pub fn target_name(value: Value) -> Result<String, String> {
65 match value {
66 Value::String(value) => non_blank_target(value),
67 Value::Keyword(value) => non_blank_target(value.to_string()),
68 Value::Symbol(value) => non_blank_target(value.to_string()),
69 _ => Err("work/plan-invalid: work target must be a string, keyword, or symbol".into()),
70 }
71}
72
73fn non_blank_target(value: String) -> Result<String, String> {
74 if value.trim().is_empty() {
75 Err("work/plan-invalid: work target cannot be blank".into())
76 } else {
77 Ok(value)
78 }
79}
80
81fn plan_error(message: impl Into<String>) -> PromiseRejection {
82 PromiseRejection::Value(map([
83 ("code", Value::Keyword("work/plan-error".into())),
84 ("message", Value::String(message.into())),
85 ("retryable", Value::Bool(false)),
86 ]))
87}
88
89fn truthy(value: &Value) -> bool {
90 !matches!(value, Value::Nil | Value::Bool(false))
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum WorkOperation {
96 Pure,
97 Step,
98 Chain,
99 Each,
100 Filter,
101 Fold,
102 All,
103 Choose,
104 Graph,
105 Batch,
106 Bind,
107 Ensure,
108 Await,
109}
110
111impl WorkOperation {
112 pub const VERSION: i64 = 1;
113
114 pub fn as_str(self) -> &'static str {
115 match self {
116 Self::Pure => "pure",
117 Self::Step => "step",
118 Self::Chain => "chain",
119 Self::Each => "each",
120 Self::Filter => "filter",
121 Self::Fold => "fold",
122 Self::All => "all",
123 Self::Choose => "choose",
124 Self::Graph => "graph",
125 Self::Batch => "batch",
126 Self::Bind => "bind",
127 Self::Ensure => "ensure",
128 Self::Await => "await",
129 }
130 }
131
132 pub fn parse(value: &Value) -> Result<Self, String> {
133 let Value::Keyword(value) = value else {
134 return Err("work/plan-invalid: work operation must be a keyword".into());
135 };
136 match value.get_name() {
137 "pure" => Ok(Self::Pure),
138 "step" => Ok(Self::Step),
139 "chain" => Ok(Self::Chain),
140 "each" => Ok(Self::Each),
141 "filter" => Ok(Self::Filter),
142 "fold" => Ok(Self::Fold),
143 "all" => Ok(Self::All),
144 "choose" => Ok(Self::Choose),
145 "graph" => Ok(Self::Graph),
146 "batch" => Ok(Self::Batch),
147 "bind" => Ok(Self::Bind),
148 "ensure" => Ok(Self::Ensure),
149 "await" => Ok(Self::Await),
150 _ => Err(format!("work/plan-unsupported: {}", value.as_str())),
151 }
152 }
153}
154
155#[derive(Clone, Debug)]
157pub struct WorkPlan {
158 value: Value,
159}
160
161impl WorkPlan {
162 pub fn from_value(value: Value) -> Result<Self, String> {
163 validate_plan(&value)?;
164 Ok(Self { value })
165 }
166
167 pub fn value(&self) -> Value {
168 self.value.clone()
169 }
170
171 pub fn operation(&self) -> WorkOperation {
172 WorkOperation::parse(&field(&self.value, OP_KEY).expect("validated plan has operation"))
173 .expect("validated plan has supported operation")
174 }
175
176 pub fn encode_hta(&self) -> Result<Vec<u8>, String> {
177 crate::hta::encode(&self.value)
178 }
179
180 pub fn decode_hta(bytes: &[u8]) -> Result<Self, String> {
181 Self::from_value(crate::hta::decode_canonical(bytes)?)
182 }
183
184 pub fn pure(target: impl Into<String>) -> Result<Self, String> {
185 Self::leaf(WorkOperation::Pure, target)
186 }
187
188 pub fn step(target: impl Into<String>) -> Result<Self, String> {
189 Self::leaf(WorkOperation::Step, target)
190 }
191
192 pub fn leaf(operation: WorkOperation, target: impl Into<String>) -> Result<Self, String> {
193 match operation {
194 WorkOperation::Pure | WorkOperation::Step => Self::from_value(map([
195 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
196 (OP_KEY, Value::Keyword(operation.as_str().into())),
197 (TARGET_KEY, Value::String(non_blank_target(target.into())?)),
198 ])),
199 _ => Err("work/plan-invalid: only pure and step are leaf operations".into()),
200 }
201 }
202
203 pub fn chain(children: Vec<Self>) -> Result<Self, String> {
204 Self::children(WorkOperation::Chain, children)
205 }
206
207 pub fn all(children: Vec<Self>) -> Result<Self, String> {
208 Self::children(WorkOperation::All, children)
209 }
210
211 pub fn each(child: Self) -> Result<Self, String> {
212 Self::child(WorkOperation::Each, child)
213 }
214
215 pub fn filter(child: Self) -> Result<Self, String> {
216 Self::child(WorkOperation::Filter, child)
217 }
218
219 pub fn children(operation: WorkOperation, children: Vec<Self>) -> Result<Self, String> {
220 Self::from_value(map([
221 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
222 (OP_KEY, Value::Keyword(operation.as_str().into())),
223 (
224 CHILDREN_KEY,
225 Value::Vector(children.into_iter().map(|child| child.value).collect()),
226 ),
227 ]))
228 }
229
230 pub fn child(operation: WorkOperation, child: Self) -> Result<Self, String> {
231 Self::from_value(map([
232 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
233 (OP_KEY, Value::Keyword(operation.as_str().into())),
234 (CHILD_KEY, child.value),
235 ]))
236 }
237
238 pub fn fold(initial: Value, reducer: Self) -> Result<Self, String> {
239 Self::from_value(map([
240 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
241 (OP_KEY, Value::Keyword("fold".into())),
242 (INITIAL_KEY, initial),
243 (REDUCER_KEY, reducer.value),
244 ]))
245 }
246
247 pub fn choose(selector: Self, choices: Value) -> Result<Self, String> {
248 Self::from_value(map([
249 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
250 (OP_KEY, Value::Keyword("choose".into())),
251 (SELECTOR_KEY, selector.value),
252 (CHOICES_KEY, choices),
253 ]))
254 }
255
256 pub fn graph(graph: Value) -> Result<Self, String> {
257 Self::generic(WorkOperation::Graph, graph)
258 }
259
260 pub fn batch(stages: Value) -> Result<Self, String> {
261 Self::generic(WorkOperation::Batch, stages)
262 }
263
264 pub fn bind(source: Self, continuation_target: impl Into<String>) -> Result<Self, String> {
265 Self::from_value(map([
266 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
267 (OP_KEY, Value::Keyword("bind".into())),
268 (SOURCE_KEY, source.value),
269 (
270 CONTINUATION_KEY,
271 Value::String(non_blank_target(continuation_target.into())?),
272 ),
273 ]))
274 }
275
276 pub fn ensure(body: Self, cleanup: Self) -> Result<Self, String> {
277 Self::from_value(map([
278 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
279 (OP_KEY, Value::Keyword("ensure".into())),
280 (CHILD_KEY, body.value),
281 (CLEANUP_KEY, cleanup.value),
282 ]))
283 }
284
285 pub fn await_(wait: Value) -> Result<Self, String> {
286 Self::from_value(map([
287 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
288 (OP_KEY, Value::Keyword("await".into())),
289 (WAIT_KEY, wait),
290 ]))
291 }
292
293 pub fn generic(operation: WorkOperation, fields: Value) -> Result<Self, String> {
294 let fields = map_entries(&fields, "operation fields")?;
295 let output = fields
296 .into_iter()
297 .chain([
298 (key(VERSION_KEY), Value::Number(WorkOperation::VERSION)),
299 (key(OP_KEY), Value::Keyword(operation.as_str().into())),
300 ])
301 .collect();
302 Self::from_value(Value::Map(output))
303 }
304}
305
306pub type WorkTarget = Rc<dyn Fn(Value, WorkContext) -> Result<Value, PromiseRejection>>;
308
309#[derive(Clone, Default)]
311pub struct WorkRegistry {
312 targets: Rc<RefCell<HashMap<String, WorkTarget>>>,
313}
314
315impl WorkRegistry {
316 pub fn bind(&self, name: impl Into<String>, target: WorkTarget) -> Result<(), String> {
317 let name = non_blank_target(name.into())?;
318 self.targets.borrow_mut().insert(name, target);
319 Ok(())
320 }
321
322 pub fn unbind(&self, name: &str) -> bool {
323 self.targets.borrow_mut().remove(name).is_some()
324 }
325
326 pub fn reset(&self) {
327 self.targets.borrow_mut().clear();
328 }
329
330 pub fn target(&self, name: &str) -> Option<WorkTarget> {
331 self.targets.borrow().get(name).cloned()
332 }
333
334 pub fn target_names(&self) -> Vec<String> {
335 let mut names = self.targets.borrow().keys().cloned().collect::<Vec<_>>();
336 names.sort();
337 names
338 }
339
340 pub fn identity(&self) -> usize {
341 Rc::as_ptr(&self.targets) as usize
342 }
343}
344
345pub type SuspensionTarget = Rc<dyn Fn(Value, WorkContext) -> Result<Value, PromiseRejection>>;
346
347#[derive(Clone, Default)]
349pub struct WorkRuntime {
350 registry: WorkRegistry,
351 suspension: Option<SuspensionTarget>,
352}
353
354impl WorkRuntime {
355 pub fn new(registry: WorkRegistry) -> Self {
356 Self {
357 registry,
358 suspension: None,
359 }
360 }
361
362 pub fn registry(&self) -> WorkRegistry {
363 self.registry.clone()
364 }
365
366 pub fn with_suspension(mut self, suspension: SuspensionTarget) -> Self {
367 self.suspension = Some(suspension);
368 self
369 }
370
371 pub fn reset(&self) {
372 self.registry.reset();
373 }
374
375 pub fn evaluate(
376 &self,
377 plan: &WorkPlan,
378 input: Value,
379 context: WorkContext,
380 ) -> Result<Value, PromiseRejection> {
381 execute(self.clone(), plan.value(), input, context, 0)
382 }
383}
384
385impl WorkHost {
386 pub fn submit_plan(
388 &self,
389 runtime: WorkRuntime,
390 plan: WorkPlan,
391 input: Value,
392 options: WorkOptions,
393 ) -> Result<WorkRun, String> {
394 self.submit_scoped_rejection(options, move |context| {
395 runtime.evaluate(&plan, input, context)
396 })
397 }
398
399 pub fn reset(&self) {
401 self.kill();
402 let mut host = self.inner.borrow_mut();
403 host.queue.clear();
404 host.runs.clear();
405 host.next_id = 1;
406 host.started = true;
407 }
408}
409
410fn validate_plan(value: &Value) -> Result<(), String> {
411 let (Value::Map(_) | Value::OrderedMap(_) | Value::SortedMap(_)) = value else {
412 return Err("work/plan-invalid: plan must be a map".into());
413 };
414 if field(value, VERSION_KEY) != Some(Value::Number(WorkOperation::VERSION)) {
415 return Err("work/plan-invalid: unsupported plan version".into());
416 }
417 let operation =
418 WorkOperation::parse(&field(value, OP_KEY).ok_or("work/plan-invalid: missing operation")?)?;
419 match operation {
420 WorkOperation::Pure | WorkOperation::Step => {
421 target_name(
422 field(value, TARGET_KEY).ok_or("work/plan-invalid: leaf requires target")?,
423 )?;
424 }
425 WorkOperation::Chain | WorkOperation::All => {
426 for child in vector(
427 field(value, CHILDREN_KEY).ok_or("work/plan-invalid: missing children")?,
428 CHILDREN_KEY,
429 )? {
430 validate_plan(&child)?;
431 }
432 }
433 WorkOperation::Each | WorkOperation::Filter => {
434 validate_plan(&field(value, CHILD_KEY).ok_or("work/plan-invalid: missing child")?)?;
435 }
436 WorkOperation::Fold => {
437 validate_plan(&field(value, REDUCER_KEY).ok_or("work/plan-invalid: missing reducer")?)?;
438 }
439 WorkOperation::Choose => {
440 validate_plan(
441 &field(value, SELECTOR_KEY).ok_or("work/plan-invalid: missing selector")?,
442 )?;
443 let choices = field(value, CHOICES_KEY).ok_or("work/plan-invalid: missing choices")?;
444 for choice in map_values(&choices, CHOICES_KEY)? {
445 validate_plan(&choice)?;
446 }
447 }
448 WorkOperation::Bind => {
449 validate_plan(
450 &field(value, SOURCE_KEY).ok_or("work/plan-invalid: missing bind source")?,
451 )?;
452 target_name(
453 field(value, CONTINUATION_KEY)
454 .ok_or("work/plan-invalid: missing continuation target")?,
455 )?;
456 }
457 WorkOperation::Ensure => {
458 validate_plan(
459 &field(value, CHILD_KEY).ok_or("work/plan-invalid: missing ensure body")?,
460 )?;
461 validate_plan(&field(value, CLEANUP_KEY).ok_or("work/plan-invalid: missing cleanup")?)?;
462 }
463 WorkOperation::Await => {
464 if field(value, WAIT_KEY).is_none() {
465 return Err("work/plan-invalid: await requires a wait descriptor".into());
466 }
467 }
468 WorkOperation::Graph => {
469 let nodes = field(value, NODES_KEY).ok_or("work/plan-invalid: missing graph nodes")?;
470 for child in map_values(&nodes, NODES_KEY)? {
471 validate_plan(&child)?;
472 }
473 for id in vector(
474 field(value, ORDER_KEY).ok_or("work/plan-invalid: missing graph order")?,
475 ORDER_KEY,
476 )? {
477 let child = map_lookup(&nodes, &id)
478 .ok_or("work/plan-invalid: graph order refers to an unknown node")?;
479 validate_plan(&child)?;
480 }
481 }
482 WorkOperation::Batch => {
483 validate_plan(
484 &field(value, PROCESS_KEY).ok_or("work/plan-invalid: missing batch process")?,
485 )?;
486 }
487 }
488 Ok(())
489}
490
491fn map_values(value: &Value, name: &str) -> Result<Vec<Value>, String> {
492 match value {
493 Value::Map(entries) => Ok(entries.iter().map(|(_, value)| value.clone()).collect()),
494 Value::OrderedMap(entries) => Ok(entries.iter().map(|(_, value)| value.clone()).collect()),
495 Value::SortedMap(entries) => Ok(entries.iter().map(|(_, value)| value.clone()).collect()),
496 _ => Err(format!("work/plan-invalid: {name} must be a map")),
497 }
498}
499
500fn map_entries(value: &Value, name: &str) -> Result<Vec<(Value, Value)>, String> {
501 match value {
502 Value::Map(entries) => Ok(entries
503 .iter()
504 .map(|(key, value)| (key.clone(), value.clone()))
505 .collect()),
506 Value::OrderedMap(entries) => Ok(entries
507 .iter()
508 .map(|(key, value)| (key.clone(), value.clone()))
509 .collect()),
510 Value::SortedMap(entries) => Ok(entries
511 .iter()
512 .map(|(key, value)| (key.clone(), value.clone()))
513 .collect()),
514 _ => Err(format!("work/plan-invalid: {name} must be a map")),
515 }
516}
517
518fn map_lookup(value: &Value, lookup: &Value) -> Option<Value> {
519 match value {
520 Value::Map(entries) => entries.get(lookup).cloned(),
521 Value::OrderedMap(entries) => entries.get(lookup).cloned(),
522 Value::SortedMap(entries) => entries.get(lookup).cloned(),
523 _ => None,
524 }
525}
526
527fn then(
528 value: Value,
529 continuation: Rc<dyn Fn(Value) -> Result<Value, PromiseRejection>>,
530) -> Result<Value, PromiseRejection> {
531 let Value::Promise(source) = value else {
532 return continuation(value);
533 };
534 let output = Promise::new();
535 let destination = output.clone();
536 source.on_settle(Rc::new(move |state| match state {
537 PromiseState::Fulfilled(value) => match continuation(value) {
538 Ok(Value::Promise(value)) => {
539 destination.adopt(&value);
540 }
541 Ok(value) => {
542 destination.resolve(value);
543 }
544 Err(error) => {
545 destination.reject_rejection(error);
546 }
547 },
548 PromiseState::Rejected(error) => {
549 destination.reject_rejection(error);
550 }
551 PromiseState::Pending => {}
552 }));
553 Ok(Value::Promise(output))
554}
555
556fn settle(
557 value: Value,
558 fulfilled: Rc<dyn Fn(Value) -> Result<Value, PromiseRejection>>,
559 rejected: Rc<dyn Fn(PromiseRejection) -> Result<Value, PromiseRejection>>,
560) -> Result<Value, PromiseRejection> {
561 let Value::Promise(source) = value else {
562 return fulfilled(value);
563 };
564 let output = Promise::new();
565 let destination = output.clone();
566 source.on_settle(Rc::new(move |state| {
567 let next = match state {
568 PromiseState::Fulfilled(value) => fulfilled(value),
569 PromiseState::Rejected(error) => rejected(error),
570 PromiseState::Pending => return,
571 };
572 match next {
573 Ok(Value::Promise(value)) => {
574 destination.adopt(&value);
575 }
576 Ok(value) => {
577 destination.resolve(value);
578 }
579 Err(error) => {
580 destination.reject_rejection(error);
581 }
582 }
583 }));
584 Ok(Value::Promise(output))
585}
586
587fn execute(
588 runtime: WorkRuntime,
589 value: Value,
590 input: Value,
591 context: WorkContext,
592 bind_depth: usize,
593) -> Result<Value, PromiseRejection> {
594 validate_plan(&value).map_err(plan_error)?;
595 let operation = WorkOperation::parse(&field(&value, OP_KEY).expect("validated plan has op"))
596 .map_err(plan_error)?;
597 context.check_cancelled()?;
598 let _ = context.emit(
599 Value::Keyword("work/node-started".into()),
600 map([("operation", Value::Keyword(operation.as_str().into()))]),
601 );
602 let result = match operation {
603 WorkOperation::Pure | WorkOperation::Step => {
604 execute_target(&runtime, &value, input, context.clone())
605 }
606 WorkOperation::Chain => execute_chain(runtime, value, input, context.clone(), bind_depth),
607 WorkOperation::All => execute_all(runtime, value, input, context.clone(), bind_depth),
608 WorkOperation::Each => {
609 execute_each(runtime, value, input, context.clone(), bind_depth, false)
610 }
611 WorkOperation::Filter => {
612 execute_each(runtime, value, input, context.clone(), bind_depth, true)
613 }
614 WorkOperation::Fold => execute_fold(runtime, value, input, context.clone(), bind_depth),
615 WorkOperation::Choose => execute_choose(runtime, value, input, context.clone(), bind_depth),
616 WorkOperation::Bind => execute_bind(runtime, value, input, context.clone(), bind_depth),
617 WorkOperation::Ensure => execute_ensure(runtime, value, input, context.clone(), bind_depth),
618 WorkOperation::Await => execute_await(&runtime, value, context.clone()),
619 WorkOperation::Graph => execute_graph(runtime, value, input, context.clone(), bind_depth),
620 WorkOperation::Batch => execute_batch(runtime, value, input, context.clone(), bind_depth),
621 };
622 if result.is_ok() {
623 let _ = context.emit(
624 Value::Keyword("work/node-completed".into()),
625 map([("operation", Value::Keyword(operation.as_str().into()))]),
626 );
627 }
628 result
629}
630
631fn execute_target(
632 runtime: &WorkRuntime,
633 value: &Value,
634 input: Value,
635 context: WorkContext,
636) -> Result<Value, PromiseRejection> {
637 let target = target_name(field(value, TARGET_KEY).expect("validated leaf has target"))
638 .map_err(plan_error)?;
639 runtime
640 .registry
641 .target(&target)
642 .ok_or_else(|| plan_error(format!("work/target-unavailable: {target}")))?(input, context)
643}
644
645fn execute_chain(
646 runtime: WorkRuntime,
647 value: Value,
648 input: Value,
649 context: WorkContext,
650 depth: usize,
651) -> Result<Value, PromiseRejection> {
652 let children = vector(
653 field(&value, CHILDREN_KEY).expect("validated chain children"),
654 CHILDREN_KEY,
655 )
656 .map_err(plan_error)?;
657 fn next(
658 runtime: WorkRuntime,
659 children: Rc<Vec<Value>>,
660 index: usize,
661 input: Value,
662 context: WorkContext,
663 depth: usize,
664 ) -> Result<Value, PromiseRejection> {
665 let Some(child) = children.get(index).cloned() else {
666 return Ok(input);
667 };
668 let runtime_next = runtime.clone();
669 let children_next = children.clone();
670 let context_next = context.clone();
671 let value = execute(runtime, child, input, context, depth)?;
672 then(
673 value,
674 Rc::new(move |resolved| {
675 next(
676 runtime_next.clone(),
677 children_next.clone(),
678 index + 1,
679 resolved,
680 context_next.clone(),
681 depth,
682 )
683 }),
684 )
685 }
686 next(runtime, Rc::new(children), 0, input, context, depth)
687}
688
689fn execute_all(
690 runtime: WorkRuntime,
691 value: Value,
692 input: Value,
693 context: WorkContext,
694 depth: usize,
695) -> Result<Value, PromiseRejection> {
696 let children = vector(
697 field(&value, CHILDREN_KEY).expect("validated all children"),
698 CHILDREN_KEY,
699 )
700 .map_err(plan_error)?;
701 fn next(
702 runtime: WorkRuntime,
703 children: Rc<Vec<Value>>,
704 index: usize,
705 input: Value,
706 context: WorkContext,
707 depth: usize,
708 output: Vec<Value>,
709 ) -> Result<Value, PromiseRejection> {
710 let Some(child) = children.get(index).cloned() else {
711 return Ok(Value::Vector(output.into_iter().collect()));
712 };
713 let runtime_next = runtime.clone();
714 let children_next = children.clone();
715 let context_next = context.clone();
716 let value = execute(runtime, child, input.clone(), context, depth)?;
717 then(
718 value,
719 Rc::new(move |resolved| {
720 let mut next_output = output.clone();
721 next_output.push(resolved);
722 next(
723 runtime_next.clone(),
724 children_next.clone(),
725 index + 1,
726 input.clone(),
727 context_next.clone(),
728 depth,
729 next_output,
730 )
731 }),
732 )
733 }
734 next(
735 runtime,
736 Rc::new(children),
737 0,
738 input,
739 context,
740 depth,
741 Vec::new(),
742 )
743}
744
745fn sequence_input(value: Value) -> Result<Vec<Value>, PromiseRejection> {
746 vector(value, "work input").map_err(plan_error)
747}
748
749fn execute_each(
750 runtime: WorkRuntime,
751 value: Value,
752 input: Value,
753 context: WorkContext,
754 depth: usize,
755 filtering: bool,
756) -> Result<Value, PromiseRejection> {
757 let child = field(&value, CHILD_KEY).expect("validated child");
758 let values = sequence_input(input)?;
759 fn next(
760 runtime: WorkRuntime,
761 child: Value,
762 values: Rc<Vec<Value>>,
763 index: usize,
764 context: WorkContext,
765 depth: usize,
766 filtering: bool,
767 output: Vec<Value>,
768 ) -> Result<Value, PromiseRejection> {
769 let Some(item) = values.get(index).cloned() else {
770 return Ok(Value::Vector(output.into_iter().collect()));
771 };
772 let runtime_next = runtime.clone();
773 let child_next = child.clone();
774 let values_next = values.clone();
775 let context_next = context.clone();
776 let value = execute(runtime, child, item.clone(), context, depth)?;
777 then(
778 value,
779 Rc::new(move |resolved| {
780 let mut next_output = output.clone();
781 if !filtering || truthy(&resolved) {
782 next_output.push(if filtering { item.clone() } else { resolved });
783 }
784 next(
785 runtime_next.clone(),
786 child_next.clone(),
787 values_next.clone(),
788 index + 1,
789 context_next.clone(),
790 depth,
791 filtering,
792 next_output,
793 )
794 }),
795 )
796 }
797 next(
798 runtime,
799 child,
800 Rc::new(values),
801 0,
802 context,
803 depth,
804 filtering,
805 Vec::new(),
806 )
807}
808
809fn execute_fold(
810 runtime: WorkRuntime,
811 value: Value,
812 input: Value,
813 context: WorkContext,
814 depth: usize,
815) -> Result<Value, PromiseRejection> {
816 let reducer = field(&value, REDUCER_KEY).expect("validated reducer");
817 let initial = field(&value, INITIAL_KEY).unwrap_or(Value::Nil);
818 let values = sequence_input(input)?;
819 fn next(
820 runtime: WorkRuntime,
821 reducer: Value,
822 values: Rc<Vec<Value>>,
823 index: usize,
824 accumulator: Value,
825 context: WorkContext,
826 depth: usize,
827 ) -> Result<Value, PromiseRejection> {
828 let Some(item) = values.get(index).cloned() else {
829 return Ok(accumulator);
830 };
831 let runtime_next = runtime.clone();
832 let reducer_next = reducer.clone();
833 let values_next = values.clone();
834 let context_next = context.clone();
835 let request = map([("acc", accumulator), ("item", item)]);
836 let value = execute(runtime, reducer, request, context, depth)?;
837 then(
838 value,
839 Rc::new(move |resolved| {
840 next(
841 runtime_next.clone(),
842 reducer_next.clone(),
843 values_next.clone(),
844 index + 1,
845 resolved,
846 context_next.clone(),
847 depth,
848 )
849 }),
850 )
851 }
852 next(
853 runtime,
854 reducer,
855 Rc::new(values),
856 0,
857 initial,
858 context,
859 depth,
860 )
861}
862
863fn execute_choose(
864 runtime: WorkRuntime,
865 value: Value,
866 input: Value,
867 context: WorkContext,
868 depth: usize,
869) -> Result<Value, PromiseRejection> {
870 let selector = field(&value, SELECTOR_KEY).expect("validated selector");
871 let choices = field(&value, CHOICES_KEY).expect("validated choices");
872 let runtime_next = runtime.clone();
873 let context_next = context.clone();
874 let selected = execute(runtime, selector, input.clone(), context, depth)?;
875 then(
876 selected,
877 Rc::new(move |selected| {
878 let child =
879 map_lookup(&choices, &selected).ok_or_else(|| plan_error("work/choice-missing"))?;
880 execute(
881 runtime_next.clone(),
882 child,
883 input.clone(),
884 context_next.clone(),
885 depth,
886 )
887 }),
888 )
889}
890
891fn execute_graph(
892 runtime: WorkRuntime,
893 value: Value,
894 input: Value,
895 context: WorkContext,
896 depth: usize,
897) -> Result<Value, PromiseRejection> {
898 let nodes = field(&value, NODES_KEY).expect("validated graph nodes");
899 let order = vector(
900 field(&value, ORDER_KEY).expect("validated graph order"),
901 ORDER_KEY,
902 )
903 .map_err(plan_error)?;
904 fn next(
905 runtime: WorkRuntime,
906 nodes: Value,
907 order: Rc<Vec<Value>>,
908 index: usize,
909 input: Value,
910 context: WorkContext,
911 depth: usize,
912 output: Vec<(Value, Value)>,
913 ) -> Result<Value, PromiseRejection> {
914 let Some(id) = order.get(index).cloned() else {
915 return Ok(Value::Map(output.into_iter().collect()));
916 };
917 let child = map_lookup(&nodes, &id).expect("validated graph node");
918 let runtime_next = runtime.clone();
919 let nodes_next = nodes.clone();
920 let order_next = order.clone();
921 let input_next = input.clone();
922 let context_next = context.clone();
923 let value = execute(runtime, child, input, context, depth)?;
924 then(
925 value,
926 Rc::new(move |resolved| {
927 let mut next_output = output.clone();
928 next_output.push((id.clone(), resolved));
929 next(
930 runtime_next.clone(),
931 nodes_next.clone(),
932 order_next.clone(),
933 index + 1,
934 input_next.clone(),
935 context_next.clone(),
936 depth,
937 next_output,
938 )
939 }),
940 )
941 }
942 next(
943 runtime,
944 nodes,
945 Rc::new(order),
946 0,
947 input,
948 context,
949 depth,
950 Vec::new(),
951 )
952}
953
954fn execute_batch(
955 runtime: WorkRuntime,
956 value: Value,
957 input: Value,
958 context: WorkContext,
959 depth: usize,
960) -> Result<Value, PromiseRejection> {
961 let each = map([
962 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
963 (OP_KEY, Value::Keyword("each".into())),
964 (
965 CHILD_KEY,
966 field(&value, PROCESS_KEY).expect("validated batch process"),
967 ),
968 ]);
969 execute_each(runtime, each, input, context, depth, false)
970}
971
972fn execute_bind(
973 runtime: WorkRuntime,
974 value: Value,
975 input: Value,
976 context: WorkContext,
977 depth: usize,
978) -> Result<Value, PromiseRejection> {
979 let maximum = field(&value, MAXIMUM_DEPTH_KEY)
980 .and_then(|value| match value {
981 Value::Number(value) if value > 0 => Some(value as usize),
982 _ => None,
983 })
984 .unwrap_or(64);
985 if depth >= maximum {
986 return Err(plan_error("work/bind-depth-exceeded"));
987 }
988 let source = field(&value, SOURCE_KEY).expect("validated source");
989 let target = target_name(field(&value, CONTINUATION_KEY).expect("validated continuation"))
990 .map_err(plan_error)?;
991 let runtime_next = runtime.clone();
992 let context_next = context.clone();
993 let source_value = execute(runtime, source, input, context, depth)?;
994 then(
995 source_value,
996 Rc::new(move |resolved| {
997 let produced = runtime_next
998 .registry
999 .target(&target)
1000 .ok_or_else(|| plan_error(format!("work/target-unavailable: {target}")))?(
1001 resolved.clone(),
1002 context_next.clone(),
1003 )?;
1004 let plan = WorkPlan::from_value(produced)
1005 .map_err(|_| plan_error("work/bind-target-returned-non-plan"))?;
1006 execute(
1007 runtime_next.clone(),
1008 plan.value(),
1009 resolved,
1010 context_next.clone(),
1011 depth + 1,
1012 )
1013 }),
1014 )
1015}
1016
1017fn finish_ensure(
1018 runtime: WorkRuntime,
1019 cleanup: Value,
1020 input: Value,
1021 context: WorkContext,
1022 depth: usize,
1023 body_status: &'static str,
1024 body_result: Value,
1025 body_error: Option<PromiseRejection>,
1026) -> Result<Value, PromiseRejection> {
1027 let cleanup_value = map([
1028 ("work/body-status", Value::Keyword(body_status.into())),
1029 ("work/body-result", body_result.clone()),
1030 ("work/input", input),
1031 ]);
1032 let cleanup_result = execute(runtime, cleanup, cleanup_value, context, depth)?;
1033 then(
1034 cleanup_result,
1035 Rc::new(move |_| match &body_error {
1036 Some(error) => Err(error.clone()),
1037 None => Ok(body_result.clone()),
1038 }),
1039 )
1040}
1041
1042fn execute_ensure(
1043 runtime: WorkRuntime,
1044 value: Value,
1045 input: Value,
1046 context: WorkContext,
1047 depth: usize,
1048) -> Result<Value, PromiseRejection> {
1049 let body = field(&value, CHILD_KEY).expect("validated ensure body");
1050 let cleanup = field(&value, CLEANUP_KEY).expect("validated cleanup");
1051 let result = execute(runtime.clone(), body, input.clone(), context.clone(), depth);
1052 match result {
1053 Ok(result) => {
1054 let completed_runtime = runtime.clone();
1055 let completed_cleanup = cleanup.clone();
1056 let completed_input = input.clone();
1057 let completed_context = context.clone();
1058 settle(
1059 result,
1060 Rc::new(move |body_result| {
1061 finish_ensure(
1062 completed_runtime.clone(),
1063 completed_cleanup.clone(),
1064 completed_input.clone(),
1065 completed_context.clone(),
1066 depth,
1067 "completed",
1068 body_result,
1069 None,
1070 )
1071 }),
1072 Rc::new(move |body_error| {
1073 finish_ensure(
1074 runtime.clone(),
1075 cleanup.clone(),
1076 input.clone(),
1077 context.clone(),
1078 depth,
1079 "failed",
1080 Value::Nil,
1081 Some(body_error),
1082 )
1083 }),
1084 )
1085 }
1086 Err(body_error) => finish_ensure(
1087 runtime,
1088 cleanup,
1089 input,
1090 context,
1091 depth,
1092 "failed",
1093 Value::Nil,
1094 Some(body_error),
1095 ),
1096 }
1097}
1098
1099fn execute_await(
1100 runtime: &WorkRuntime,
1101 value: Value,
1102 context: WorkContext,
1103) -> Result<Value, PromiseRejection> {
1104 let wait = field(&value, WAIT_KEY).expect("validated await");
1105 runtime
1106 .suspension
1107 .as_ref()
1108 .ok_or_else(|| plan_error("work/suspension-unavailable"))?(wait, context)
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114 use std::cell::Cell;
1115
1116 fn registry() -> WorkRegistry {
1117 let registry = WorkRegistry::default();
1118 registry
1119 .bind(
1120 "fixture/inc",
1121 Rc::new(|value, _| match value {
1122 Value::Number(value) => Ok(Value::Number(value + 1)),
1123 _ => Err(plan_error("fixture expects number")),
1124 }),
1125 )
1126 .unwrap();
1127 registry
1128 .bind(
1129 "fixture/double",
1130 Rc::new(|value, _| match value {
1131 Value::Number(value) => Ok(Value::Number(value * 2)),
1132 _ => Err(plan_error("fixture expects number")),
1133 }),
1134 )
1135 .unwrap();
1136 registry
1137 }
1138
1139 #[test]
1140 fn hta_round_trip_is_canonical_for_closure_free_plans() {
1141 let plan = WorkPlan::chain(vec![
1142 WorkPlan::pure("fixture/inc").unwrap(),
1143 WorkPlan::step("fixture/double").unwrap(),
1144 ])
1145 .unwrap();
1146 let bytes = plan.encode_hta().unwrap();
1147 assert_eq!(
1148 bytes,
1149 WorkPlan::decode_hta(&bytes).unwrap().encode_hta().unwrap()
1150 );
1151 }
1152
1153 #[test]
1154 fn named_targets_execute_through_the_existing_host_lifecycle() {
1155 let host = WorkHost::new();
1156 let runtime = WorkRuntime::new(registry());
1157 let plan = WorkPlan::chain(vec![
1158 WorkPlan::pure("fixture/inc").unwrap(),
1159 WorkPlan::step("fixture/double").unwrap(),
1160 ])
1161 .unwrap();
1162 let run = host
1163 .submit_plan(
1164 runtime,
1165 plan,
1166 Value::Number(4),
1167 WorkOptions::with_id("plan-run").unwrap(),
1168 )
1169 .unwrap();
1170 assert_eq!(
1171 run.work_result().wait_state(),
1172 PromiseState::Fulfilled(Value::Number(10))
1173 );
1174 assert_eq!(
1175 run.work_status().state,
1176 super::super::WorkRunState::Completed
1177 );
1178 }
1179
1180 #[test]
1181 fn missing_targets_fail_closed_and_reset_is_idempotent() {
1182 let host = WorkHost::new();
1183 let runtime = WorkRuntime::default();
1184 let run = host
1185 .submit_plan(
1186 runtime,
1187 WorkPlan::pure("missing").unwrap(),
1188 Value::Nil,
1189 WorkOptions::default(),
1190 )
1191 .unwrap();
1192 assert!(matches!(
1193 run.work_result().wait_state(),
1194 PromiseState::Rejected(_)
1195 ));
1196 host.reset();
1197 host.reset();
1198 assert_eq!(host.status().run_count, 0);
1199 assert!(host.started());
1200 }
1201
1202 #[test]
1203 fn keyword_targets_use_their_stable_names() {
1204 let plan = WorkPlan::from_value(map([
1205 (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
1206 (OP_KEY, Value::Keyword("pure".into())),
1207 (TARGET_KEY, Value::Keyword("fixture/inc".into())),
1208 ]))
1209 .unwrap();
1210 let host = WorkHost::new();
1211 let run = host
1212 .submit_plan(
1213 WorkRuntime::new(registry()),
1214 plan,
1215 Value::Number(4),
1216 WorkOptions::default(),
1217 )
1218 .unwrap();
1219 assert_eq!(
1220 run.work_result().wait_state(),
1221 PromiseState::Fulfilled(Value::Number(5))
1222 );
1223 }
1224
1225 #[test]
1226 fn ensure_runs_cleanup_for_completed_and_failed_bodies() {
1227 let registry = registry();
1228 let cleanup_count = Rc::new(Cell::new(0));
1229 let cleanup_count_next = cleanup_count.clone();
1230 registry
1231 .bind(
1232 "fixture/cleanup",
1233 Rc::new(move |_, _| {
1234 cleanup_count_next.set(cleanup_count_next.get() + 1);
1235 Ok(Value::Nil)
1236 }),
1237 )
1238 .unwrap();
1239 registry
1240 .bind(
1241 "fixture/fail",
1242 Rc::new(|_, _| Err(plan_error("fixture fails"))),
1243 )
1244 .unwrap();
1245
1246 let host = WorkHost::new();
1247 let runtime = WorkRuntime::new(registry);
1248 let cleanup = WorkPlan::step("fixture/cleanup").unwrap();
1249 let completed = host
1250 .submit_plan(
1251 runtime.clone(),
1252 WorkPlan::ensure(WorkPlan::pure("fixture/inc").unwrap(), cleanup.clone()).unwrap(),
1253 Value::Number(4),
1254 WorkOptions::with_id("ensure-completed").unwrap(),
1255 )
1256 .unwrap();
1257 assert_eq!(
1258 completed.work_result().wait_state(),
1259 PromiseState::Fulfilled(Value::Number(5))
1260 );
1261
1262 let failed = host
1263 .submit_plan(
1264 runtime,
1265 WorkPlan::ensure(WorkPlan::pure("fixture/fail").unwrap(), cleanup).unwrap(),
1266 Value::Number(4),
1267 WorkOptions::with_id("ensure-failed").unwrap(),
1268 )
1269 .unwrap();
1270 assert!(matches!(
1271 failed.work_result().wait_state(),
1272 PromiseState::Rejected(_)
1273 ));
1274 assert_eq!(cleanup_count.get(), 2);
1275 }
1276
1277 #[test]
1278 fn graph_and_batch_execute_their_data_owned_children() {
1279 let runtime = WorkRuntime::new(registry());
1280 let host = WorkHost::new();
1281 let graph = WorkPlan::graph(map([
1282 (
1283 "work/nodes",
1284 Value::Map(
1285 [(
1286 Value::Keyword("increment".into()),
1287 WorkPlan::pure("fixture/inc").unwrap().value(),
1288 )]
1289 .into_iter()
1290 .collect(),
1291 ),
1292 ),
1293 (
1294 "work/order",
1295 Value::Vector([Value::Keyword("increment".into())].into_iter().collect()),
1296 ),
1297 ]))
1298 .unwrap();
1299 let graph_result = host
1300 .submit_plan(
1301 runtime.clone(),
1302 graph,
1303 Value::Number(4),
1304 WorkOptions::with_id("plan-graph").unwrap(),
1305 )
1306 .unwrap()
1307 .work_result()
1308 .wait_state();
1309 let PromiseState::Fulfilled(graph_result) = graph_result else {
1310 panic!("graph plan should succeed");
1311 };
1312 assert_eq!(
1313 map_lookup(&graph_result, &Value::Keyword("increment".into())),
1314 Some(Value::Number(5))
1315 );
1316
1317 let batch = WorkPlan::batch(map([(
1318 "work/process",
1319 WorkPlan::step("fixture/double").unwrap().value(),
1320 )]))
1321 .unwrap();
1322 assert_eq!(
1323 host.submit_plan(
1324 runtime,
1325 batch,
1326 Value::Vector([Value::Number(2), Value::Number(3)].into_iter().collect()),
1327 WorkOptions::with_id("plan-batch").unwrap()
1328 )
1329 .unwrap()
1330 .work_result()
1331 .wait_state(),
1332 PromiseState::Fulfilled(Value::Vector(
1333 [Value::Number(4), Value::Number(6)].into_iter().collect()
1334 ))
1335 );
1336
1337 let invalid = WorkPlan::graph(map([
1338 (
1339 "work/nodes",
1340 Value::Map(
1341 [(Value::Keyword("unused".into()), Value::Number(1))]
1342 .into_iter()
1343 .collect(),
1344 ),
1345 ),
1346 (
1347 "work/order",
1348 Value::Vector(Vec::new().into_iter().collect()),
1349 ),
1350 ]))
1351 .unwrap_err();
1352 assert!(invalid.contains("plan must be a map"));
1353 }
1354}