Skip to content
2 changes: 2 additions & 0 deletions bin/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ wasm-pack build --target nodejs --release
echo ""
echo "Adding @urbdyn/ to package.json ..."
sed -i.bak 's|"name": "petgraph-wasm",|"name": "@urbdyn/petgraph-wasm",|g' "$repo_dir/pkg/package.json"
echo "Adding iterators to JS classes ..."
$repo_dir/bin/patch_compiled_output.js
46 changes: 46 additions & 0 deletions bin/patch_compiled_output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env node

/**
* This file is used to replace a particular error that occurs when ncc
* compiles oracledb code. More details on this issue can be found at:
* https://github.com/vercel/ncc/issues/205
*/

const fs = require('fs')
const path = require('path')

// File to make swap on
const targetFilePath = path.join(__dirname, '../pkg/petgraph_wasm.js')
const replacements = [
[path.join(__dirname, 'replacement_1_in.txt'), path.join(__dirname, 'replacement_1_out.txt')],
[path.join(__dirname, 'replacement_2_in.txt'), path.join(__dirname, 'replacement_2_out.txt')],
]

function main() {
console.log('Starting patch_compiled_output.js ...')

console.log(`targetFilePath = ${targetFilePath}`)
const targetFileText = fs.readFileSync(targetFilePath).toString()

let newFileText = targetFileText
for ([textInFile, textOutFile] of replacements) {
const textIn = fs.readFileSync(textInFile).toString()
const textOut = fs.readFileSync(textOutFile).toString()

console.log(`textInFile = ${textInFile}`)
console.log(`textOutFile = ${textOutFile}`)

newFileText = newFileText.replace(textIn, textOut)
}

if (newFileText !== targetFileText) {
console.log('Making replacement!')
// Update file with fixes
fs.writeFileSync(targetFilePath, newFileText)
} else {
console.log('No replacements made.')
}
console.log('Ending patch_compiled_output.js\n')
}

main()
3 changes: 3 additions & 0 deletions bin/replacement_1_in.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class SccGroups {

static __wrap(ptr) {
13 changes: 13 additions & 0 deletions bin/replacement_1_out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class SccGroups {
// NOTE: THIS IS ADDED AT BUILD TIME AFTER COMPILATION.
[Symbol.iterator]() {
let counter = 0;
return {
next: () => {
counter++
return { value: this.getGroup(counter - 1), done: counter > this.length }
},
}
}

static __wrap(ptr) {
3 changes: 3 additions & 0 deletions bin/replacement_2_in.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class SccGroup {

static __wrap(ptr) {
13 changes: 13 additions & 0 deletions bin/replacement_2_out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class SccGroup {
// NOTE: THIS IS ADDED AT BUILD TIME AFTER COMPILATION.
[Symbol.iterator]() {
let counter = 0;
return {
next: () => {
counter++
return { value: this.getItem(counter - 1), done: counter > this.length }
},
}
}

static __wrap(ptr) {
41 changes: 31 additions & 10 deletions example_js/basic_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

let petgraph = require('../pkg/petgraph_wasm')

let g = petgraph.DirectedGraph.new()
let g = new petgraph.DiGraph()

let cities = ["NYC","Vilnius","Knoxville","Taipei","Buenos Aires"]
const cities = ["NYC","Vilnius","Knoxville","Taipei","Buenos Aires"]
let city_name_to_index = {}
cities.forEach((city) => {
console.log(`Adding node: ${city}`)
g.add_node(city)
const i = g.addNode(city)
city_name_to_index[city] = i
})

let city_pairs = [
Expand All @@ -22,16 +24,35 @@ let city_pairs = [
["Buenos Aires","Taipei"],
]
city_pairs.forEach(([src,dest]) => {
const src_index = g.get_node_index(src)
const dest_index = g.get_node_index(dest)
console.log(`Adding graph edge: ${src} -> ${dest}`)
g.add_edge(src_index, dest_index, 0)
g.addEdge(city_name_to_index[src], city_name_to_index[dest], 0)
})

console.log("Sorting nodes ...")
const sorted_g = g.get_sorted()
console.log("\nSorting nodes ...")
const sorted_g = petgraph.toposort(g)
console.log(sorted_g)
sorted_g.forEach((i) => {
console.log(typeof i)
console.log(i)
console.log(`${i} = ${g.nodeWeight(i)}`)
})

let city_pairs2 = [
["Taipei","Buenos Aires"],
]

console.log("\nAdding city pairs 2 ...")
city_pairs2.forEach(([src,dest]) => {
console.log(`Adding graph edge: ${src} -> ${dest}`)
g.addEdge(city_name_to_index[src], city_name_to_index[dest], 0)
})

console.log("\nCreating SCC of nodes (tarjan) ...")

const scc_g = petgraph.tarjanScc(g)

console.log("\nIterator test of SCC of nodes (tarjan) ...")
for (scc_g_i of scc_g) {
console.log(scc_g_i)
for (scc_g_i_item of scc_g_i) {
console.log(`${scc_g_i_item} = ${g.nodeWeight(scc_g_i_item)}`)
}
}
54 changes: 54 additions & 0 deletions src/algo/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
use crate::graph_impl::DiGraph;
use crate::js_helpers::scc_groups::SccGroups;
use crate::GraphError;
use petgraph::algo;
use petgraph::graph;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(js_name = kosarajuScc)]
pub fn kosaraju_scc(graph: &DiGraph) -> SccGroups {
SccGroups::new(algo::kosaraju_scc(&graph.graph))
}

#[wasm_bindgen(js_name = tarjanScc)]
pub fn tarjan_scc(graph: &DiGraph) -> SccGroups {
SccGroups::new(algo::tarjan_scc(&graph.graph))
}

#[wasm_bindgen]
pub fn toposort(graph: &DiGraph) -> Result<Vec<usize>, JsValue> {
match algo::toposort(&graph.graph, None) {
Expand All @@ -26,6 +37,7 @@ mod tests {
use super::*;
use crate::js_helpers::test::*;
use crate::{GraphError, GraphItemType};
use graph::NodeIndex;
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
Expand All @@ -43,4 +55,46 @@ mod tests {
let expect_err = GraphError::new("Cycle detected", GraphItemType::Node, 1);
assert_eq!(sort_err.into_serde::<GraphError>().unwrap(), expect_err);
}

#[wasm_bindgen_test]
fn can_get_kosaraju_scc() {
let (mut g, _nodes, _edges) = new_test_graph2();
g.add_edge(2, 1, JsValue::NULL);
let scc_groups: SccGroups = kosaraju_scc(&g);
assert_eq!(
scc_groups.try_into_vecs().unwrap(),
vec![
vec![NodeIndex::new(6)],
vec![NodeIndex::new(5)],
vec![
NodeIndex::new(0),
NodeIndex::new(3),
NodeIndex::new(2),
NodeIndex::new(1)
],
vec![NodeIndex::new(4)]
]
);
}

#[wasm_bindgen_test]
fn can_get_tarjan_scc() {
let (mut g, _nodes, _edges) = new_test_graph2();
g.add_edge(2, 1, JsValue::NULL);
let scc_groups: SccGroups = tarjan_scc(&g);
assert_eq!(
scc_groups.try_into_vecs().unwrap(),
vec![
vec![
NodeIndex::new(3),
NodeIndex::new(1),
NodeIndex::new(2),
NodeIndex::new(0)
],
vec![NodeIndex::new(4)],
vec![NodeIndex::new(6)],
vec![NodeIndex::new(5)]
]
);
}
}
1 change: 1 addition & 0 deletions src/js_helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
//! needed in addition to the raw petgraph features so that the library is usable
//! from Javascript.

pub mod scc_groups;
pub mod test;
pub mod vec_tree;
153 changes: 153 additions & 0 deletions src/js_helpers/scc_groups.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use crate::js_helpers::vec_tree::*;

use petgraph::graph;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
#[derive(Debug)]
pub struct SccGroups {
#[wasm_bindgen(skip)]
pub vec_tree: VecTree<graph::NodeIndex, usize>,
}

impl SccGroups {
/// Create SccGroups from 2D Vec of NodeIndex, as returned by SCC algorithms.
pub fn new(data: Vec<Vec<graph::NodeIndex>>) -> Self {
Self {
vec_tree: VecTree::new2d(data, |x| x.index() as usize),
}
}

/// Attempts to extract the `Vec`s from the inner `vec_tree`, which is inside an `Rc`.
/// If this fails then self is returned.
pub fn try_into_vecs(self) -> Result<Vec<Vec<graph::NodeIndex>>, Self> {
match self.vec_tree.try_unwrap() {
Ok(vti) => match vti {
VecTreeInner::Vt2D(vt2d) => Ok(vt2d.into_vecs()),
_ => panic!("SccGroups has wrong dimensional VecTree"),
},
Err(vec_tree) => Err(Self { vec_tree }),
}
}
}

#[wasm_bindgen]
impl SccGroups {
#[wasm_bindgen(js_name = getGroup)]
pub fn get_group(&self, index: usize) -> Option<SccGroup> {
match self.vec_tree.get(&[index]) {
VecTreeElement::View(Some(view)) => Some(SccGroup { view }),
VecTreeElement::View(None) => None,
VecTreeElement::Item(_) => panic!("SccGroups::get_group returned item"),
}
}

#[wasm_bindgen(js_name = length, getter)]
pub fn len(&self) -> usize {
match self.vec_tree.get(&[]) {
VecTreeElement::View(Some(view)) => view.len(),
VecTreeElement::View(None) => 0,
VecTreeElement::Item(_) => panic!("SccGroups::len returned item"),
}
}

#[wasm_bindgen(js_name = isEmpty)]
pub fn is_empty(&self) -> bool {
self.len() == 0
}

/// Create copy of SccGroups as `Array<Array<number>>`.
/// This is a convenience method for workings with SccGroups!
/// Using native Javascript `Array`s is less memory efficient.
/// Calling this function will produce a full copy using nested `Array`
/// meaning this will greatly increase memory consumption.
#[wasm_bindgen(js_name = toArrays)]
pub fn to_arrays(&self) -> js_sys::Array {
match &*self.vec_tree.inner() {
VecTreeInner::Vt2D(vt2d) => vt2d
.data_iter()
.map(|child_vec| {
child_vec
.iter()
.map(|x| JsValue::from(x.index() as u32))
.collect::<js_sys::Array>()
})
.collect::<js_sys::Array>(),
_ => panic!("SccGroups has wrong dimensional VecTree"),
}
}
}

#[wasm_bindgen]
#[derive(Debug)]
pub struct SccGroup {
#[wasm_bindgen(skip)]
pub view: VecTreeView<graph::NodeIndex, usize>,
}

#[wasm_bindgen]
impl SccGroup {
#[wasm_bindgen(js_name = getItem)]
pub fn get_item(&self, index: usize) -> Option<usize> {
self.view.get_item(index)
}

#[wasm_bindgen(js_name = length, getter)]
pub fn len(&self) -> usize {
self.view.len()
}

#[wasm_bindgen(js_name = isEmpty)]
pub fn is_empty(&self) -> bool {
self.view.is_empty()
}
}

#[cfg(test)]
mod tests {
use super::*;
use graph::NodeIndex;
use wasm_bindgen_test::*;

fn new_test_scc_groups() -> SccGroups {
SccGroups::new(vec![
vec![
NodeIndex::new(0),
NodeIndex::new(1),
NodeIndex::new(2),
NodeIndex::new(3),
],
vec![NodeIndex::new(4)],
])
}

#[wasm_bindgen_test]
fn can_access_scc_groups() {
// JS array => [0]
let js_array_0 = js_sys::Array::of1(&JsValue::from(0u8));
// JsValue of SccGroups1
let scc_groups = JsValue::from(new_test_scc_groups());
assert!(scc_groups.is_object());
// Retrieve the "getGroup" function by name for scc_groups
let scc_groups_get_group_raw: JsValue =
js_sys::Reflect::get(&scc_groups, &JsValue::from_str("getGroup")).unwrap();
assert!(scc_groups_get_group_raw.is_function());
let scc_groups_get_group = js_sys::Function::from(scc_groups_get_group_raw);

// Call scc_groups.getGroup(0) to get first scc_group object
let scc_group0: JsValue =
js_sys::Reflect::apply(&scc_groups_get_group, &scc_groups, &js_array_0).unwrap();
assert!(scc_group0.is_object());
// Retrieve the "getItem" function by name for scc_group
let scc_group0_get_item_raw: JsValue =
js_sys::Reflect::get(&scc_group0, &JsValue::from_str("getItem")).unwrap();
assert!(scc_group0_get_item_raw.is_function());
let scc_group0_get_item = js_sys::Function::from(scc_group0_get_item_raw);

// Retrieve the first item from the first group
let scc_group0_item0: JsValue =
js_sys::Reflect::apply(&scc_group0_get_item, &scc_group0, &js_array_0).unwrap();

assert_eq!(scc_group0_item0, JsValue::from(0));
}
}
Loading