Files
asepharyana-hub/apps/tools/backend/workers/src/nats/progress.rs
T
asepharyanaandKilo 67288c8723 feat(infra): add tools service with document scanner, image & PDF tools
Implement self-hosted document scanner and media processing tools as
an alternative to CamScanner/ilovepdf without third-party uploads.

Backend: Rust Axum gateway + worker pool with NATS JetStream queue
Frontend: Next.js 16 + shadcn/ui + Tailwind v4 + Framer Motion
Pipeline: Canny edge detection -> DLT homography warp -> Sauvola
binarization -> Hough deskew -> Tesseract OCR -> searchable PDF

Phase 1 (MVP) delivers:
- Document scanner with perspective correction and OCR
- Image compress/resize/convert tools
- PDF merge/split/compress tools
- Real-time WebSocket progress updates
- Rate limiting, auto-cleanup, Prometheus metrics
- Full CI/CD pipeline with Docker multi-stage build

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:08:09 +07:00

82 lines
2.3 KiB
Rust

use redis::AsyncCommands;
use uuid::Uuid;
use tools_common::types::{JobStatus, Tool};
/// Reports progress from worker to NATS and Redis.
pub struct ProgressReporter {
redis: redis::Client,
nats: async_nats::Client,
job_id: Uuid,
tool: Tool,
}
impl ProgressReporter {
pub fn new(redis: redis::Client, nats: async_nats::Client, job_id: Uuid, tool: Tool) -> Self {
Self {
redis,
nats,
job_id,
tool,
}
}
/// Report progress: updates Redis and publishes to NATS.
pub async fn report(
&self,
status: JobStatus,
stage: &str,
progress: u8,
message: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Update Redis
if let Ok(mut conn) = self.redis.get_multiplexed_async_connection().await {
let key = format!("job:{}", self.job_id);
if let Ok(json) = conn.get::<_, String>(&key).await {
if let Ok(mut job) = serde_json::from_str::<tools_common::types::Job>(&json) {
job.status = status.clone();
let updated = serde_json::to_string(&job).unwrap_or(json);
let _: Result<(), _> = conn.set_ex(key, updated, job.ttl_seconds).await;
}
}
}
// Publish to NATS
let progress_msg = tools_common::types::JobProgress {
job_id: self.job_id,
status,
stage: stage.to_string(),
progress,
message: message.to_string(),
};
let subject = format!("tools.{}.progress.{}", self.tool.subject_prefix(), self.job_id);
if let Ok(payload) = serde_json::to_vec(&progress_msg) {
let _ = self.nats.publish(subject, payload.into()).await;
}
tracing::debug!(
job_id = %self.job_id,
stage = %stage,
progress = %progress,
"Progress update"
);
Ok(())
}
pub fn job_id(&self) -> Uuid {
self.job_id
}
}
impl Clone for ProgressReporter {
fn clone(&self) -> Self {
Self {
redis: self.redis.clone(),
nats: self.nats.clone(),
job_id: self.job_id,
tool: self.tool.clone(),
}
}
}