use wasm_bindgen::prelude::*;
use crate::alphabet::DnaSequence;
use crate::fm_index::seq_id::SeqId;
use crate::fm_index::{FmIndex, FmIndexConfig};
fn to_js_err(e: impl std::fmt::Display) -> JsValue {
JsValue::from_str(&e.to_string())
}
fn parse_fasta(input: &str) -> Result<Vec<DnaSequence>, JsValue> {
let mut sequences: Vec<DnaSequence> = Vec::new();
let mut current_seq: Option<String> = None;
let mut current_header: Option<String> = None;
for line in input.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(rest) = line.strip_prefix('>') {
if let Some(seq_str) = current_seq.take() {
if !seq_str.is_empty() {
let header = current_header.take().unwrap_or_default();
sequences.push(
DnaSequence::from_str_with_header(&seq_str, &header).map_err(to_js_err)?,
);
}
}
current_header = Some(rest.trim().to_string());
current_seq = Some(String::new());
} else {
match current_seq.as_mut() {
Some(buf) => buf.push_str(line),
None => {
sequences.push(DnaSequence::from_str(line).map_err(to_js_err)?);
}
}
}
}
if let Some(seq_str) = current_seq.take() {
if !seq_str.is_empty() {
let header = current_header.take().unwrap_or_default();
sequences
.push(DnaSequence::from_str_with_header(&seq_str, &header).map_err(to_js_err)?);
}
}
if sequences.is_empty() {
return Err(JsValue::from_str("no sequences found in input"));
}
Ok(sequences)
}
#[wasm_bindgen]
pub struct FmIndexBuilder {
sequences: Vec<DnaSequence>,
sa_sample_rate: u32,
}
#[wasm_bindgen]
impl FmIndexBuilder {
#[wasm_bindgen(constructor)]
pub fn new(sa_sample_rate: Option<u32>) -> Self {
Self {
sequences: Vec::new(),
sa_sample_rate: sa_sample_rate.unwrap_or(32),
}
}
pub fn add_sequence(&mut self, seq: &str) -> Result<(), JsValue> {
let dna = DnaSequence::from_str(seq).map_err(to_js_err)?;
self.sequences.push(dna);
Ok(())
}
pub fn add_fasta(&mut self, fasta: &str) -> Result<(), JsValue> {
let mut seqs = parse_fasta(fasta)?;
self.sequences.append(&mut seqs);
Ok(())
}
pub fn sequence_count(&self) -> usize {
self.sequences.len()
}
pub fn clear(&mut self) {
self.sequences.clear();
}
pub fn build_cpu(&self) -> Result<FmIndexHandle, JsValue> {
if self.sequences.is_empty() {
return Err(JsValue::from_str("no sequences added"));
}
let config = FmIndexConfig {
sa_sample_rate: self.sa_sample_rate,
use_gpu: false,
..Default::default()
};
let index = FmIndex::build_cpu(&self.sequences, &config).map_err(to_js_err)?;
Ok(FmIndexHandle { index })
}
pub async fn build_gpu(&self) -> Result<FmIndexHandle, JsValue> {
if self.sequences.is_empty() {
return Err(JsValue::from_str("no sequences added"));
}
let config = FmIndexConfig {
sa_sample_rate: self.sa_sample_rate,
use_gpu: true,
..Default::default()
};
let index = FmIndex::build(&self.sequences, &config)
.await
.map_err(to_js_err)?;
Ok(FmIndexHandle { index })
}
}
#[wasm_bindgen]
pub struct FmIndexHandle {
index: FmIndex,
}
#[wasm_bindgen]
impl FmIndexHandle {
pub fn count(&self, pattern: &str) -> Result<u32, JsValue> {
let encoded = encode_pattern(pattern)?;
Ok(self.index.count(&encoded))
}
pub fn locate(&self, pattern: &str) -> Result<js_sys::Array, JsValue> {
let encoded = encode_pattern(pattern)?;
let hits = self.index.locate(&encoded);
let result = js_sys::Array::new_with_length(hits.len() as u32);
for (i, (seq_id, pos)) in hits.into_iter().enumerate() {
let pair = js_sys::Array::new_with_length(2);
pair.set(0, wasm_bindgen::JsValue::from_f64(seq_id.get() as f64));
pair.set(1, wasm_bindgen::JsValue::from_f64(pos as f64));
result.set(i as u32, pair.into());
}
Ok(result)
}
pub fn seq_header(&self, id: u32) -> Option<String> {
self.index.seq_header(SeqId::new(id)).map(str::to_owned)
}
pub fn seq_id(&self, header: &str) -> Option<u32> {
self.index.seq_id(header).map(SeqId::get)
}
pub fn seq_headers(&self) -> js_sys::Array {
self.index
.seq_headers()
.iter()
.map(|h| wasm_bindgen::JsValue::from_str(h))
.collect()
}
pub fn text_len(&self) -> u32 {
self.index.text_len()
}
pub fn num_sequences(&self) -> u32 {
self.index.num_sequences()
}
pub fn to_bytes(&self) -> Result<Vec<u8>, JsValue> {
self.index.to_bytes().map_err(to_js_err)
}
pub fn from_bytes(data: &[u8]) -> Result<FmIndexHandle, JsValue> {
let index = FmIndex::from_bytes(data).map_err(to_js_err)?;
Ok(FmIndexHandle { index })
}
}
fn encode_pattern(pattern: &str) -> Result<Vec<u8>, JsValue> {
use crate::alphabet::encode_char;
pattern
.chars()
.enumerate()
.map(|(i, ch)| {
encode_char(ch).ok_or_else(|| {
JsValue::from_str(&format!("invalid DNA character '{}' at position {}", ch, i))
})
})
.collect()
}