fix: more work on background tasks

This commit is contained in:
2026-07-24 11:43:11 +02:00
parent c187108f89
commit da13bac755
45 changed files with 2905 additions and 732 deletions

View File

@@ -12,6 +12,7 @@ impl BdsApp {
let on_progress: engine::rebuild::ProgressFn =
Arc::new(move |pct, event| {
tm.report_progress(tid, Some(pct), Some(event.localized(locale)));
!tm.is_cancelled(tid)
});
let report = engine::rebuild::rebuild_from_filesystem_with_progress(
db.conn(),
@@ -42,14 +43,28 @@ impl BdsApp {
"engine.calendarStarted",
move |db_path, project_id, data_dir, tm, tid| {
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
tm.report_progress(tid, Some(0.20), Some(t(locale, "engine.loadingPosts")));
engine::calendar::regenerate_calendar(db.conn(), &data_dir, &project_id)
.map_err(|e| e.to_string())?;
tm.report_progress(
tid,
Some(0.90),
Some(t(locale, "engine.writingCalendar")),
);
engine::calendar::regenerate_calendar_with_progress(
db.conn(),
&data_dir,
&project_id,
|current, total, name| {
tm.report_progress(
tid,
Some(current as f32 / total.max(1) as f32),
Some(tw(
locale,
"engine.checkingItem",
&[
("current", &current.to_string()),
("total", &total.to_string()),
("name", name),
],
)),
);
!tm.is_cancelled(tid)
},
)
.map_err(|e| e.to_string())?;
Ok("done".to_string())
},
)
@@ -59,24 +74,39 @@ impl BdsApp {
TabType::TranslationValidation,
"tabBar.translationValidation",
);
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
if self.active_project.is_none() || self.data_dir.is_none() {
return Task::none();
};
}
self.translation_validation_state.is_running = true;
self.translation_validation_state.error_message = None;
let db_path = self.db_path.clone();
let project_id = project.id.clone();
let data_dir = data_dir.clone();
Task::perform(
async move {
tokio::task::spawn_blocking(move || {
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
let meta = engine::meta::read_project_json(&data_dir)
.map_err(|e| e.to_string())?;
let main_lang = meta.main_language.as_deref().unwrap_or("en");
let blog_langs = meta.blog_languages.clone();
let on_item: engine::validate_translations::ItemProgressFn =
Box::new(move |_current, _total, _name| {});
let locale = self.ui_locale;
self.spawn_result_task(
"menu.item.validateTranslations",
move |db_path, project_id, data_dir, tm, tid| {
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
let meta = engine::meta::read_project_json(&data_dir)
.map_err(|e| e.to_string())?;
let main_lang = meta.main_language.as_deref().unwrap_or("en");
let blog_langs = meta.blog_languages.clone();
let progress_tm = Arc::clone(&tm);
let on_item: engine::validate_translations::ItemProgressFn =
Box::new(move |current, total, name| {
progress_tm.report_progress(
tid,
Some(current as f32 / total.max(1) as f32),
Some(tw(
locale,
"engine.checkingItem",
&[
("current", &current.to_string()),
("total", &total.to_string()),
("name", name),
],
)),
);
!progress_tm.is_cancelled(tid)
});
let report =
engine::validate_translations::validate_translations_with_progress(
db.conn(),
&data_dir,
@@ -85,16 +115,31 @@ impl BdsApp {
main_lang,
Some(on_item),
)
.map_err(|e| e.to_string())
})
.await
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
.map_err(|e| e.to_string())?;
tm.report_progress(
tid,
Some(1.0),
Some(tw(
locale,
"engine.validateTranslationsComplete",
&[
("dbIssues", &report.db_issues.len().to_string()),
("fsIssues", &report.fs_issues.len().to_string()),
],
)),
);
Ok(report)
},
Message::TranslationValidationLoaded,
|task_id, result| Message::TranslationValidationLoaded { task_id, result },
)
}
Message::TranslationValidationLoaded(result) => {
Message::TranslationValidationLoaded { task_id, result } => {
let cancelled = self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
self.translation_validation_state.is_running = false;
if cancelled {
return Task::none();
}
match result {
Ok(report) => {
self.translation_validation_state.report = Some(report);
@@ -149,8 +194,13 @@ impl BdsApp {
self.open_singleton_tab(TabType::MetadataDiff, "tabBar.metadataDiff");
self.start_metadata_diff()
}
Message::MetadataDiffLoaded(result) => {
Message::MetadataDiffLoaded { task_id, result } => {
let cancelled = self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
self.metadata_diff_state.is_running = false;
if cancelled {
return Task::none();
}
match result {
Ok(report) => {
self.metadata_diff_state.report = Some(report);
@@ -170,36 +220,38 @@ impl BdsApp {
else {
return Task::none();
};
let (Some(project), Some(data_dir)) =
(self.active_project.as_ref(), self.data_dir.as_ref())
else {
if self.active_project.is_none() || self.data_dir.is_none() {
return Task::none();
};
}
self.metadata_diff_state.is_repairing = true;
let db_path = self.db_path.clone();
let project_id = project.id.clone();
let data_dir = data_dir.clone();
Task::perform(
async move {
tokio::task::spawn_blocking(move || {
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
engine::metadata_diff::repair_metadata_diff_item(
db.conn(),
&data_dir,
&project_id,
direction,
&item,
)
.map_err(|error| error.to_string())
})
.await
.map_err(|error| error.to_string())?
let locale = self.ui_locale;
self.spawn_result_task(
"metadataDiff.repair",
move |db_path, project_id, data_dir, tm, tid| {
tm.report_progress(tid, Some(0.0), Some(t(locale, "metadataDiff.repair")));
if tm.is_cancelled(tid) {
return Err("operation cancelled".into());
}
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
engine::metadata_diff::repair_metadata_diff_item(
db.conn(),
&data_dir,
&project_id,
direction,
&item,
)
.map_err(|error| error.to_string())
},
Message::MetadataDiffItemRepaired,
|task_id, result| Message::MetadataDiffItemRepaired { task_id, result },
)
}
Message::MetadataDiffItemRepaired(result) => {
Message::MetadataDiffItemRepaired { task_id, result } => {
let cancelled = self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
self.metadata_diff_state.is_repairing = false;
if cancelled {
return Task::none();
}
match result {
Ok(()) => {
self.notify(
@@ -214,6 +266,62 @@ impl BdsApp {
}
}
}
Message::ImportMetadataOrphan(index) => {
let Some(orphan) = self
.metadata_diff_state
.report
.as_ref()
.and_then(|report| report.orphans.get(index))
.cloned()
else {
return Task::none();
};
self.metadata_diff_state.is_repairing = true;
let locale = self.ui_locale;
self.spawn_result_task(
"metadataDiff.importOrphan",
move |db_path, project_id, data_dir, tm, tid| {
tm.report_progress(
tid,
Some(0.0),
Some(t(locale, "metadataDiff.importOrphan")),
);
if tm.is_cancelled(tid) {
return Err("operation cancelled".into());
}
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
engine::metadata_diff::import_orphan_file(
db.conn(),
&data_dir,
&project_id,
&orphan,
)
.map_err(|error| error.to_string())
},
|task_id, result| Message::MetadataOrphanImported { task_id, result },
)
}
Message::MetadataOrphanImported { task_id, result } => {
let cancelled = self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
self.metadata_diff_state.is_repairing = false;
if cancelled {
return Task::none();
}
match result {
Ok(()) => {
self.notify(
ToastLevel::Success,
&t(self.ui_locale, "metadataDiff.orphanImported"),
);
self.start_metadata_diff()
}
Err(error) => {
self.notify(ToastLevel::Error, &error);
Task::none()
}
}
}
Message::RunSiteValidation => self.start_site_validation(),
Message::ApplySiteValidation => self.apply_site_validation(),
Message::EngineTaskDone {
@@ -223,15 +331,14 @@ impl BdsApp {
result,
} => {
let search_rebuild_finished = self.search_index_rebuild_task_id == Some(task_id);
let cancelled = self.task_manager.status(task_id) == Some(TaskStatus::Cancelled);
let refresh_semantic_tags = !cancelled
let cancellation_requested = self.task_manager.is_cancelled(task_id);
let refresh_semantic_tags = !cancellation_requested
&& result.is_ok()
&& matches!(
operation,
"embeddings.indexing" | "menu.item.rebuildEmbeddingIndex"
);
match &result {
_ if cancelled => {}
Ok(detail) => {
self.task_manager.complete(task_id);
if operation == "engine.rebuildStarted" {
@@ -258,12 +365,14 @@ impl BdsApp {
}
Err(err) => {
self.task_manager.fail(task_id, err.clone());
let message = tw(
self.ui_locale,
"common.operationFailed",
&[("operation", &label), ("error", err)],
);
self.notify(ToastLevel::Error, &message);
if !cancellation_requested {
let message = tw(
self.ui_locale,
"common.operationFailed",
&[("operation", &label), ("error", err)],
);
self.notify(ToastLevel::Error, &message);
}
}
}
if search_rebuild_finished {
@@ -293,12 +402,41 @@ impl BdsApp {
self.refresh_task_snapshots();
Task::batch([sidebar_task, semantic_task])
}
Message::SiteGenerationPrepared {
task_id,
validation,
force,
result,
} => {
let cancelled = self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
if cancelled {
if validation.is_some() {
self.site_validation_state.is_applying = false;
}
return Task::none();
}
match result {
Ok((prepared, counts)) => {
self.queue_prepared_site_generation(validation, force, prepared, counts)
}
Err(error) => {
if validation.is_some() {
self.site_validation_state.is_applying = false;
self.site_validation_state.error_message = Some(error.clone());
}
self.notify(ToastLevel::Error, &error);
Task::none()
}
}
}
Message::SiteGenerationSectionDone {
group_id,
task_id,
result,
} => {
if self.task_manager.status(task_id) == Some(TaskStatus::Cancelled) {
if self.task_manager.is_cancelled(task_id) {
self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
return Task::none();
}
@@ -335,21 +473,54 @@ impl BdsApp {
}
}
let should_index =
let next_phase =
self.site_generation_workflows
.get(&group_id)
.is_some_and(|workflow| {
workflow.index_task_id.is_none()
.and_then(|workflow| {
(workflow.calendar_task_id.is_none()
&& workflow.index_task_id.is_none()
&& workflow.render_task_ids.iter().all(|task_id| {
self.task_manager.status(*task_id)
== Some(TaskStatus::Completed)
})
}))
.then_some(workflow.calendar_needed)
});
self.refresh_task_snapshots();
if should_index {
self.queue_site_search_index(&group_id)
} else {
Task::none()
match next_phase {
Some(true) => self.queue_site_calendar(&group_id),
Some(false) => self.queue_site_search_index(&group_id),
None => Task::none(),
}
}
Message::SiteGenerationCalendarDone {
group_id,
task_id,
result,
} => {
if self.task_manager.is_cancelled(task_id) {
self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
return Task::none();
}
match result {
Ok(()) => {
self.task_manager.complete(task_id);
self.refresh_task_snapshots();
self.queue_site_search_index(&group_id)
}
Err(error) => {
self.task_manager.fail(task_id, error.clone());
self.task_manager.cancel_group(&group_id);
if let Some(workflow) = self.site_generation_workflows.remove(&group_id)
&& workflow.kind == SiteGenerationKind::Validation
{
self.site_validation_state.is_applying = false;
self.site_validation_state.error_message = Some(error.clone());
}
self.notify(ToastLevel::Error, &error);
self.refresh_task_snapshots();
Task::none()
}
}
}
Message::SiteGenerationIndexDone {
@@ -357,7 +528,8 @@ impl BdsApp {
task_id,
result,
} => {
if self.task_manager.status(task_id) == Some(TaskStatus::Cancelled) {
if self.task_manager.is_cancelled(task_id) {
self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
return Task::none();
}
@@ -415,8 +587,13 @@ impl BdsApp {
}
}
}
Message::SiteValidationLoaded(result) => {
Message::SiteValidationLoaded { task_id, result } => {
let cancelled = self.finish_result_task(task_id, &result);
self.refresh_task_snapshots();
self.site_validation_state.is_running = false;
if cancelled {
return Task::none();
}
self.site_validation_state.has_run = true;
match result {
Ok(report) => {

View File

@@ -259,10 +259,11 @@ impl BdsApp {
Task::perform(
async move {
let Some(worker) = task_manager.admit(task_id).await else {
return Err("cancelled".to_string());
};
tokio::task::spawn_blocking(move || {
if !task_manager.wait_until_runnable(task_id) {
return Err("cancelled".to_string());
}
let _worker = worker;
let engine = GitEngine::new(&data_dir);
let old_head = (operation == GitOperation::Pull)
.then(|| engine.head())

View File

@@ -14,35 +14,40 @@ fn shorten_task_url(url: &str) -> String {
}
impl BdsApp {
pub(super) fn finish_result_task<T>(
&self,
task_id: TaskId,
result: &Result<T, String>,
) -> bool {
let cancellation_requested = self.task_manager.is_cancelled(task_id);
match result {
Ok(_) => self.task_manager.complete(task_id),
Err(error) => self.task_manager.fail(task_id, error.clone()),
}
cancellation_requested && result.is_err()
}
pub(super) fn refresh_task_snapshots(&mut self) {
self.task_snapshots = self
.task_manager
.snapshots()
.into_iter()
.map(|snapshot| {
let status_str = match &snapshot.status {
TaskStatus::Pending => t(self.ui_locale, "tasks.statusPending"),
TaskStatus::Running => t(self.ui_locale, "tasks.statusRunning"),
TaskStatus::Completed => t(self.ui_locale, "tasks.statusCompleted"),
TaskStatus::Failed(error) => {
tw(self.ui_locale, "tasks.statusFailed", &[("error", error)])
}
TaskStatus::Cancelled => t(self.ui_locale, "tasks.statusCancelled"),
};
TaskSnapshot {
id: snapshot.id,
label: snapshot.label,
group_id: snapshot.group_id,
group_name: snapshot.group_name,
status: status_str,
progress: snapshot.progress,
message: snapshot.message,
is_cancellable: matches!(
snapshot.status,
TaskStatus::Pending | TaskStatus::Running
),
}
.map(|snapshot| TaskSnapshot {
id: snapshot.id,
source: crate::state::navigation::TaskSource::Local,
label: snapshot.label,
group_id: snapshot.group_id,
group_name: snapshot.group_name,
status: snapshot.status.clone(),
progress: snapshot.progress,
message: snapshot.message,
cancellation_requested: snapshot.cancellation_requested,
is_cancellable: matches!(
snapshot.status,
TaskStatus::Pending | TaskStatus::Running
) && !snapshot.cancellation_requested,
})
.chain(self.remote_task_snapshots.iter().cloned())
.collect();
}
@@ -69,7 +74,7 @@ impl BdsApp {
} else {
SiteGenerationKind::Full
};
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
let Some(data_dir) = &self.data_dir else {
if kind == SiteGenerationKind::Validation {
self.site_validation_state.is_applying = false;
}
@@ -79,6 +84,16 @@ impl BdsApp {
);
return Task::none();
};
if self.active_project.is_none() {
if kind == SiteGenerationKind::Validation {
self.site_validation_state.is_applying = false;
}
self.notify(
ToastLevel::Error,
&t(self.ui_locale, "engine.generateSiteNoProject"),
);
return Task::none();
}
let metadata = match engine::meta::read_project_json(data_dir) {
Ok(metadata) => metadata,
Err(error) => {
@@ -107,37 +122,122 @@ impl BdsApp {
return Task::none();
}
let sections = validation.as_ref().map_or_else(
|| engine::generation::GenerationSection::ALL.to_vec(),
|validation| engine::generation::sections_from_validation_report(validation, &metadata),
);
if sections.is_empty() {
return Task::none();
}
let task_validation = validation.clone();
let locale = self.ui_locale;
self.spawn_result_task(
"engine.prepareGeneration",
move |db_path, project_id, data_dir, tm, tid| {
tm.report_progress(
tid,
Some(0.0),
Some(t(locale, "engine.preparingGeneration")),
);
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
let metadata = engine::meta::read_project_json(&data_dir)
.map_err(|error| error.to_string())?;
if tm.is_cancelled(tid) {
return Err("operation cancelled".into());
}
let posts =
bds_core::db::queries::post::list_posts_by_project(db.conn(), &project_id)
.map_err(|error| error.to_string())?
.into_iter()
.filter(engine::generation::has_published_snapshot)
.collect::<Vec<_>>();
let total_posts = posts.len();
let mut sources = Vec::with_capacity(total_posts);
for (index, post) in posts.into_iter().enumerate() {
tm.report_progress(
tid,
Some(0.05 + (index + 1) as f32 / total_posts.max(1) as f32 * 0.35),
Some(t(locale, "engine.loadingPosts")),
);
if tm.is_cancelled(tid) {
return Err("operation cancelled".into());
}
if let Some(source) =
engine::generation::load_published_post_source(&data_dir, post)
.map_err(|error| error.to_string())?
{
sources.push(source);
}
}
tm.report_progress(
tid,
Some(0.45),
Some(t(locale, "engine.preparingGeneration")),
);
let prepared = Arc::new(
engine::generation::prepare_site_generation(
db.conn(),
&data_dir,
&project_id,
&metadata,
&sources,
)
.map_err(|error| error.to_string())?,
);
if tm.is_cancelled(tid) {
return Err("operation cancelled".into());
}
let sections = task_validation.as_ref().map_or_else(
|| engine::generation::GenerationSection::ALL.to_vec(),
|report| engine::generation::sections_from_validation_report(report, &metadata),
);
let counts = sections
.into_iter()
.map(|section| {
(
section,
engine::generation::prepared_section_page_count(
&prepared,
task_validation.as_ref(),
section,
),
)
})
.collect();
tm.report_progress(tid, Some(1.0), Some(t(locale, "engine.generationPrepared")));
Ok((prepared, counts))
},
move |task_id, result| Message::SiteGenerationPrepared {
task_id,
validation: validation.clone(),
force,
result,
},
)
}
pub(super) fn queue_prepared_site_generation(
&mut self,
validation: Option<engine::validate_site::SiteValidationReport>,
force: bool,
prepared: Arc<engine::generation::PreparedSiteGeneration>,
page_estimates: HashMap<engine::generation::GenerationSection, usize>,
) -> Task<Message> {
let kind = if validation.is_some() {
SiteGenerationKind::Validation
} else if force {
SiteGenerationKind::Forced
} else {
SiteGenerationKind::Full
};
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
return Task::none();
};
let sections = page_estimates.keys().copied().collect::<Vec<_>>();
let calendar_needed = kind == SiteGenerationKind::Validation
&& sections.iter().any(|section| {
matches!(
section,
engine::generation::GenerationSection::Core
| engine::generation::GenerationSection::Date
)
});
let project_id = project.id.clone();
let db_path = self.db_path.clone();
let data_dir = data_dir.clone();
let published_posts = match self.db.as_ref().and_then(|db| {
bds_core::db::queries::post::list_posts_by_project(db.conn(), &project_id).ok()
}) {
Some(posts) => posts
.into_iter()
.filter(engine::generation::has_published_snapshot)
.collect::<Vec<_>>(),
None => return Task::none(),
};
let page_estimates = match bds_core::render::estimate_site_render_pages(
&data_dir,
&metadata,
&published_posts,
) {
Ok(estimates) => estimates,
Err(error) => {
self.notify(ToastLevel::Error, &error.to_string());
return Task::none();
}
};
let group_id = format!("site-generation:{}", Uuid::new_v4());
let group_name = t(
self.ui_locale,
@@ -150,7 +250,10 @@ impl BdsApp {
let mut render_task_ids = Vec::new();
let mut tasks = Vec::new();
for section in sections {
for section in engine::generation::GenerationSection::ALL
.into_iter()
.filter(|section| sections.contains(section))
{
let label = t(self.ui_locale, generation_section_label_key(section));
self.add_output(&label);
let page_work = page_estimates[&section];
@@ -173,14 +276,20 @@ impl BdsApp {
let task_data_dir = data_dir.clone();
let task_group_id = group_id.clone();
let task_validation = validation.clone();
let task_prepared = Arc::clone(&prepared);
let locale = self.ui_locale;
tasks.push(Task::perform(
async move {
let Some(worker) = task_manager.admit(task_id).await else {
return Err("cancelled".to_string());
};
tokio::task::spawn_blocking(move || {
let _worker = worker;
run_site_generation_section(
task_db_path,
task_project_id,
task_data_dir,
task_prepared,
task_manager,
task_id,
section,
@@ -210,6 +319,8 @@ impl BdsApp {
data_dir,
group_name,
render_task_ids,
calendar_needed,
calendar_task_id: None,
index_task_id: None,
report: engine::generation::GenerationReport::default(),
},
@@ -218,6 +329,75 @@ impl BdsApp {
Task::batch(tasks)
}
pub(super) fn queue_site_calendar(&mut self, group_id: &str) -> Task<Message> {
let Some(workflow) = self.site_generation_workflows.get(group_id).cloned() else {
return Task::none();
};
let label = t(self.ui_locale, "engine.calendarStarted");
let task_id = self
.task_manager
.submit_grouped(&label, group_id, &workflow.group_name);
if let Some(workflow) = self.site_generation_workflows.get_mut(group_id) {
workflow.calendar_task_id = Some(task_id);
}
self.refresh_task_snapshots();
let task_manager = Arc::clone(&self.task_manager);
let task_group_id = group_id.to_string();
let locale = self.ui_locale;
Task::perform(
async move {
let Some(worker) = task_manager.admit(task_id).await else {
return Err("cancelled".to_string());
};
tokio::task::spawn_blocking(move || {
let _worker = worker;
task_manager.report_progress(
task_id,
Some(0.0),
Some(t(locale, "engine.loadingPosts")),
);
let db =
Database::open(&workflow.db_path).map_err(|error| error.to_string())?;
engine::calendar::regenerate_calendar_with_progress(
db.conn(),
&workflow.data_dir,
&workflow.project_id,
|current, total, name| {
task_manager.report_progress(
task_id,
Some(current as f32 / total.max(1) as f32),
Some(tw(
locale,
"engine.checkingItem",
&[
("current", &current.to_string()),
("total", &total.to_string()),
("name", name),
],
)),
);
!task_manager.is_cancelled(task_id)
},
)
.map_err(|error| error.to_string())?;
task_manager.report_progress(
task_id,
Some(1.0),
Some(t(locale, "engine.writingCalendar")),
);
Ok(())
})
.await
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
},
move |result| Message::SiteGenerationCalendarDone {
group_id: task_group_id.clone(),
task_id,
result,
},
)
}
pub(super) fn queue_site_search_index(&mut self, group_id: &str) -> Task<Message> {
let Some(workflow) = self.site_generation_workflows.get(group_id).cloned() else {
return Task::none();
@@ -237,10 +417,11 @@ impl BdsApp {
let task_group_id = group_id.to_string();
Task::perform(
async move {
let Some(worker) = task_manager.admit(task_id).await else {
return Err("cancelled".to_string());
};
tokio::task::spawn_blocking(move || {
if !task_manager.wait_until_runnable(task_id) {
return Err("cancelled".to_string());
}
let _worker = worker;
let db =
Database::open(&workflow.db_path).map_err(|error| error.to_string())?;
let metadata = engine::meta::read_project_json(&workflow.data_dir)
@@ -312,7 +493,7 @@ impl BdsApp {
if workflow.kind == SiteGenerationKind::Validation {
self.site_validation_state.is_applying = false;
}
self.add_output(&t(self.ui_locale, "engine.generationCancelled"));
self.add_output(&t(self.ui_locale, "engine.generationCancellationRequested"));
self.refresh_task_snapshots();
true
}
@@ -337,7 +518,10 @@ impl BdsApp {
let task_id = self.task_manager.submit(&label);
self.search_index_rebuild_running = true;
self.search_index_rebuild_task_id = Some(task_id);
self.active_modal = Some(modal::ModalState::SearchIndexRebuilding);
self.active_modal = Some(modal::ModalState::SearchIndexRebuilding {
task_id,
cancellation_requested: false,
});
self.refresh_task_snapshots();
self.sync_menu_state();
@@ -346,10 +530,11 @@ impl BdsApp {
let task_manager = Arc::clone(&self.task_manager);
Task::perform(
async move {
let Some(worker) = task_manager.admit(task_id).await else {
return Err("cancelled".to_string());
};
tokio::task::spawn_blocking(move || {
if !task_manager.wait_until_runnable(task_id) {
return Err("cancelled".to_string());
}
let _worker = worker;
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
let progress_manager = Arc::clone(&task_manager);
let on_item: engine::search::ItemProgressFn =
@@ -373,6 +558,7 @@ impl BdsApp {
Some(progress),
Some(message),
);
!progress_manager.is_cancelled(task_id)
});
let report = engine::search::rebuild_search_index(db.conn(), Some(on_item))
.map_err(|error| error.to_string())?;
@@ -403,6 +589,49 @@ impl BdsApp {
self.spawn_engine_task_in_group(label_key, None, work)
}
pub(super) fn spawn_result_task<T, F, M>(
&mut self,
label_key: &'static str,
work: F,
message: M,
) -> Task<Message>
where
T: Send + 'static,
F: FnOnce(PathBuf, String, PathBuf, Arc<TaskManager>, TaskId) -> Result<T, String>
+ Send
+ 'static,
M: Fn(TaskId, Result<T, String>) -> Message + Send + Sync + 'static,
{
let (Some(project_id), Some(data_dir)) = (
self.active_project
.as_ref()
.map(|project| project.id.clone()),
self.data_dir.clone(),
) else {
return Task::none();
};
let label = t(self.ui_locale, label_key);
self.add_output(&label);
let task_id = self.task_manager.submit(&label);
self.refresh_task_snapshots();
let task_manager = Arc::clone(&self.task_manager);
let db_path = self.db_path.clone();
Task::perform(
async move {
let Some(worker) = task_manager.admit(task_id).await else {
return Err("cancelled".to_string());
};
tokio::task::spawn_blocking(move || {
let _worker = worker;
work(db_path, project_id, data_dir, task_manager, task_id)
})
.await
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
},
move |result| message(task_id, result),
)
}
pub(super) fn spawn_grouped_engine_task<F>(
&mut self,
label_key: &'static str,
@@ -453,10 +682,11 @@ impl BdsApp {
Task::perform(
async move {
let Some(worker) = tm.admit(task_id).await else {
return Err("cancelled".to_string());
};
tokio::task::spawn_blocking(move || {
if !tm.wait_until_runnable(task_id) {
return Err("cancelled".to_string());
}
let _worker = worker;
work(db_path, project_id, data_dir, tm, task_id)
})
.await
@@ -490,6 +720,7 @@ fn run_site_generation_section(
db_path: PathBuf,
project_id: String,
data_dir: PathBuf,
prepared: Arc<engine::generation::PreparedSiteGeneration>,
task_manager: Arc<TaskManager>,
task_id: TaskId,
section: engine::generation::GenerationSection,
@@ -498,9 +729,6 @@ fn run_site_generation_section(
locale: UiLocale,
expected_pages: usize,
) -> Result<engine::generation::GenerationReport, String> {
if !task_manager.wait_until_runnable(task_id) {
return Err("cancelled".to_string());
}
task_manager.report_progress(
task_id,
Some(if expected_pages == 0 { 1.0 } else { 0.0 }),
@@ -511,26 +739,6 @@ fn run_site_generation_section(
)),
);
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
let metadata = engine::meta::read_project_json(&data_dir).map_err(|error| error.to_string())?;
let posts = bds_core::db::queries::post::list_posts_by_project(db.conn(), &project_id)
.map_err(|error| error.to_string())?;
let sources = posts
.into_iter()
.filter(engine::generation::has_published_snapshot)
.map(|post| engine::generation::load_published_post_source(&data_dir, post))
.collect::<Result<Vec<_>, _>>()
.map_err(|error| error.to_string())?
.into_iter()
.flatten()
.collect::<Vec<_>>();
let prepared = engine::generation::prepare_site_generation(
db.conn(),
&data_dir,
&project_id,
&metadata,
&sources,
)
.map_err(|error| error.to_string())?;
let output_dir = data_dir.join("html");
std::fs::create_dir_all(&output_dir).map_err(|error| error.to_string())?;
let render_manager = Arc::clone(&task_manager);
@@ -565,7 +773,7 @@ fn run_site_generation_section(
db.conn(),
&output_dir,
&project_id,
&prepared,
prepared.as_ref(),
&validation,
section,
on_page,
@@ -576,7 +784,7 @@ fn run_site_generation_section(
db.conn(),
&output_dir,
&project_id,
&prepared,
prepared.as_ref(),
section,
force,
&on_rendered,