Skip to main content

hara_native/
work.rs

1use crate::core::{Promise, PromiseRejection, PromiseState, Value};
2use crate::lang::protocol::IComponent;
3use std::cell::{Cell, RefCell};
4use std::collections::{HashMap, VecDeque};
5use std::fmt;
6use std::rc::{Rc, Weak};
7use std::time::Duration;
8
9pub(crate) mod guest;
10pub mod plan;
11mod scope;
12mod types;
13
14pub use scope::{
15    current_work_context, monotonic_nanos, process_work_host, WorkCancellationToken, WorkContext,
16};
17pub use types::{WorkDeadline, WorkHostStatus, WorkId, WorkOptions, WorkRunState, WorkRunStatus};
18
19use scope::{
20    cancellation_rejection, deadline_remaining_millis, install_progress_hooks, next_work_id,
21    now_millis, resolve_deadline, work_failure,
22};
23
24pub(crate) use scope::with_current_work_context;
25
26type WorkTask = Box<dyn FnOnce(WorkContext) -> Result<Value, PromiseRejection>>;
27type WorkFinalizer = Box<dyn FnOnce(WorkContext) -> Result<(), PromiseRejection>>;
28
29struct PendingWork {
30    id: WorkId,
31    task: WorkTask,
32}
33
34struct WorkHostInner {
35    started: bool,
36    next_id: u64,
37    runs: HashMap<WorkId, WorkRun>,
38    queue: VecDeque<PendingWork>,
39}
40
41/// Cloneable process-owned host for live work handles.
42///
43/// Rust Hara values are currently evaluator-thread values (`Rc`, not `Send`).
44/// The host therefore schedules work cooperatively on that evaluator thread.
45/// Submission only enqueues; polling or waiting on the result Promise, or an
46/// explicit [`WorkHost::run`], advances the run.
47#[derive(Clone)]
48pub struct WorkHost {
49    inner: Rc<RefCell<WorkHostInner>>,
50}
51
52impl fmt::Debug for WorkHost {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        formatter
55            .debug_struct("WorkHost")
56            .field("status", &self.status())
57            .finish()
58    }
59}
60
61impl Default for WorkHost {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl WorkHost {
68    pub fn new() -> Self {
69        Self {
70            inner: Rc::new(RefCell::new(WorkHostInner {
71                started: true,
72                next_id: 1,
73                runs: HashMap::new(),
74                queue: VecDeque::new(),
75            })),
76        }
77    }
78
79    /// Submit work without executing it inline.
80    pub fn submit<F>(&self, id: Option<&str>, task: F) -> Result<WorkRun, String>
81    where
82        F: FnOnce() -> Result<Value, String> + 'static,
83    {
84        let options = WorkOptions {
85            id: id.map(WorkId::new).transpose()?,
86            ..WorkOptions::default()
87        };
88        self.submit_scoped(options, move |_| task())
89    }
90
91    /// Submit work whose executor already returns a native structured rejection.
92    pub fn submit_rejection<F>(&self, id: Option<&str>, task: F) -> Result<WorkRun, String>
93    where
94        F: FnOnce() -> Result<Value, PromiseRejection> + 'static,
95    {
96        let options = WorkOptions {
97            id: id.map(WorkId::new).transpose()?,
98            ..WorkOptions::default()
99        };
100        self.submit_scoped_rejection(options, move |_| task())
101    }
102
103    pub fn submit_scoped<F>(&self, options: WorkOptions, task: F) -> Result<WorkRun, String>
104    where
105        F: FnOnce(WorkContext) -> Result<Value, String> + 'static,
106    {
107        self.submit_scoped_rejection(options, move |context| task(context).map_err(work_failure))
108    }
109
110    pub fn submit_scoped_rejection<F>(
111        &self,
112        options: WorkOptions,
113        task: F,
114    ) -> Result<WorkRun, String>
115    where
116        F: FnOnce(WorkContext) -> Result<Value, PromiseRejection> + 'static,
117    {
118        let parent = if options.detached {
119            None
120        } else {
121            current_work_context()
122                .filter(|context| context.host.same_identity(self))
123                .map(|context| context.run)
124        };
125        self.submit_with_parent(parent, options, Box::new(task))
126    }
127
128    fn submit_with_parent(
129        &self,
130        parent: Option<WorkRun>,
131        options: WorkOptions,
132        task: WorkTask,
133    ) -> Result<WorkRun, String> {
134        let mut host = self.inner.borrow_mut();
135        if !host.started {
136            return Err("native work host is stopped".into());
137        }
138        let deadline = resolve_deadline(&options, parent.as_ref());
139        let id = match options.id.clone() {
140            Some(id) => id,
141            None => next_work_id(&mut host)?,
142        };
143        if host.runs.contains_key(&id) {
144            return Err(format!("work run ID already exists: {id}"));
145        }
146        if parent
147            .as_ref()
148            .is_some_and(|parent| !parent.accepts_children())
149        {
150            return Err("parent work scope is closed".into());
151        }
152
153        let result = Promise::new();
154        let run = WorkRun {
155            inner: Rc::new(WorkRunInner {
156                id: id.clone(),
157                result,
158                status: RefCell::new(WorkRunStatus {
159                    id: id.clone(),
160                    state: WorkRunState::Queued,
161                    started_at_millis: now_millis(),
162                    finished_at_millis: None,
163                    error: None,
164                    cancel_reason: None,
165                    parent_id: parent.as_ref().map(WorkRun::work_id),
166                    child_count: 0,
167                    deadline_remaining_millis: deadline.map(deadline_remaining_millis),
168                    detached: options.detached,
169                }),
170                host: Rc::downgrade(&self.inner),
171                parent: parent.as_ref().map(|parent| Rc::downgrade(&parent.inner)),
172                children: RefCell::new(HashMap::new()),
173                deadline,
174                cancellation: RefCell::new(None),
175                body_done: Cell::new(false),
176                body_outcome: RefCell::new(None),
177                finalizers: RefCell::new(Vec::new()),
178                finalizers_started: Cell::new(false),
179                active_promise: RefCell::new(None),
180                parent_notified: Cell::new(false),
181                events: Rc::new(RefCell::new(WorkEventLog {
182                    values: vec![work_event(
183                        &id,
184                        1,
185                        "work/run-queued",
186                        WorkRunState::Queued,
187                        None,
188                    )],
189                    ..WorkEventLog::default()
190                })),
191            }),
192        };
193        if let Some(parent) = &parent {
194            if !parent.attach_child(run.clone()) {
195                return Err("parent work scope is closed".into());
196            }
197        }
198        install_progress_hooks(self, &run);
199        host.runs.insert(id.clone(), run.clone());
200        host.queue.push_back(PendingWork { id, task });
201        drop(host);
202        run.check_deadline();
203        Ok(run)
204    }
205
206    /// Resolve a live handle from a portable raw identifier.
207    pub fn resolve(&self, reference: &str) -> Result<WorkRun, String> {
208        let id = WorkId::new(reference)?;
209        self.resolve_id(&id)
210    }
211
212    pub fn resolve_id(&self, id: &WorkId) -> Result<WorkRun, String> {
213        let run = self
214            .inner
215            .borrow()
216            .runs
217            .get(id)
218            .cloned()
219            .ok_or_else(|| format!("unknown work run: {id}"))?;
220        run.check_deadline();
221        Ok(run)
222    }
223
224    /// Run one queued item by ID. Returns false when no runnable item remains.
225    pub fn run(&self, id: &WorkId) -> bool {
226        let (run, task) = {
227            let mut host = self.inner.borrow_mut();
228            let Some(index) = host.queue.iter().position(|pending| &pending.id == id) else {
229                return false;
230            };
231            let pending = host.queue.remove(index).expect("queued work disappeared");
232            let run = host
233                .runs
234                .get(id)
235                .cloned()
236                .expect("queued work has no live run");
237            (run, pending.task)
238        };
239        run.check_deadline();
240        if !run.mark_running() {
241            return false;
242        }
243        let context = WorkContext {
244            host: self.clone(),
245            run: run.clone(),
246        };
247        let result = with_current_work_context(context.clone(), || task(context));
248        match result {
249            Ok(value) => run.settle_body(value),
250            Err(error) => run.fail_body(error),
251        }
252        true
253    }
254
255    pub fn run_next(&self) -> bool {
256        let id = self
257            .inner
258            .borrow()
259            .queue
260            .front()
261            .map(|pending| pending.id.clone());
262        id.is_some_and(|id| self.run(&id))
263    }
264
265    pub fn drain(&self) {
266        while self.run_next() {}
267    }
268
269    fn progress(&self, id: &WorkId) {
270        let _ = self.run(id);
271        let run = self.resolve_id(id).ok();
272        if let Some(run) = &run {
273            run.progress_active_promise();
274        }
275        while run.as_ref().is_some_and(|run| !run.closed()) && self.run_next() {}
276        if let Some(run) = run {
277            run.progress_active_promise();
278            run.check_deadline();
279        }
280    }
281
282    fn wait_for(&self, id: &WorkId) {
283        self.progress(id);
284        if let Ok(run) = self.resolve_id(id) {
285            let active = run.inner.active_promise.borrow().clone();
286            if let Some(active) = active {
287                active.wait_state();
288            }
289            self.progress(id);
290        }
291    }
292
293    pub fn status(&self) -> WorkHostStatus {
294        let host = self.inner.borrow();
295        WorkHostStatus {
296            state: if host.started { "started" } else { "stopped" },
297            run_count: host.runs.len(),
298            queued_count: host.queue.len(),
299        }
300    }
301
302    pub fn started(&self) -> bool {
303        self.inner.borrow().started
304    }
305
306    pub fn start(&self) {
307        self.inner.borrow_mut().started = true;
308    }
309
310    pub fn stop(&self) {
311        self.inner.borrow_mut().started = false;
312    }
313
314    pub fn kill(&self) {
315        let runs = {
316            let mut host = self.inner.borrow_mut();
317            host.started = false;
318            host.runs.values().cloned().collect::<Vec<_>>()
319        };
320        for run in runs {
321            run.cancel(Value::Keyword("host-stopped".into()));
322        }
323    }
324
325    pub fn same_identity(&self, other: &Self) -> bool {
326        Rc::ptr_eq(&self.inner, &other.inner)
327    }
328}
329
330impl IComponent for WorkHost {
331    type Metadata = WorkHostStatus;
332
333    fn props(&self) -> Self::Metadata {
334        self.status()
335    }
336
337    fn status(&self) -> Self::Metadata {
338        WorkHost::status(self)
339    }
340
341    fn started(&self) -> bool {
342        WorkHost::started(self)
343    }
344
345    fn stopped(&self) -> bool {
346        !WorkHost::started(self)
347    }
348
349    fn start(&mut self) {
350        WorkHost::start(self);
351    }
352
353    fn stop(&mut self) {
354        WorkHost::stop(self);
355    }
356
357    fn kill(&mut self) {
358        WorkHost::kill(self);
359    }
360}
361
362#[derive(Clone)]
363struct CancellationRequest {
364    reason: Value,
365    rejection: PromiseRejection,
366}
367
368struct WorkRunInner {
369    id: WorkId,
370    result: Promise,
371    status: RefCell<WorkRunStatus>,
372    host: Weak<RefCell<WorkHostInner>>,
373    parent: Option<Weak<WorkRunInner>>,
374    children: RefCell<HashMap<WorkId, WorkRun>>,
375    deadline: Option<WorkDeadline>,
376    cancellation: RefCell<Option<CancellationRequest>>,
377    body_done: Cell<bool>,
378    body_outcome: RefCell<Option<Result<Value, PromiseRejection>>>,
379    finalizers: RefCell<Vec<WorkFinalizer>>,
380    finalizers_started: Cell<bool>,
381    active_promise: RefCell<Option<Promise>>,
382    parent_notified: Cell<bool>,
383    events: Rc<RefCell<WorkEventLog>>,
384}
385
386#[derive(Default)]
387struct WorkEventLog {
388    values: Vec<Value>,
389    closed: bool,
390    cursors: Vec<Weak<RefCell<WorkEventCursor>>>,
391}
392
393struct WorkEventCursor {
394    next: usize,
395    pending: Option<Promise>,
396    closed: bool,
397}
398
399/// Live process-owned handle returned immediately from submission.
400#[derive(Clone)]
401pub struct WorkRun {
402    inner: Rc<WorkRunInner>,
403}
404
405impl fmt::Debug for WorkRun {
406    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
407        formatter
408            .debug_struct("WorkRun")
409            .field("status", &self.work_status())
410            .finish()
411    }
412}
413
414impl WorkRun {
415    pub fn work_id(&self) -> WorkId {
416        self.inner.id.clone()
417    }
418
419    pub fn work_status(&self) -> WorkRunStatus {
420        self.check_deadline();
421        let mut status = self.inner.status.borrow().clone();
422        status.child_count = self.inner.children.borrow().len();
423        status.deadline_remaining_millis = self.inner.deadline.map(deadline_remaining_millis);
424        status
425    }
426
427    pub fn deadline(&self) -> Option<WorkDeadline> {
428        self.inner.deadline
429    }
430
431    pub fn cancellation_token(&self) -> WorkCancellationToken {
432        WorkCancellationToken {
433            run: Rc::downgrade(&self.inner),
434        }
435    }
436
437    /// Return the same native result Promise on every call.
438    pub fn work_result(&self) -> Promise {
439        self.inner.result.clone()
440    }
441
442    pub fn work_cancel(&self, reason: Value) -> Promise {
443        let result = Promise::new();
444        result.resolve(Value::Bool(self.cancel(reason)));
445        result
446    }
447
448    pub fn work_events(&self, after: usize) -> Value {
449        let cursor = Rc::new(RefCell::new(WorkEventCursor {
450            next: after,
451            pending: None,
452            closed: false,
453        }));
454        self.inner
455            .events
456            .borrow_mut()
457            .cursors
458            .push(Rc::downgrade(&cursor));
459        let events = self.inner.events.clone();
460        let next_cursor = cursor.clone();
461        let next = Rc::new(move || event_next(&events, &next_cursor));
462        let events = self.inner.events.clone();
463        let close_cursor = cursor;
464        let close = Rc::new(move || {
465            let pending = {
466                let mut cursor = close_cursor.borrow_mut();
467                cursor.closed = true;
468                cursor.pending.take()
469            };
470            if let Some(pending) = pending {
471                pending.resolve(Value::Nil);
472            }
473            events.borrow_mut().cursors.retain(|candidate| {
474                candidate
475                    .upgrade()
476                    .is_some_and(|candidate| !Rc::ptr_eq(&candidate, &close_cursor))
477            });
478            Ok(())
479        });
480        crate::core::host_stream(next, close)
481    }
482
483    /// Append one ordered domain event to this live run.
484    pub fn emit(&self, kind: Value, data: Value) -> bool {
485        if self.closed() {
486            return false;
487        }
488        let kind = match kind {
489            Value::Keyword(value) => value.to_string(),
490            Value::Symbol(value) => value.to_string(),
491            Value::String(value) => value,
492            _ => return false,
493        };
494        self.publish_domain_event(&kind, data);
495        true
496    }
497
498    pub fn cancel(&self, reason: Value) -> bool {
499        let rejection = cancellation_rejection(reason.clone());
500        {
501            let mut cancellation = self.inner.cancellation.borrow_mut();
502            if cancellation.is_some() || self.closed() {
503                return false;
504            }
505            *cancellation = Some(CancellationRequest {
506                reason: reason.clone(),
507                rejection: rejection.clone(),
508            });
509        }
510
511        let previous = {
512            let mut status = self.inner.status.borrow_mut();
513            let previous = status.state;
514            if !previous.terminal() {
515                status.state = WorkRunState::Cancelling;
516                status.cancel_reason = Some(reason.clone());
517            }
518            previous
519        };
520        self.publish_event(
521            "work/run-cancelling",
522            WorkRunState::Cancelling,
523            Some(reason.clone()),
524            false,
525        );
526        if previous == WorkRunState::Queued {
527            if let Some(host) = self.inner.host.upgrade() {
528                host.borrow_mut()
529                    .queue
530                    .retain(|pending| pending.id != self.inner.id);
531            }
532            self.inner.body_done.set(true);
533        }
534        let active = self.inner.active_promise.borrow().clone();
535        if let Some(active) = active {
536            active.cancel();
537        }
538        let children = self
539            .inner
540            .children
541            .borrow()
542            .values()
543            .cloned()
544            .collect::<Vec<_>>();
545        for child in children {
546            child.cancel(reason.clone());
547        }
548        self.finish_if_ready();
549        true
550    }
551
552    pub fn closed(&self) -> bool {
553        self.inner.status.borrow().state.terminal()
554    }
555
556    pub fn same_identity(&self, other: &Self) -> bool {
557        Rc::ptr_eq(&self.inner, &other.inner)
558    }
559
560    fn accepts_children(&self) -> bool {
561        !self.inner.body_done.get()
562            && !self.inner.finalizers_started.get()
563            && self.inner.cancellation.borrow().is_none()
564            && !self.closed()
565    }
566
567    fn attach_child(&self, child: WorkRun) -> bool {
568        if !self.accepts_children() {
569            return false;
570        }
571        self.inner
572            .children
573            .borrow_mut()
574            .insert(child.work_id(), child);
575        true
576    }
577
578    fn child_closed(&self, child: &WorkRun) {
579        self.inner.children.borrow_mut().remove(&child.work_id());
580        self.finish_if_ready();
581    }
582
583    fn mark_running(&self) -> bool {
584        self.check_deadline();
585        let mut status = self.inner.status.borrow_mut();
586        if status.state != WorkRunState::Queued {
587            return false;
588        }
589        status.state = WorkRunState::Running;
590        drop(status);
591        self.publish_event("work/run-running", WorkRunState::Running, None, false);
592        true
593    }
594
595    fn settle_body(&self, value: Value) {
596        if let Value::Promise(source) = value {
597            if source.same_identity(&self.inner.result) {
598                self.fail_body(work_failure("work result promise adoption cycle".into()));
599                return;
600            }
601            *self.inner.active_promise.borrow_mut() = Some(source.clone());
602            if self.inner.cancellation.borrow().is_some() {
603                source.cancel();
604            }
605            let run = Rc::downgrade(&self.inner);
606            source.on_settle(Rc::new(move |state| {
607                let Some(inner) = run.upgrade() else {
608                    return;
609                };
610                let run = WorkRun { inner };
611                run.inner.active_promise.borrow_mut().take();
612                match state {
613                    PromiseState::Pending => return,
614                    PromiseState::Fulfilled(value) => {
615                        *run.inner.body_outcome.borrow_mut() = Some(Ok(value));
616                    }
617                    PromiseState::Rejected(error) => {
618                        *run.inner.body_outcome.borrow_mut() = Some(Err(error));
619                    }
620                }
621                run.inner.body_done.set(true);
622                run.finish_if_ready();
623            }));
624            self.set_nonterminal_state(if self.inner.cancellation.borrow().is_some() {
625                WorkRunState::Cancelling
626            } else {
627                WorkRunState::Waiting
628            });
629            source.state();
630            return;
631        }
632        *self.inner.body_outcome.borrow_mut() = Some(Ok(value));
633        self.inner.body_done.set(true);
634        self.finish_if_ready();
635    }
636
637    fn fail_body(&self, error: PromiseRejection) {
638        *self.inner.body_outcome.borrow_mut() = Some(Err(error));
639        self.inner.body_done.set(true);
640        self.finish_if_ready();
641    }
642
643    fn progress_active_promise(&self) {
644        let active = self.inner.active_promise.borrow().clone();
645        if let Some(active) = active {
646            active.state();
647        }
648    }
649
650    fn check_deadline(&self) {
651        if self.inner.deadline.is_some_and(WorkDeadline::expired) {
652            self.cancel(Value::Keyword("deadline-exceeded".into()));
653        }
654    }
655
656    fn register_finalizer(&self, finalizer: WorkFinalizer) -> bool {
657        if self.inner.finalizers_started.get() || self.closed() {
658            return false;
659        }
660        self.inner.finalizers.borrow_mut().push(finalizer);
661        true
662    }
663
664    fn finish_if_ready(&self) {
665        if !self.inner.body_done.get() {
666            return;
667        }
668        if !self.inner.children.borrow().is_empty() {
669            self.set_nonterminal_state(if self.inner.cancellation.borrow().is_some() {
670                WorkRunState::Cancelling
671            } else {
672                WorkRunState::Waiting
673            });
674            return;
675        }
676        if self.inner.finalizers_started.replace(true) {
677            return;
678        }
679
680        let context = self.context();
681        let mut finalizer_error = None;
682        let mut finalizers = std::mem::take(&mut *self.inner.finalizers.borrow_mut());
683        while let Some(finalizer) = finalizers.pop() {
684            let result = with_current_work_context(context.clone(), || finalizer(context.clone()));
685            if finalizer_error.is_none() {
686                if let Err(error) = result {
687                    finalizer_error = Some(error);
688                }
689            }
690        }
691
692        if let Some(cancellation) = self.inner.cancellation.borrow().clone() {
693            self.settle_terminal(
694                WorkRunState::Cancelled,
695                Some(cancellation.rejection.clone()),
696                Some(cancellation.reason),
697                None,
698            );
699            return;
700        }
701        if let Some(error) = finalizer_error {
702            self.settle_terminal(WorkRunState::Failed, Some(error), None, None);
703            return;
704        }
705        match self.inner.body_outcome.borrow_mut().take() {
706            Some(Ok(value)) => {
707                self.settle_terminal(WorkRunState::Completed, None, None, Some(value));
708            }
709            Some(Err(error)) => {
710                self.settle_terminal(WorkRunState::Failed, Some(error), None, None);
711            }
712            None => {
713                self.settle_terminal(
714                    WorkRunState::Failed,
715                    Some(work_failure("work body produced no outcome".into())),
716                    None,
717                    None,
718                );
719            }
720        }
721    }
722
723    fn settle_terminal(
724        &self,
725        state: WorkRunState,
726        error: Option<PromiseRejection>,
727        cancel_reason: Option<Value>,
728        value: Option<Value>,
729    ) -> bool {
730        {
731            let mut status = self.inner.status.borrow_mut();
732            if status.state.terminal() {
733                return false;
734            }
735            status.state = state;
736            status.finished_at_millis = Some(now_millis());
737            status.error = error.clone();
738            status.cancel_reason = cancel_reason.clone();
739        }
740        let event_detail = if state == WorkRunState::Failed {
741            error.as_ref().map(PromiseRejection::value)
742        } else {
743            cancel_reason
744        };
745        match state {
746            WorkRunState::Completed => {
747                self.inner.result.resolve(value.unwrap_or(Value::Nil));
748            }
749            WorkRunState::Failed | WorkRunState::Cancelled => {
750                self.inner
751                    .result
752                    .reject_rejection(error.expect("terminal failure requires rejection"));
753            }
754            _ => unreachable!("non-terminal state passed to settle_terminal"),
755        }
756        self.publish_event(work_event_type(state), state, event_detail, true);
757        self.notify_parent();
758        true
759    }
760
761    fn set_nonterminal_state(&self, state: WorkRunState) {
762        let mut status = self.inner.status.borrow_mut();
763        let changed = !status.state.terminal() && status.state != state;
764        if changed {
765            status.state = state;
766        }
767        drop(status);
768        if changed && state == WorkRunState::Waiting {
769            self.publish_event("work/run-waiting", state, None, false);
770        }
771    }
772
773    fn notify_parent(&self) {
774        if self.inner.parent_notified.replace(true) {
775            return;
776        }
777        let Some(parent) = self.inner.parent.as_ref().and_then(Weak::upgrade) else {
778            return;
779        };
780        WorkRun { inner: parent }.child_closed(self);
781    }
782
783    fn context(&self) -> WorkContext {
784        let host = self
785            .inner
786            .host
787            .upgrade()
788            .map(|inner| WorkHost { inner })
789            .expect("work host was dropped while run remained live");
790        WorkContext {
791            host,
792            run: self.clone(),
793        }
794    }
795
796    fn publish_event(
797        &self,
798        kind: &str,
799        state: WorkRunState,
800        detail: Option<Value>,
801        terminal: bool,
802    ) {
803        let pending = {
804            let mut events = self.inner.events.borrow_mut();
805            let sequence = events.values.len() + 1;
806            events
807                .values
808                .push(work_event(&self.inner.id, sequence, kind, state, detail));
809            if terminal {
810                events.closed = true;
811            }
812            let mut pending = Vec::new();
813            let mut retained = Vec::new();
814            for weak in std::mem::take(&mut events.cursors) {
815                let Some(cursor) = weak.upgrade() else {
816                    continue;
817                };
818                if let Some(settlement) = event_take(&events, &mut cursor.borrow_mut()) {
819                    pending.push(settlement);
820                }
821                retained.push(Rc::downgrade(&cursor));
822            }
823            events.cursors = retained;
824            pending
825        };
826        for (promise, value) in pending {
827            promise.resolve(value);
828        }
829    }
830
831    fn publish_domain_event(&self, kind: &str, data: Value) {
832        let pending = {
833            let mut events = self.inner.events.borrow_mut();
834            let sequence = events.values.len() + 1;
835            events.values.push(Value::Map(
836                [
837                    (
838                        Value::Keyword("event/type".into()),
839                        Value::Keyword(kind.into()),
840                    ),
841                    (
842                        Value::Keyword("event/run".into()),
843                        Value::String(self.inner.id.to_string()),
844                    ),
845                    (
846                        Value::Keyword("event/sequence".into()),
847                        Value::Number(sequence as i64),
848                    ),
849                    (Value::Keyword("event/data".into()), data),
850                ]
851                .into_iter()
852                .collect(),
853            ));
854            let mut pending = Vec::new();
855            let mut retained = Vec::new();
856            for weak in std::mem::take(&mut events.cursors) {
857                let Some(cursor) = weak.upgrade() else {
858                    continue;
859                };
860                if let Some(settlement) = event_take(&events, &mut cursor.borrow_mut()) {
861                    pending.push(settlement);
862                }
863                retained.push(Rc::downgrade(&cursor));
864            }
865            events.cursors = retained;
866            pending
867        };
868        for (promise, value) in pending {
869            promise.resolve(value);
870        }
871    }
872}
873
874fn work_event(
875    id: &WorkId,
876    sequence: usize,
877    kind: &str,
878    state: WorkRunState,
879    detail: Option<Value>,
880) -> Value {
881    let data = Value::Map(
882        [
883            (
884                Value::Keyword("state".into()),
885                Value::Keyword(work_state_name(state).into()),
886            ),
887            (
888                Value::Keyword("detail".into()),
889                detail.unwrap_or(Value::Nil),
890            ),
891        ]
892        .into_iter()
893        .collect(),
894    );
895    Value::Map(
896        [
897            (
898                Value::Keyword("event/type".into()),
899                Value::Keyword(kind.into()),
900            ),
901            (
902                Value::Keyword("event/run".into()),
903                Value::String(id.to_string()),
904            ),
905            (
906                Value::Keyword("event/sequence".into()),
907                Value::Number(sequence as i64),
908            ),
909            (Value::Keyword("event/data".into()), data),
910        ]
911        .into_iter()
912        .collect(),
913    )
914}
915
916fn work_state_name(state: WorkRunState) -> &'static str {
917    match state {
918        WorkRunState::Queued => "queued",
919        WorkRunState::Running => "running",
920        WorkRunState::Waiting => "waiting",
921        WorkRunState::Cancelling => "cancelling",
922        WorkRunState::Completed => "completed",
923        WorkRunState::Failed => "failed",
924        WorkRunState::Cancelled => "cancelled",
925    }
926}
927
928fn work_event_type(state: WorkRunState) -> &'static str {
929    match state {
930        WorkRunState::Completed => "work/run-completed",
931        WorkRunState::Failed => "work/run-failed",
932        WorkRunState::Cancelled => "work/run-cancelled",
933        _ => unreachable!("terminal work event requires a terminal state"),
934    }
935}
936
937fn event_take(events: &WorkEventLog, cursor: &mut WorkEventCursor) -> Option<(Promise, Value)> {
938    let promise = cursor.pending.take()?;
939    if cursor.next < events.values.len() {
940        let value = events.values[cursor.next].clone();
941        cursor.next += 1;
942        return Some((promise, value));
943    }
944    if events.closed || cursor.closed {
945        cursor.closed = true;
946        return Some((promise, Value::Nil));
947    }
948    cursor.pending = Some(promise);
949    None
950}
951
952fn event_next(
953    events: &Rc<RefCell<WorkEventLog>>,
954    cursor: &Rc<RefCell<WorkEventCursor>>,
955) -> Result<Promise, String> {
956    let promise = Promise::new();
957    let settlement = {
958        let events = events.borrow();
959        let mut cursor = cursor.borrow_mut();
960        if cursor.closed {
961            Some((promise.clone(), Value::Nil))
962        } else if cursor.pending.is_some() {
963            return Err("stream/pending-pull: only one Stream/next may be pending".into());
964        } else {
965            cursor.pending = Some(promise.clone());
966            event_take(&events, &mut cursor)
967        }
968    };
969    if let Some((target, value)) = settlement {
970        target.resolve(value);
971    }
972    Ok(promise)
973}
974
975#[cfg(test)]
976mod tests;