|
| 1 | +use crate::native::db::connection::NxDbConnection; |
| 2 | +use napi::bindgen_prelude::External; |
| 3 | +use rusqlite::params; |
| 4 | +use std::sync::{Arc, Mutex}; |
| 5 | +use tracing::debug; |
| 6 | + |
| 7 | +pub const SCHEMA: &str = "CREATE TABLE IF NOT EXISTS task_invocations ( |
| 8 | + root_pid INTEGER NOT NULL, |
| 9 | + parent_pid INTEGER NOT NULL, |
| 10 | + task_id TEXT NOT NULL, |
| 11 | + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| 12 | + PRIMARY KEY (root_pid, task_id) |
| 13 | +);"; |
| 14 | + |
| 15 | +#[napi(object)] |
| 16 | +#[derive(Clone, Debug)] |
| 17 | +pub struct InvocationRecord { |
| 18 | + pub parent_pid: u32, |
| 19 | + pub task_id: String, |
| 20 | +} |
| 21 | + |
| 22 | +#[napi] |
| 23 | +pub struct TaskInvocationTracker { |
| 24 | + db: Arc<Mutex<NxDbConnection>>, |
| 25 | + root_pid: u32, |
| 26 | +} |
| 27 | + |
| 28 | +#[napi] |
| 29 | +impl TaskInvocationTracker { |
| 30 | + #[napi(constructor)] |
| 31 | + pub fn new( |
| 32 | + #[napi(ts_arg_type = "ExternalObject<NxDbConnection>")] db: &External< |
| 33 | + Arc<Mutex<NxDbConnection>>, |
| 34 | + >, |
| 35 | + root_pid: u32, |
| 36 | + ) -> anyhow::Result<Self> { |
| 37 | + Ok(Self { |
| 38 | + db: Arc::clone(db), |
| 39 | + root_pid, |
| 40 | + }) |
| 41 | + } |
| 42 | + |
| 43 | + /// Register a task as invoked. Throws if the task was already registered (loop detected). |
| 44 | + #[napi] |
| 45 | + pub fn register_task(&self, parent_pid: u32, task_id: String) -> anyhow::Result<()> { |
| 46 | + self.db.lock().unwrap().execute( |
| 47 | + "INSERT INTO task_invocations (root_pid, parent_pid, task_id) VALUES (?1, ?2, ?3)", |
| 48 | + params![self.root_pid, parent_pid, task_id], |
| 49 | + )?; |
| 50 | + debug!( |
| 51 | + "Registered task invocation: root_pid={}, parent_pid={}, task_id={}", |
| 52 | + self.root_pid, parent_pid, &task_id |
| 53 | + ); |
| 54 | + Ok(()) |
| 55 | + } |
| 56 | + |
| 57 | + /// Remove a task invocation record after task completes. |
| 58 | + #[napi] |
| 59 | + pub fn unregister_task(&self, task_id: String) -> anyhow::Result<()> { |
| 60 | + self.db.lock().unwrap().execute( |
| 61 | + "DELETE FROM task_invocations WHERE root_pid = ?1 AND task_id = ?2", |
| 62 | + params![self.root_pid, task_id], |
| 63 | + )?; |
| 64 | + debug!( |
| 65 | + "Unregistered task invocation: root_pid={}, task_id={}", |
| 66 | + self.root_pid, &task_id |
| 67 | + ); |
| 68 | + Ok(()) |
| 69 | + } |
| 70 | + |
| 71 | + /// Get all invocations for this root_pid, ordered by creation time. |
| 72 | + #[napi] |
| 73 | + pub fn get_invocation_chain(&self) -> anyhow::Result<Vec<InvocationRecord>> { |
| 74 | + let db = self.db.lock().unwrap(); |
| 75 | + let mut stmt = db.prepare( |
| 76 | + "SELECT parent_pid, task_id FROM task_invocations WHERE root_pid = ?1 ORDER BY created_at ASC", |
| 77 | + )?; |
| 78 | + let records = stmt |
| 79 | + .query_map(params![self.root_pid], |row| { |
| 80 | + Ok(InvocationRecord { |
| 81 | + parent_pid: row.get(0)?, |
| 82 | + task_id: row.get(1)?, |
| 83 | + }) |
| 84 | + })? |
| 85 | + .collect::<Result<Vec<_>, _>>()?; |
| 86 | + Ok(records) |
| 87 | + } |
| 88 | + |
| 89 | + /// Clean up stale invocations older than 1 day (handles PID recycling). |
| 90 | + #[napi] |
| 91 | + pub fn cleanup_stale(&self) -> anyhow::Result<()> { |
| 92 | + let deleted = self.db.lock().unwrap().execute( |
| 93 | + "DELETE FROM task_invocations WHERE created_at < datetime('now', '-1 day')", |
| 94 | + [], |
| 95 | + )?; |
| 96 | + if deleted > 0 { |
| 97 | + debug!("Cleaned up {} stale invocation records", deleted); |
| 98 | + } |
| 99 | + Ok(()) |
| 100 | + } |
| 101 | +} |
0 commit comments