pub use job_queue::config::{self, QueueConfig, QueueConfigBuilder};
pub use job_queue::db;
pub use job_queue::error::{self, QueueError};
pub use job_queue::events::{self, *};
pub use job_queue::executor;
pub use job_queue::queue::{self, QueueManager};
pub use job_queue::types::{self, JobResult, QueueJob, QueueJobStatus, QueuePriority};
pub use job_queue::{JobContext, JobHandler, QueueEventEmitter};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tauri::{AppHandle, Emitter};
pub use stack_ids::TraceCtx;
pub fn trace_ctx_from_event_trace_id(trace_id: &Option<String>) -> Option<TraceCtx> {
trace_id.as_deref().map(TraceCtx::from_legacy_trace_id)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum DropPolicy {
DropOldest,
#[default]
DropNewest,
Block,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmitterConfig {
pub buffer_size: usize,
pub drop_policy: DropPolicy,
pub coalesce_interval_ms: u64,
#[deprecated(since = "0.3.0", note = "Use include_trace_ctx instead")]
pub include_trace_id: bool,
pub include_trace_ctx: bool,
}
#[allow(deprecated)]
impl Default for EmitterConfig {
fn default() -> Self {
Self {
buffer_size: 256,
drop_policy: DropPolicy::default(),
coalesce_interval_ms: 50,
include_trace_id: false,
include_trace_ctx: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trace_ctx_bridge_preserves_legacy_trace_id() {
let trace = Some("ui-trace-123".to_string());
let ctx = trace_ctx_from_event_trace_id(&trace).expect("trace ctx expected");
assert_eq!(ctx.trace_id, "ui-trace-123");
}
#[test]
fn trace_ctx_bridge_handles_missing_trace_id() {
assert!(trace_ctx_from_event_trace_id(&None).is_none());
}
}
impl EmitterConfig {
#[allow(deprecated)]
fn should_keep_trace(&self) -> bool {
self.include_trace_ctx || self.include_trace_id
}
}
pub struct CoalescingEmitter {
inner: Arc<dyn QueueEventEmitter>,
config: EmitterConfig,
progress_state: Mutex<ProgressState>,
}
#[derive(Default)]
struct ProgressState {
last_progress: HashMap<String, Instant>,
pending_progress: HashMap<String, JobProgressEvent>,
pending_order: VecDeque<String>,
}
impl ProgressState {
fn take_pending(&mut self, job_id: &str) -> Option<JobProgressEvent> {
let event = self.pending_progress.remove(job_id);
if event.is_some() {
self.pending_order
.retain(|queued_job_id| queued_job_id != job_id);
}
event
}
fn clear_job(&mut self, job_id: &str) -> Option<JobProgressEvent> {
let pending = self.take_pending(job_id);
self.last_progress.remove(job_id);
pending
}
fn enqueue_progress(
&mut self,
event: JobProgressEvent,
config: &EmitterConfig,
) -> Option<JobProgressEvent> {
if self.pending_progress.contains_key(&event.job_id) {
self.pending_progress.insert(event.job_id.clone(), event);
return None;
}
if self.pending_progress.len() >= config.buffer_size.max(1) {
match config.drop_policy {
DropPolicy::DropNewest => return None,
DropPolicy::DropOldest => {
if let Some(oldest_job_id) = self.pending_order.pop_front() {
self.pending_progress.remove(&oldest_job_id);
}
}
DropPolicy::Block => {
let flushed = self
.pending_order
.pop_front()
.and_then(|oldest_job_id| self.pending_progress.remove(&oldest_job_id));
self.pending_order.push_back(event.job_id.clone());
self.pending_progress.insert(event.job_id.clone(), event);
return flushed;
}
}
}
self.pending_order.push_back(event.job_id.clone());
self.pending_progress.insert(event.job_id.clone(), event);
None
}
}
impl CoalescingEmitter {
pub fn new(inner: Arc<dyn QueueEventEmitter>, config: EmitterConfig) -> Self {
Self {
inner,
config,
progress_state: Mutex::new(ProgressState::default()),
}
}
pub fn arc(
inner: Arc<dyn QueueEventEmitter>,
config: EmitterConfig,
) -> Arc<dyn QueueEventEmitter> {
Arc::new(Self::new(inner, config))
}
fn prepare_progress_emit(&self, event: JobProgressEvent) -> Vec<JobProgressEvent> {
let mut state = self
.progress_state
.lock()
.unwrap_or_else(|e| e.into_inner());
let interval = Duration::from_millis(self.config.coalesce_interval_ms);
let now = Instant::now();
if let Some(last) = state.last_progress.get(&event.job_id) {
if now.duration_since(*last) < interval {
return state
.enqueue_progress(event, &self.config)
.into_iter()
.collect();
}
}
state.last_progress.insert(event.job_id.clone(), now);
let mut to_emit = Vec::with_capacity(2);
if let Some(pending) = state.take_pending(&event.job_id) {
to_emit.push(pending);
}
to_emit.push(event);
to_emit
}
#[allow(deprecated)]
fn normalize_progress_event(&self, mut event: JobProgressEvent) -> JobProgressEvent {
if !self.config.should_keep_trace() {
event.trace_id = None;
event.trace_ctx = None;
}
event
}
fn take_terminal_pending(&self, job_id: &str) -> Option<JobProgressEvent> {
let mut state = self
.progress_state
.lock()
.unwrap_or_else(|e| e.into_inner());
state.clear_job(job_id)
}
}
#[allow(deprecated)]
impl QueueEventEmitter for CoalescingEmitter {
fn emit_job_started(&self, event: JobStartedEvent) {
let mut event = event;
if !self.config.should_keep_trace() {
event.trace_id = None;
event.trace_ctx = None;
}
self.inner.emit_job_started(event);
}
fn emit_job_completed(&self, event: JobCompletedEvent) {
let mut event = event;
if !self.config.should_keep_trace() {
event.trace_id = None;
event.trace_ctx = None;
}
if let Some(pending) = self.take_terminal_pending(&event.job_id) {
self.inner.emit_job_progress(pending);
}
self.inner.emit_job_completed(event);
}
fn emit_job_failed(&self, event: JobFailedEvent) {
let mut event = event;
if !self.config.should_keep_trace() {
event.trace_id = None;
event.trace_ctx = None;
}
if let Some(pending) = self.take_terminal_pending(&event.job_id) {
self.inner.emit_job_progress(pending);
}
self.inner.emit_job_failed(event);
}
fn emit_job_progress(&self, event: JobProgressEvent) {
let event = self.normalize_progress_event(event);
for event in self.prepare_progress_emit(event) {
self.inner.emit_job_progress(event);
}
}
fn emit_job_cancelled(&self, event: JobCancelledEvent) {
let mut event = event;
if !self.config.should_keep_trace() {
event.trace_id = None;
event.trace_ctx = None;
}
if let Some(pending) = self.take_terminal_pending(&event.job_id) {
self.inner.emit_job_progress(pending);
}
self.inner.emit_job_cancelled(event);
}
}
pub struct TauriEventEmitter {
app_handle: AppHandle,
}
impl TauriEventEmitter {
pub fn new(app_handle: AppHandle) -> Self {
Self { app_handle }
}
pub fn arc(app_handle: AppHandle) -> Arc<dyn QueueEventEmitter> {
Arc::new(Self::new(app_handle))
}
}
impl QueueEventEmitter for TauriEventEmitter {
fn emit_job_started(&self, event: JobStartedEvent) {
if let Err(e) = self.app_handle.emit("queue:job_started", event) {
tracing::debug!(error = %e, "Failed to emit queue:job_started event");
}
}
fn emit_job_completed(&self, event: JobCompletedEvent) {
if let Err(e) = self.app_handle.emit("queue:job_completed", event) {
tracing::debug!(error = %e, "Failed to emit queue:job_completed event");
}
}
fn emit_job_failed(&self, event: JobFailedEvent) {
if let Err(e) = self.app_handle.emit("queue:job_failed", event) {
tracing::debug!(error = %e, "Failed to emit queue:job_failed event");
}
}
fn emit_job_progress(&self, event: JobProgressEvent) {
if let Err(e) = self.app_handle.emit("queue:job_progress", event) {
tracing::debug!(error = %e, "Failed to emit queue:job_progress event");
}
}
fn emit_job_cancelled(&self, event: JobCancelledEvent) {
if let Err(e) = self.app_handle.emit("queue:job_cancelled", event) {
tracing::debug!(error = %e, "Failed to emit queue:job_cancelled event");
}
}
}