#[derive(Debug, thiserror::Error)]
pub enum BullMqError {
#[error("invalid queue name {name:?}: {reason}")]
InvalidQueueName {
name: String,
reason: &'static str,
},
#[error("bullmq error: {0}")]
BullMq(#[from] bullmq::Error),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueueChannel {
Amadeus,
Notifica,
BajkomatApi,
KsefGpt,
N8n,
Shopify,
Custom(String),
}
impl QueueChannel {
pub fn get_queue(&self) -> &str {
match self {
QueueChannel::Amadeus => "amadeus",
QueueChannel::Notifica => "notifica",
QueueChannel::BajkomatApi => "bajkomat-api",
QueueChannel::KsefGpt => "ksef-gpt",
QueueChannel::N8n => "n8n",
QueueChannel::Shopify => "shopify",
QueueChannel::Custom(name) => name.as_str(),
}
}
pub fn validate(&self) -> Result<(), BullMqError> {
let name = self.get_queue();
if name.is_empty() {
return Err(BullMqError::InvalidQueueName {
name: name.to_string(),
reason: "must not be empty",
});
}
if name.contains(':') {
return Err(BullMqError::InvalidQueueName {
name: name.to_string(),
reason: "must not contain ':' — it would corrupt the bull:<queue>:* key layout",
});
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Backoff {
Fixed {
delay_ms: u64,
},
Exponential {
delay_ms: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RemovePolicy {
Always,
Never,
KeepLast(usize),
}
#[derive(Debug, Clone, Default)]
pub struct EnqueueOptions {
pub delay_ms: Option<u64>,
pub priority: Option<u32>,
pub attempts: Option<u32>,
pub backoff: Option<Backoff>,
pub remove_on_complete: Option<RemovePolicy>,
pub remove_on_fail: Option<RemovePolicy>,
pub job_id: Option<String>,
pub deduplication_id: Option<String>,
}
impl EnqueueOptions {
pub(crate) fn into_bullmq(self) -> bullmq::JobOptions {
bullmq::JobOptions {
delay: self.delay_ms,
priority: self.priority,
attempts: self.attempts,
job_id: self.job_id,
backoff: self.backoff.map(|b| match b {
Backoff::Fixed { delay_ms } => bullmq::types::BackoffStrategy::Fixed(delay_ms),
Backoff::Exponential { delay_ms } => {
bullmq::types::BackoffStrategy::Exponential(delay_ms)
}
}),
remove_on_complete: self.remove_on_complete.map(into_remove_on_finish),
remove_on_fail: self.remove_on_fail.map(into_remove_on_finish),
deduplication: self
.deduplication_id
.map(|id| bullmq::DeduplicationOptions {
id,
ttl: None,
extend: None,
replace: None,
keep_last_if_active: None,
}),
..Default::default()
}
}
}
fn into_remove_on_finish(policy: RemovePolicy) -> bullmq::types::RemoveOnFinish {
match policy {
RemovePolicy::Always => bullmq::types::RemoveOnFinish::Bool(true),
RemovePolicy::Never => bullmq::types::RemoveOnFinish::Bool(false),
RemovePolicy::KeepLast(n) => bullmq::types::RemoveOnFinish::Count(n),
}
}
#[derive(Debug, Clone)]
pub struct BulkJob {
pub name: String,
pub payload: serde_json::Value,
pub options: Option<EnqueueOptions>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JobHandle {
pub id: String,
pub name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct JobSnapshot {
pub id: String,
pub name: String,
pub data: serde_json::Value,
pub attempts_made: u32,
pub timestamp: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct QueueCounts {
pub waiting: u64,
pub active: u64,
pub delayed: u64,
pub prioritized: u64,
pub completed: u64,
pub failed: u64,
pub waiting_children: u64,
pub paused: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobStateFilter {
Waiting,
Active,
Delayed,
Prioritized,
Completed,
Failed,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_known_queues_to_kebab_case() {
assert_eq!(QueueChannel::Amadeus.get_queue(), "amadeus");
assert_eq!(QueueChannel::Notifica.get_queue(), "notifica");
assert_eq!(QueueChannel::BajkomatApi.get_queue(), "bajkomat-api");
assert_eq!(QueueChannel::KsefGpt.get_queue(), "ksef-gpt");
assert_eq!(QueueChannel::N8n.get_queue(), "n8n");
assert_eq!(QueueChannel::Shopify.get_queue(), "shopify");
}
#[test]
fn custom_queue_returns_its_own_name() {
let channel = QueueChannel::Custom("my-queue".to_string());
assert_eq!(channel.get_queue(), "my-queue");
}
#[test]
fn validate_accepts_known_queues() {
assert!(QueueChannel::KsefGpt.validate().is_ok());
}
#[test]
fn validate_rejects_empty_custom_name() {
let err = QueueChannel::Custom(String::new()).validate().unwrap_err();
assert!(matches!(err, BullMqError::InvalidQueueName { .. }));
}
#[test]
fn validate_rejects_colon_in_custom_name() {
let err = QueueChannel::Custom("a:b".to_string())
.validate()
.unwrap_err();
assert!(matches!(err, BullMqError::InvalidQueueName { .. }));
}
#[test]
fn default_options_map_to_empty_bullmq_options() {
let mapped = EnqueueOptions::default().into_bullmq();
assert!(mapped.delay.is_none());
assert!(mapped.priority.is_none());
assert!(mapped.attempts.is_none());
assert!(mapped.job_id.is_none());
}
#[test]
fn maps_scalar_options() {
let mapped = EnqueueOptions {
delay_ms: Some(5_000),
priority: Some(3),
attempts: Some(4),
job_id: Some("job-1".to_string()),
..Default::default()
}
.into_bullmq();
assert_eq!(mapped.delay, Some(5_000));
assert_eq!(mapped.priority, Some(3));
assert_eq!(mapped.attempts, Some(4));
assert_eq!(mapped.job_id.as_deref(), Some("job-1"));
}
#[test]
fn maps_backoff_variants() {
use bullmq::types::BackoffStrategy;
let fixed = EnqueueOptions {
backoff: Some(Backoff::Fixed { delay_ms: 1_000 }),
..Default::default()
}
.into_bullmq();
assert!(matches!(fixed.backoff, Some(BackoffStrategy::Fixed(1_000))));
let exp = EnqueueOptions {
backoff: Some(Backoff::Exponential { delay_ms: 250 }),
..Default::default()
}
.into_bullmq();
assert!(matches!(
exp.backoff,
Some(BackoffStrategy::Exponential(250))
));
}
#[test]
fn maps_remove_policies() {
use bullmq::types::RemoveOnFinish;
let mapped = EnqueueOptions {
remove_on_complete: Some(RemovePolicy::Always),
remove_on_fail: Some(RemovePolicy::KeepLast(50)),
..Default::default()
}
.into_bullmq();
assert!(matches!(
mapped.remove_on_complete,
Some(RemoveOnFinish::Bool(true))
));
assert!(matches!(
mapped.remove_on_fail,
Some(RemoveOnFinish::Count(50))
));
let never = EnqueueOptions {
remove_on_complete: Some(RemovePolicy::Never),
..Default::default()
}
.into_bullmq();
assert!(matches!(
never.remove_on_complete,
Some(RemoveOnFinish::Bool(false))
));
}
#[test]
fn maps_deduplication_id() {
let mapped = EnqueueOptions {
deduplication_id: Some("dedup-key".to_string()),
..Default::default()
}
.into_bullmq();
assert_eq!(
mapped.deduplication.map(|d| d.id).as_deref(),
Some("dedup-key")
);
}
}