Skip to main content

ruvector_node/
lib.rs

1//! Node.js bindings for Ruvector via NAPI-RS
2//!
3//! High-performance Rust vector database with zero-copy buffer sharing,
4//! async/await support, and complete TypeScript type definitions.
5
6#![allow(clippy::all)]
7#![allow(clippy::pedantic)]
8
9use napi::bindgen_prelude::*;
10use napi_derive::napi;
11use ruvector_core::{
12    types::{DbOptions, HnswConfig, QuantizationConfig},
13    DistanceMetric, SearchQuery, SearchResult, VectorDB as CoreVectorDB, VectorEntry,
14};
15use std::sync::Arc;
16use std::sync::RwLock;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19// Import new crates
20use ruvector_collections::CollectionManager as CoreCollectionManager;
21use ruvector_filter::FilterExpression;
22use ruvector_metrics::{gather_metrics, HealthChecker, HealthStatus};
23use std::path::PathBuf;
24
25/// Distance metric for similarity calculation
26#[napi(string_enum)]
27#[derive(Debug)]
28pub enum JsDistanceMetric {
29    /// Euclidean (L2) distance
30    Euclidean,
31    /// Cosine similarity (converted to distance)
32    Cosine,
33    /// Dot product (converted to distance for maximization)
34    DotProduct,
35    /// Manhattan (L1) distance
36    Manhattan,
37}
38
39impl From<JsDistanceMetric> for DistanceMetric {
40    fn from(metric: JsDistanceMetric) -> Self {
41        match metric {
42            JsDistanceMetric::Euclidean => DistanceMetric::Euclidean,
43            JsDistanceMetric::Cosine => DistanceMetric::Cosine,
44            JsDistanceMetric::DotProduct => DistanceMetric::DotProduct,
45            JsDistanceMetric::Manhattan => DistanceMetric::Manhattan,
46        }
47    }
48}
49
50/// Quantization configuration
51#[napi(object)]
52#[derive(Debug, Clone)]
53pub struct JsQuantizationConfig {
54    /// Quantization type: "none", "scalar", "product", "binary"
55    pub r#type: String,
56    /// Number of subspaces (for product quantization)
57    pub subspaces: Option<u32>,
58    /// Codebook size (for product quantization)
59    pub k: Option<u32>,
60}
61
62impl From<JsQuantizationConfig> for QuantizationConfig {
63    fn from(config: JsQuantizationConfig) -> Self {
64        match config.r#type.as_str() {
65            "none" => QuantizationConfig::None,
66            "scalar" => QuantizationConfig::Scalar,
67            "product" => QuantizationConfig::Product {
68                subspaces: config.subspaces.unwrap_or(16) as usize,
69                k: config.k.unwrap_or(256) as usize,
70            },
71            "binary" => QuantizationConfig::Binary,
72            // Turbo4 (ADR-296): applied 4-bit quantization. Seed/multiplier
73            // use the core defaults; a full JS surface can extend this later.
74            "turbo4" => QuantizationConfig::Turbo4 {
75                rotation_seed: ruvector_core::types::default_turbo4_rotation_seed(),
76                rescore_multiplier: ruvector_core::types::default_turbo4_rescore_multiplier(),
77                policy: ruvector_core::types::SearchPolicy::default(),
78                search_quantization: ruvector_core::types::SearchQuantization::default(),
79            },
80            _ => QuantizationConfig::Scalar,
81        }
82    }
83}
84
85/// HNSW index configuration
86#[napi(object)]
87#[derive(Debug, Clone)]
88pub struct JsHnswConfig {
89    /// Number of connections per layer (M)
90    pub m: Option<u32>,
91    /// Size of dynamic candidate list during construction
92    pub ef_construction: Option<u32>,
93    /// Size of dynamic candidate list during search
94    pub ef_search: Option<u32>,
95    /// Maximum number of elements
96    pub max_elements: Option<u32>,
97}
98
99impl From<JsHnswConfig> for HnswConfig {
100    fn from(config: JsHnswConfig) -> Self {
101        HnswConfig {
102            m: config.m.unwrap_or(32) as usize,
103            ef_construction: config.ef_construction.unwrap_or(200) as usize,
104            ef_search: config.ef_search.unwrap_or(100) as usize,
105            max_elements: config.max_elements.unwrap_or(10_000_000) as usize,
106        }
107    }
108}
109
110/// Database configuration options
111#[napi(object)]
112#[derive(Debug)]
113pub struct JsDbOptions {
114    /// Vector dimensions
115    pub dimensions: u32,
116    /// Distance metric
117    pub distance_metric: Option<JsDistanceMetric>,
118    /// Storage path
119    pub storage_path: Option<String>,
120    /// HNSW configuration
121    pub hnsw_config: Option<JsHnswConfig>,
122    /// Quantization configuration
123    pub quantization: Option<JsQuantizationConfig>,
124}
125
126impl From<JsDbOptions> for DbOptions {
127    fn from(options: JsDbOptions) -> Self {
128        DbOptions {
129            dimensions: options.dimensions as usize,
130            distance_metric: options
131                .distance_metric
132                .map(Into::into)
133                .unwrap_or(DistanceMetric::Cosine),
134            storage_path: options
135                .storage_path
136                .unwrap_or_else(|| "./ruvector.db".to_string()),
137            hnsw_config: options.hnsw_config.map(Into::into),
138            quantization: options.quantization.map(Into::into),
139        }
140    }
141}
142
143/// Vector entry
144#[napi(object)]
145pub struct JsVectorEntry {
146    /// Optional ID (auto-generated if not provided)
147    pub id: Option<String>,
148    /// Vector data as Float32Array or array of numbers
149    pub vector: Float32Array,
150    /// Optional metadata as JSON string (use JSON.stringify on objects)
151    pub metadata: Option<String>,
152}
153
154impl JsVectorEntry {
155    fn to_core(&self) -> Result<VectorEntry> {
156        // Parse JSON string to HashMap<String, serde_json::Value>
157        let metadata = self.metadata.as_ref().and_then(|s| {
158            serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
159        });
160
161        Ok(VectorEntry {
162            id: self.id.clone(),
163            vector: self.vector.to_vec(),
164            metadata,
165        })
166    }
167}
168
169/// Search query parameters
170#[napi(object)]
171pub struct JsSearchQuery {
172    /// Query vector as Float32Array or array of numbers
173    pub vector: Float32Array,
174    /// Number of results to return (top-k)
175    pub k: u32,
176    /// Optional ef_search parameter for HNSW
177    pub ef_search: Option<u32>,
178    /// Optional metadata filter as JSON string (use JSON.stringify on objects)
179    pub filter: Option<String>,
180}
181
182impl JsSearchQuery {
183    fn to_core(&self) -> Result<SearchQuery> {
184        // Parse JSON string to HashMap<String, serde_json::Value>
185        let filter = self.filter.as_ref().and_then(|s| {
186            serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
187        });
188
189        Ok(SearchQuery {
190            vector: self.vector.to_vec(),
191            k: self.k as usize,
192            filter,
193            ef_search: self.ef_search.map(|v| v as usize),
194        })
195    }
196}
197
198/// Search result with similarity score
199#[napi(object)]
200#[derive(Clone)]
201pub struct JsSearchResult {
202    /// Vector ID
203    pub id: String,
204    /// Distance/similarity score (lower is better for distance metrics)
205    pub score: f64,
206    /// Vector data (if requested)
207    pub vector: Option<Float32Array>,
208    /// Metadata as JSON string (use JSON.parse to convert to object)
209    pub metadata: Option<String>,
210}
211
212impl From<SearchResult> for JsSearchResult {
213    fn from(result: SearchResult) -> Self {
214        // Convert Vec<f32> to Float32Array
215        let vector = result.vector.map(|v| Float32Array::new(v));
216
217        // Convert HashMap to JSON string
218        let metadata = result.metadata.and_then(|m| serde_json::to_string(&m).ok());
219
220        JsSearchResult {
221            id: result.id,
222            score: f64::from(result.score),
223            vector,
224            metadata,
225        }
226    }
227}
228
229/// High-performance vector database with HNSW indexing
230#[napi]
231pub struct VectorDB {
232    inner: Arc<RwLock<CoreVectorDB>>,
233}
234
235#[napi]
236impl VectorDB {
237    /// Create a new vector database with the given options
238    ///
239    /// # Example
240    /// ```javascript
241    /// const db = new VectorDB({
242    ///   dimensions: 384,
243    ///   distanceMetric: 'Cosine',
244    ///   storagePath: './vectors.db',
245    ///   hnswConfig: {
246    ///     m: 32,
247    ///     efConstruction: 200,
248    ///     efSearch: 100
249    ///   }
250    /// });
251    /// ```
252    #[napi(constructor)]
253    pub fn new(options: JsDbOptions) -> Result<Self> {
254        let core_options: DbOptions = options.into();
255        let db = CoreVectorDB::new(core_options)
256            .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
257
258        Ok(Self {
259            inner: Arc::new(RwLock::new(db)),
260        })
261    }
262
263    /// Create a vector database with default options
264    ///
265    /// # Example
266    /// ```javascript
267    /// const db = VectorDB.withDimensions(384);
268    /// ```
269    #[napi(factory)]
270    pub fn with_dimensions(dimensions: u32) -> Result<Self> {
271        let db = CoreVectorDB::with_dimensions(dimensions as usize)
272            .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
273
274        Ok(Self {
275            inner: Arc::new(RwLock::new(db)),
276        })
277    }
278
279    /// Insert a vector entry into the database
280    ///
281    /// Returns the ID of the inserted vector (auto-generated if not provided)
282    ///
283    /// # Example
284    /// ```javascript
285    /// const id = await db.insert({
286    ///   vector: new Float32Array([1.0, 2.0, 3.0]),
287    ///   metadata: { text: 'example' }
288    /// });
289    /// ```
290    #[napi]
291    pub async fn insert(&self, entry: JsVectorEntry) -> Result<String> {
292        let core_entry = entry.to_core()?;
293        let db = self.inner.clone();
294
295        tokio::task::spawn_blocking(move || {
296            let db = db.read().expect("RwLock poisoned");
297            db.insert(core_entry)
298        })
299        .await
300        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
301        .map_err(|e| Error::from_reason(format!("Insert failed: {}", e)))
302    }
303
304    /// Insert multiple vectors in a batch
305    ///
306    /// Returns an array of IDs for the inserted vectors
307    ///
308    /// # Example
309    /// ```javascript
310    /// const ids = await db.insertBatch([
311    ///   { vector: new Float32Array([1, 2, 3]) },
312    ///   { vector: new Float32Array([4, 5, 6]) }
313    /// ]);
314    /// ```
315    #[napi]
316    pub async fn insert_batch(&self, entries: Vec<JsVectorEntry>) -> Result<Vec<String>> {
317        let core_entries: Result<Vec<VectorEntry>> = entries.iter().map(|e| e.to_core()).collect();
318        let core_entries = core_entries?;
319        let db = self.inner.clone();
320
321        tokio::task::spawn_blocking(move || {
322            let db = db.read().expect("RwLock poisoned");
323            db.insert_batch(core_entries)
324        })
325        .await
326        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
327        .map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e)))
328    }
329
330    /// Search for similar vectors
331    ///
332    /// Returns an array of search results sorted by similarity
333    ///
334    /// # Example
335    /// ```javascript
336    /// const results = await db.search({
337    ///   vector: new Float32Array([1, 2, 3]),
338    ///   k: 10,
339    ///   filter: { category: 'example' }
340    /// });
341    /// ```
342    #[napi]
343    pub async fn search(&self, query: JsSearchQuery) -> Result<Vec<JsSearchResult>> {
344        let core_query = query.to_core()?;
345        let db = self.inner.clone();
346
347        tokio::task::spawn_blocking(move || {
348            let db = db.read().expect("RwLock poisoned");
349            db.search(core_query)
350        })
351        .await
352        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
353        .map_err(|e| Error::from_reason(format!("Search failed: {}", e)))
354        .map(|results| results.into_iter().map(Into::into).collect())
355    }
356
357    /// Delete a vector by ID
358    ///
359    /// Returns true if the vector was deleted, false if not found
360    ///
361    /// # Example
362    /// ```javascript
363    /// const deleted = await db.delete('vector-id');
364    /// ```
365    #[napi]
366    pub async fn delete(&self, id: String) -> Result<bool> {
367        let db = self.inner.clone();
368
369        tokio::task::spawn_blocking(move || {
370            let db = db.read().expect("RwLock poisoned");
371            db.delete(&id)
372        })
373        .await
374        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
375        .map_err(|e| Error::from_reason(format!("Delete failed: {}", e)))
376    }
377
378    /// Get a vector by ID
379    ///
380    /// Returns the vector entry if found, null otherwise
381    ///
382    /// # Example
383    /// ```javascript
384    /// const entry = await db.get('vector-id');
385    /// if (entry) {
386    ///   console.log('Found:', entry.metadata);
387    /// }
388    /// ```
389    #[napi]
390    pub async fn get(&self, id: String) -> Result<Option<JsVectorEntry>> {
391        let db = self.inner.clone();
392
393        let result = tokio::task::spawn_blocking(move || {
394            let db = db.read().expect("RwLock poisoned");
395            db.get(&id)
396        })
397        .await
398        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
399        .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?;
400
401        Ok(result.map(|entry| {
402            // Convert HashMap to JSON string
403            let metadata = entry.metadata.and_then(|m| serde_json::to_string(&m).ok());
404
405            JsVectorEntry {
406                id: entry.id,
407                vector: Float32Array::new(entry.vector),
408                metadata,
409            }
410        }))
411    }
412
413    /// Get the number of vectors in the database
414    ///
415    /// # Example
416    /// ```javascript
417    /// const count = await db.len();
418    /// console.log(`Database contains ${count} vectors`);
419    /// ```
420    #[napi]
421    pub async fn len(&self) -> Result<u32> {
422        let db = self.inner.clone();
423
424        tokio::task::spawn_blocking(move || {
425            let db = db.read().expect("RwLock poisoned");
426            db.len()
427        })
428        .await
429        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
430        .map_err(|e| Error::from_reason(format!("Len failed: {}", e)))
431        .map(|len| len as u32)
432    }
433
434    /// Check if the database is empty
435    ///
436    /// # Example
437    /// ```javascript
438    /// if (await db.isEmpty()) {
439    ///   console.log('Database is empty');
440    /// }
441    /// ```
442    #[napi]
443    pub async fn is_empty(&self) -> Result<bool> {
444        let db = self.inner.clone();
445
446        tokio::task::spawn_blocking(move || {
447            let db = db.read().expect("RwLock poisoned");
448            db.is_empty()
449        })
450        .await
451        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
452        .map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e)))
453    }
454}
455
456/// Get the version of the Ruvector library
457#[napi]
458pub fn version() -> String {
459    env!("CARGO_PKG_VERSION").to_string()
460}
461
462/// Test function to verify the bindings are working
463#[napi]
464pub fn hello() -> String {
465    "Hello from Ruvector Node.js bindings!".to_string()
466}
467
468/// Filter for metadata-based search
469#[napi(object)]
470#[derive(Debug, Clone)]
471pub struct JsFilter {
472    /// Field name to filter on
473    pub field: String,
474    /// Operator: "eq", "ne", "gt", "gte", "lt", "lte", "in", "match"
475    pub operator: String,
476    /// Value to compare against (JSON string)
477    pub value: String,
478}
479
480impl JsFilter {
481    fn to_filter_expression(&self) -> Result<FilterExpression> {
482        let value: serde_json::Value = serde_json::from_str(&self.value)
483            .map_err(|e| Error::from_reason(format!("Invalid JSON value: {}", e)))?;
484
485        Ok(match self.operator.as_str() {
486            "eq" => FilterExpression::eq(&self.field, value),
487            "ne" => FilterExpression::ne(&self.field, value),
488            "gt" => FilterExpression::gt(&self.field, value),
489            "gte" => FilterExpression::gte(&self.field, value),
490            "lt" => FilterExpression::lt(&self.field, value),
491            "lte" => FilterExpression::lte(&self.field, value),
492            "match" => FilterExpression::Match {
493                field: self.field.clone(),
494                text: value.as_str().unwrap_or("").to_string(),
495            },
496            _ => FilterExpression::eq(&self.field, value),
497        })
498    }
499}
500
501/// Collection configuration
502#[napi(object)]
503#[derive(Debug, Clone)]
504pub struct JsCollectionConfig {
505    /// Vector dimensions
506    pub dimensions: u32,
507    /// Distance metric
508    pub distance_metric: Option<JsDistanceMetric>,
509    /// HNSW configuration
510    pub hnsw_config: Option<JsHnswConfig>,
511    /// Quantization configuration
512    pub quantization: Option<JsQuantizationConfig>,
513}
514
515impl From<JsCollectionConfig> for ruvector_collections::CollectionConfig {
516    fn from(config: JsCollectionConfig) -> Self {
517        ruvector_collections::CollectionConfig {
518            dimensions: config.dimensions as usize,
519            distance_metric: config
520                .distance_metric
521                .map(Into::into)
522                .unwrap_or(DistanceMetric::Cosine),
523            hnsw_config: config.hnsw_config.map(Into::into),
524            quantization: config.quantization.map(Into::into),
525            on_disk_payload: true,
526        }
527    }
528}
529
530/// Collection statistics
531#[napi(object)]
532#[derive(Debug, Clone)]
533pub struct JsCollectionStats {
534    /// Number of vectors in the collection
535    pub vectors_count: u32,
536    /// Disk space used in bytes
537    pub disk_size_bytes: i64,
538    /// RAM space used in bytes
539    pub ram_size_bytes: i64,
540}
541
542impl From<ruvector_collections::CollectionStats> for JsCollectionStats {
543    fn from(stats: ruvector_collections::CollectionStats) -> Self {
544        JsCollectionStats {
545            vectors_count: stats.vectors_count as u32,
546            disk_size_bytes: stats.disk_size_bytes as i64,
547            ram_size_bytes: stats.ram_size_bytes as i64,
548        }
549    }
550}
551
552/// Collection alias
553#[napi(object)]
554#[derive(Debug, Clone)]
555pub struct JsAlias {
556    /// Alias name
557    pub alias: String,
558    /// Collection name
559    pub collection: String,
560}
561
562impl From<(String, String)> for JsAlias {
563    fn from(tuple: (String, String)) -> Self {
564        JsAlias {
565            alias: tuple.0,
566            collection: tuple.1,
567        }
568    }
569}
570
571/// Collection manager for multi-collection support
572#[napi]
573pub struct CollectionManager {
574    inner: Arc<RwLock<CoreCollectionManager>>,
575}
576
577#[napi]
578impl CollectionManager {
579    /// Create a new collection manager
580    ///
581    /// # Example
582    /// ```javascript
583    /// const manager = new CollectionManager('./collections');
584    /// ```
585    #[napi(constructor)]
586    pub fn new(base_path: Option<String>) -> Result<Self> {
587        let path = PathBuf::from(base_path.unwrap_or_else(|| "./collections".to_string()));
588        let manager = CoreCollectionManager::new(path).map_err(|e| {
589            Error::from_reason(format!("Failed to create collection manager: {}", e))
590        })?;
591
592        Ok(Self {
593            inner: Arc::new(RwLock::new(manager)),
594        })
595    }
596
597    /// Create a new collection
598    ///
599    /// # Example
600    /// ```javascript
601    /// await manager.createCollection('my_vectors', {
602    ///   dimensions: 384,
603    ///   distanceMetric: 'Cosine'
604    /// });
605    /// ```
606    #[napi]
607    pub async fn create_collection(&self, name: String, config: JsCollectionConfig) -> Result<()> {
608        let core_config: ruvector_collections::CollectionConfig = config.into();
609        let manager = self.inner.clone();
610
611        tokio::task::spawn_blocking(move || {
612            let manager = manager.write().expect("RwLock poisoned");
613            manager.create_collection(&name, core_config)
614        })
615        .await
616        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
617        .map_err(|e| Error::from_reason(format!("Failed to create collection: {}", e)))
618    }
619
620    /// List all collections
621    ///
622    /// # Example
623    /// ```javascript
624    /// const collections = await manager.listCollections();
625    /// console.log('Collections:', collections);
626    /// ```
627    #[napi]
628    pub async fn list_collections(&self) -> Result<Vec<String>> {
629        let manager = self.inner.clone();
630
631        tokio::task::spawn_blocking(move || {
632            let manager = manager.read().expect("RwLock poisoned");
633            manager.list_collections()
634        })
635        .await
636        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))
637    }
638
639    /// Delete a collection
640    ///
641    /// # Example
642    /// ```javascript
643    /// await manager.deleteCollection('my_vectors');
644    /// ```
645    #[napi]
646    pub async fn delete_collection(&self, name: String) -> Result<()> {
647        let manager = self.inner.clone();
648
649        tokio::task::spawn_blocking(move || {
650            let manager = manager.write().expect("RwLock poisoned");
651            manager.delete_collection(&name)
652        })
653        .await
654        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
655        .map_err(|e| Error::from_reason(format!("Failed to delete collection: {}", e)))
656    }
657
658    /// Get collection statistics
659    ///
660    /// # Example
661    /// ```javascript
662    /// const stats = await manager.getStats('my_vectors');
663    /// console.log(`Vectors: ${stats.vectorsCount}`);
664    /// ```
665    #[napi]
666    pub async fn get_stats(&self, name: String) -> Result<JsCollectionStats> {
667        let manager = self.inner.clone();
668
669        tokio::task::spawn_blocking(move || {
670            let manager = manager.read().expect("RwLock poisoned");
671            manager.collection_stats(&name)
672        })
673        .await
674        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
675        .map_err(|e| Error::from_reason(format!("Failed to get stats: {}", e)))
676        .map(Into::into)
677    }
678
679    /// Create an alias for a collection
680    ///
681    /// # Example
682    /// ```javascript
683    /// await manager.createAlias('latest', 'my_vectors_v2');
684    /// ```
685    #[napi]
686    pub async fn create_alias(&self, alias: String, collection: String) -> Result<()> {
687        let manager = self.inner.clone();
688
689        tokio::task::spawn_blocking(move || {
690            let manager = manager.write().expect("RwLock poisoned");
691            manager.create_alias(&alias, &collection)
692        })
693        .await
694        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
695        .map_err(|e| Error::from_reason(format!("Failed to create alias: {}", e)))
696    }
697
698    /// Delete an alias
699    ///
700    /// # Example
701    /// ```javascript
702    /// await manager.deleteAlias('latest');
703    /// ```
704    #[napi]
705    pub async fn delete_alias(&self, alias: String) -> Result<()> {
706        let manager = self.inner.clone();
707
708        tokio::task::spawn_blocking(move || {
709            let manager = manager.write().expect("RwLock poisoned");
710            manager.delete_alias(&alias)
711        })
712        .await
713        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
714        .map_err(|e| Error::from_reason(format!("Failed to delete alias: {}", e)))
715    }
716
717    /// List all aliases
718    ///
719    /// # Example
720    /// ```javascript
721    /// const aliases = await manager.listAliases();
722    /// for (const alias of aliases) {
723    ///   console.log(`${alias.alias} -> ${alias.collection}`);
724    /// }
725    /// ```
726    #[napi]
727    pub async fn list_aliases(&self) -> Result<Vec<JsAlias>> {
728        let manager = self.inner.clone();
729
730        let aliases = tokio::task::spawn_blocking(move || {
731            let manager = manager.read().expect("RwLock poisoned");
732            manager.list_aliases()
733        })
734        .await
735        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?;
736
737        Ok(aliases.into_iter().map(Into::into).collect())
738    }
739}
740
741/// Health response
742#[napi(object)]
743#[derive(Debug, Clone)]
744pub struct JsHealthResponse {
745    /// Status: "healthy", "degraded", or "unhealthy"
746    pub status: String,
747    /// Version string
748    pub version: String,
749    /// Uptime in seconds
750    pub uptime_seconds: i64,
751}
752
753/// Get Prometheus metrics
754///
755/// # Example
756/// ```javascript
757/// const metrics = getMetrics();
758/// console.log(metrics);
759/// ```
760#[napi]
761pub fn get_metrics() -> String {
762    gather_metrics()
763}
764
765/// Get health status
766///
767/// # Example
768/// ```javascript
769/// const health = getHealth();
770/// console.log(`Status: ${health.status}`);
771/// console.log(`Uptime: ${health.uptimeSeconds}s`);
772/// ```
773#[napi]
774pub fn get_health() -> JsHealthResponse {
775    let checker = HealthChecker::new();
776    let health = checker.health();
777
778    JsHealthResponse {
779        status: match health.status {
780            HealthStatus::Healthy => "healthy".to_string(),
781            HealthStatus::Degraded => "degraded".to_string(),
782            HealthStatus::Unhealthy => "unhealthy".to_string(),
783        },
784        version: health.version,
785        uptime_seconds: health.uptime_seconds as i64,
786    }
787}