Anchors & Paths

### Anchors (I) - Data in Holochain is best structured as a directed acyclic graph of linked actions, entries and public keys - From a starting point ID (action, entry or agent), we typically want to retrieve certain data related to it - For example, if our application will need to "get_comments_by_author(author: AgentPubKey)", then: - When we create the comment, - we also create a link - from the public key of the author - to the create action hash - Inside "get_coments_by_author(author: AgentPubKey)" - we get links from that agent public key - to retrieve all the comments by that author
### Anchors (II) - Problem: how do we build queries that don't have starting point? - For example: "get_all_comments()", which gets all comments that any agent has created - We can't go to all the cells and query all the comments that each cell has created as it's intractably expensive when the network grows - Solution: hard code a deterministic entry in the source code - Whenever we create a comment, we also create a link from the hash of that deterministic entry - Then we can implement "get_all_comments()" by doing a get links from that hash that everyone knows - These deterministic entries are called "anchors" - Demo: https://holochain-gym.github.io/developers/intermediate/anchors/#try-it

Anchors (III)


// Integrity
use hdi::prelude::*;

#[hdk_entry_helper]
struct Comment {
  comment: String
}

#[hdk_entry_helper]
// We only need a string
struct Anchor(String);
  
#[hdk_entry_defs]
#[unit_enum(UnitEntryTypes)]
enum EntryTypes { 
  Comment(Comment),
  Anchor(Anchor),
}

#[hdk_link_types]
enum LinkTypes { 
  AnchorToComment,
}

// Coordinator
use hdk::prelude::*;

#[hdk_extern]
fn create_comment(comment: Comment) -> ExternResult<ActionHash> {
  let comment_action_hash = create_entry(EntryTypes::Comment(comment))?;

  let anchor = Anchor(String::from("all_comments"));
  let _action_hash = create_entry(EntryTypes::Anchor(anchor))?; // This hash is not 
                                                                // deterministic: it will be 
                                                                // different every time this 
                                                                // function is executed
  
  let anchor_entry_hash = hash_entry(anchor)?; // This hash is deterministic: it will be the 
                                               // same independently of which cells executes 
                                               // this function  
  create_link(anchor_entry_hash, comment_action_hash, LinkTypes::AnchorToComment, ())?;
  Ok(comment_action_hash)
}

#[hdk_extern]
fn get_all_comments_hashes(_: ()) -> ExternResult<Vec<ActionHash>> {
  let anchor = Anchor(String::from("all_comments")); // Must use the same string as 
                                                     // the create function

  let anchor_hash = hash_entry(anchor)?;

  let links = get_links(anchor_hash, LinkTypes::AnchorToComment, None)?; // Get all the links
                                                                         // created above

  let action_hashes: Vec<ActionHash> = links.into_iter()
    .map(|link| ActionHash::from(link.target))
    .collect(); // Extract the action hash from the links
  
  Ok(action_hashes)
}  
### Paths (theory) - Anchors are great, but they can introduce problems - If a large number of links are attached to the same base, they become a burden on the agents responsible for those links - This is know as a "DHT Hotspot" - It's impractice to sort or filter an oversized result set - For example, a query of the form "get_comment_by_date(start_date: Timestamp, end_date: Timestamp)" - Paths solve these issues - Paths are built-in to the HDK - Paths are anchor trees - Top level anchor acts as the root node - This root node links to its children, which are also anchors - This tree can be dynamically constructed and can have arbitrary depth - Paths are composed of a vector of components - Each component is an arbitrary byte array - Normally constructed from a string - Demo: https://holochain-gym.github.io/developers/intermediate/paths/#try-it

Paths (code example)


use hdk::prelude::*;
use integrity_zome::*; // Import the types defined in our integrity zome
use chrono::*; // Import the chrono crate for time related utilites

#[hdk_extern]
fn create_comment(comment: Comment) -> ExternResult<ActionHash> {
  let comment_action_hash = create_entry(EntryTypes::Comment(comment))?;
  
  let (secs, nsecs) = sys_time()?.as_seconds_and_nanos(); // Get the current timestamp 
                                                          // in microseconds
  let dt: DateTime<Utc> = DateTime::from_utc(NaiveDateTime::from_timestamp(secs, nsecs), Utc);

  let path = Path::from(
    format!("all_comments.{}.{}", dt.year(), dt.month()) // The '.' is the separator 
                                                         // between components
  ); // Builds the path "all_comments.2022.7"

  let typed_path = path.typed(LinkTypes::PathTimeIndex)?; // "TypedPath" is a path with all the
                                                          // links of a certain †ype

  typed_path.ensure()?; // Creates the path tree structure, and all the necessary links
    
  create_link(
    typed_path.path_entry_hash()?, // Entry hash of the path
    comment_action_hash, 
    LinkTypes::PathToComment, 
    ()
  )?;

  Ok(comment_action_hash)
}

Paths (roll-ups)


#[hdk_extern]
fn get_comments_by_year(year: u32) -> ExternResult<Vec<ActionHash>> {
  let year_path = Path::from(
    format!("all_comments.{}", year) // Build the anchor in the intermediate node
  ).typed(LinkTypes::PathTimeIndex); 
  
  let month_paths: Vec<Path> = year_path.children_paths()?; // Retrieves all the children
                                                            // node month paths

  let mut all_links: Vec<Link> = vec![];
  for path in month_paths {
    let last_component: Component = path.leaf() // Returns an "Option<Component>" with 
                                                // the last component of this path
      .ok_or(wasm_error!(WasmErrorInner::Guest(String::from("The path is empty"))))?.clone(); 

    let month = String::try_from(last_component) // Converts the component's byte array 
                                                 // to a string
      .map_err(|err| wasm_error!(err))?;
  
    let mut links = get_links(
      path.path_entry_hash()?, LinkTypes::PathToComment, None
    )?; // Get all the links created above

    all_links.append(&mut links); // Collect the links
  }

  let all_links: Vec<ActionHash> = links.into_iter()
    .map(|link| ActionHash::from(link.target))
    .collect(); // Extract the action hash from the links
  
  Ok(action_hashes)
}

That's it!