rumtk_web/utils/jobs.rs
1/*
2 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 * This toolkit aims to be reliable, simple, performant, and standards compliant.
4 * Copyright (C) 2025 Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 * Copyright (C) 2025 Ethan Dixon
6 * Copyright (C) 2025 MedicalMasses L.L.C. <contact@medicalmasses.com>
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21use crate::HTMLResult;
22use rumtk_core::core::RUMResult;
23use rumtk_core::id::id_to_uuid;
24use rumtk_core::strings::rumtk_format;
25use rumtk_core::threading::threading_manager::{Task, TaskID, TaskManager};
26use rumtk_core::types::RUMBuffer;
27
28pub type JobID = TaskID;
29pub type JobBuffer = RUMBuffer;
30
31pub type JobResult = RUMResult<Option<HTMLResult>>;
32pub type Job = Task<JobResult>;
33type JobManager = TaskManager<JobResult>;
34
35static mut TASK_MANAGER: Option<JobManager> = None;
36
37pub fn job_str_id_to_id(id: &str) -> RUMResult<JobID> {
38 id_to_uuid(id)
39}
40
41pub fn init_job_manager(workers: &usize) -> RUMResult<()> {
42 let manager = TaskManager::<JobResult>::new(workers)?;
43 unsafe {
44 TASK_MANAGER = Some(manager);
45 }
46 Ok(())
47}
48
49pub fn get_manager() -> RUMResult<&'static mut JobManager> {
50 unsafe {
51 match TASK_MANAGER.as_mut() {
52 Some(m) => Ok(m),
53 None => return Err(rumtk_format!("TaskManager is not initialized")),
54 }
55 }
56}
57
58#[macro_export]
59macro_rules! rumtk_web_init_job_manager {
60 ( $workers:expr ) => {{
61 use $crate::jobs::init_job_manager;
62 init_job_manager($workers)
63 }};
64}
65
66#[macro_export]
67macro_rules! rumtk_web_get_job_manager {
68 ( ) => {{
69 use $crate::jobs::get_manager;
70 get_manager()
71 }};
72}
73
74#[macro_export]
75macro_rules! rumtk_web_generate_job_id {
76 ( $id:expr ) => {{
77 use $crate::jobs::job_str_id_to_id;
78 job_str_id_to_id($id)
79 }};
80}
81
82///
83/// THis macro allows you to check if a background job has completed.
84///
85/// If the job has completed, return the result which is of type [JobResult].
86///
87/// If the job is still going, force render a drop in loader component set to retry the check. This
88/// loader gets passed the calling element name (`$element_name`) so that it can render the results
89/// as it sees fit.
90///
91/// ## Example
92///
93/// ### Loader Render
94/// ```
95/// use rumtk_core::{rumtk_async_sleep, rumtk_new_lock};
96/// use rumtk_core::strings::{RUMString};
97/// use rumtk_web::utils::testdata::data::{JOB_LOADER_TEST_PATTERN};
98/// use rumtk_web::defaults::{PARAMS_ID, PARAMS_CSS_CLASS, DEFAULT_TEXT_ITEM, DEFAULT_NO_TEXT};
99/// use rumtk_web::utils::jobs::{JobResult};
100/// use rumtk_web::{HTMLResult, SharedAppState, URLParams, URLPath, AppState, RUMWebResponse, RUMWebData};
101/// use rumtk_web::{rumtk_web_init_job_manager, rumtk_web_get_job_manager, rumtk_web_check_on_job, rumtk_web_get_text_item, rumtk_web_post_process_html, rumtk_web_init_components};
102///
103/// let workers: usize = 5;
104/// rumtk_web_init_job_manager!(&workers);
105/// rumtk_web_init_components!(
106/// Some(vec![
107/// ("my_element", my_element)
108/// ])
109/// );
110///
111/// async fn basic_processor() -> JobResult {
112/// rumtk_async_sleep!(100).await;
113/// Ok(None)
114/// }
115///
116/// fn my_element(_path_components: URLPath, params: URLParams, state: SharedAppState) -> HTMLResult {
117/// let job_id = rumtk_web_get_text_item!(params, PARAMS_ID, DEFAULT_NO_TEXT);
118/// let css_class = rumtk_web_get_text_item!(params, PARAMS_CSS_CLASS, DEFAULT_TEXT_ITEM);
119///
120/// let job_result = rumtk_web_check_on_job!("my_element", job_id, state);
121///
122/// let job_data = match job_result {
123/// Some(t) => t?.to_string(),
124/// _ => RUMString::from("")
125/// };
126///
127/// rumtk_web_post_process_html!(job_data)
128/// }
129///
130/// let app_state = rumtk_new_lock!(AppState::default());
131/// let mut params = RUMWebData::new();
132/// let job_id = rumtk_web_get_job_manager!().unwrap().spawn_task(basic_processor()).unwrap();
133/// params.insert(RUMString::from(PARAMS_ID), job_id.to_string());
134/// let rendered = my_element(&[], ¶ms, app_state.clone()).unwrap().to_string();
135///
136/// assert!(rendered.as_str().contains(JOB_LOADER_TEST_PATTERN), "Element did not render loader!");
137///
138/// ```
139///
140/// ### Component Render
141/// ```
142/// use rumtk_core::{rumtk_sleep, rumtk_new_lock};
143/// use rumtk_core::strings::{RUMString};
144/// use rumtk_web::utils::testdata::data::{JOB_LOADER_TEST_PATTERN};
145/// use rumtk_web::defaults::{PARAMS_ID, PARAMS_CSS_CLASS, DEFAULT_TEXT_ITEM, DEFAULT_NO_TEXT};
146/// use rumtk_web::utils::jobs::{JobResult};
147/// use rumtk_web::{HTMLResult, SharedAppState, URLParams, URLPath, AppState, RUMWebResponse, RUMWebData};
148/// use rumtk_web::{rumtk_web_init_job_manager, rumtk_web_get_job_manager, rumtk_web_check_on_job, rumtk_web_get_text_item, rumtk_web_post_process_html, rumtk_web_init_components};
149///
150/// const HELLO_STR: &str = "Hello World";
151///
152/// let workers: usize = 5;
153/// rumtk_web_init_job_manager!(&workers);
154/// rumtk_web_init_components!(
155/// Some(vec![
156/// ("my_element", my_element)
157/// ])
158/// );
159///
160/// async fn basic_processor() -> JobResult {
161/// Ok(Some(rumtk_web_post_process_html!(RUMString::from(HELLO_STR))))
162/// }
163///
164/// fn my_element(_path_components: URLPath, params: URLParams, state: SharedAppState) -> HTMLResult {
165/// let job_id = rumtk_web_get_text_item!(params, PARAMS_ID, DEFAULT_NO_TEXT);
166/// let css_class = rumtk_web_get_text_item!(params, PARAMS_CSS_CLASS, DEFAULT_TEXT_ITEM);
167///
168/// let job_result = rumtk_web_check_on_job!("my_element", job_id, state);
169///
170/// let job_data = job_result.unwrap()?;
171///
172/// rumtk_web_post_process_html!(job_data)
173/// }
174///
175/// let app_state = rumtk_new_lock!(AppState::default());
176/// let mut params = RUMWebData::new();
177/// let job_id = rumtk_web_get_job_manager!().unwrap().spawn_task(basic_processor()).unwrap();
178/// params.insert(RUMString::from(PARAMS_ID), job_id.to_string());
179///
180/// rumtk_sleep!(1);
181/// let rendered = my_element(&[], ¶ms, app_state.clone()).unwrap().to_string();
182///
183/// assert!(rendered.is_empty(), "Element results survived the rendering process's filtering!");
184///
185/// ```
186///
187#[macro_export]
188macro_rules! rumtk_web_check_on_job {
189 ( $element_name:expr, $job_id:expr, $state:expr ) => {{
190 use $crate::defaults::{DEFAULT_TEXT_ITEM};
191 rumtk_web_check_on_job!($element_name, $job_id, DEFAULT_TEXT_ITEM, $state)
192 }};
193 ( $element_name:expr, $job_id:expr, $css_class:expr, $state:expr ) => {{
194 use rumtk_core::id::id_to_uuid;
195 use $crate::defaults::{PARAMS_CSS_CLASS, PARAMS_ELEMENT, PARAMS_ID};
196 use $crate::{rumtk_web_get_job_manager, rumtk_web_render_component};
197
198 let id = id_to_uuid($job_id)?;
199 let job_finished = rumtk_web_get_job_manager!()?.is_finished(&id);
200 let result = match job_finished {
201 true => &rumtk_web_get_job_manager!()?.wait_on(&id)?.result,
202 false => {
203 return rumtk_web_render_component!(
204 "job_loader",
205 [
206 (PARAMS_ID, $job_id),
207 (PARAMS_ELEMENT, $element_name),
208 (PARAMS_CSS_CLASS, $css_class),
209 ], $state
210 );
211 },
212 };
213
214 match result {
215 Some(r) => r.clone()?,
216 None => None,
217 }
218 }};
219}