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