Cloning

Cloning creates a new cell that belongs to a different network from the original cell. You can use this to create many network spaces that share the same back-end code.

Creating independent networks with the same rules

As we described in DNAs, a DNA is a template for creating the ground rules for a network of peers with its own membership and database. The network is keyed by the hash of all its integrity zomes and DNA modifiers. Cloning copies an existing DNA template and changes it slightly, so that cells created from the clone execute the same integrity code but form a new network.

An agent creates a clone by choosing a DNA in a hApp by role name or DNA hash, then calling a create-clone-cell function from either the app API or the HDK, specifying at least one new DNA modifier to change the DNA’s hash. Then they must share the modifier(s) with all other peers who want to join the network, so those peers can specify the exact same modifiers in order to create an identical clone cell with an identical DNA hash.

Creating vs joining a clone network

From the perspective of using the functions we talk about in the rest of this page, there’s really no meaningful difference between creating a network and joining it. Agents ‘become the network’ together, simply by instantiating a cell from a DNA, discovering other agent cells with the same DNA hash, and beginning to communicate with each other.

Here are the three DNA modifiers and how to use them:

Network seed

When all you want to do is create a new network space, simply specify a new network seed. It’s an arbitrary string that serves no purpose but to change the DNA hash.

DNA properties

The DNA properties are constants that your integrity and coordinator zomes can access to change their behavior. Because they can be accessed and used in validation logic, they may affect the ‘meaning’ of your integrity code, which is why they are hashed into the DNA hash.

Network clashes

You can specify DNA properties without specifying a network seed, but be aware that you will find yourself in the same network as any others who have happened to create a clone with the same properties. This is intended behavior, but it might not be desired behavior — if you’re cloning in order to both modify DNA behavior and create a new network space without any existing members or data, specify a random network seed along with the DNA properties.

Origin time

While you can specify an origin time (the earliest valid timestamp for any DHT data), in practice this isn’t necessary. Holochain will just use the origin time from the original DNA.

Clone a DNA from a client

If you want to create a clone from the client side, use the AppWebsocket.prototype.createCloneCell.

This example shows a function that creates or joins a chat room using a DNA whose role in the hApp manifest is named chat. It uses the getHolochainClient helper we created in the Front End page.

Info

import type { NetworkSeed, ClonedCell } from "@holochain/client";
import { AppWebsocket } from "@holochain/client";

async function createOrJoinChat(
    // A human-readable name. We can use this in the UI.
    // When we create the clone, we pass it to Holochain.
    // It's local to the agent and doesn't affect anything functional, so
    // different agents can give the same chat room clone a different name
    // and still be able to access the room.
    name: string,
    // If a network seed is supplied, it means we're joining an existing chat
    // and have been given the network seed by the chat's creator.
    network_seed?: NetworkSeed
): Promise<ClonedCell> {
    if (!network_seed) {
        // If no network seed is passed to the function, it means we're
        // creating a new chat. Generate a unique, random network seed to
        // ensure that the network is independent from any others.
        network_seed = crypto.randomUUID();
    }

    let client = await getHolochainClient();
    return await client.createCloneCell({
        modifiers: { network_seed },
        name,
        role_name: "chat",
    });
    // Return the info about the new clone cell, so that the network seed can
    // be shared with others.
}

Clone a DNA from a coordinator zome

You can also create a clone from within your back end with the create_clone_cell host function. You need to enable the properties feature in your zome’s Cargo.toml file:

[dependencies]
hdk = { workspace = true, features = ["properties"] }

This back-end example behaves the same as the front-end example in the previous section:

use hdk::prelude::*;

#[derive(Serialize, Deserialize, Debug)]
pub struct CreateOrJoinChatInput {
    pub name: String,
    pub network_seed: Option<NetworkSeed>,
    pub chat_dna_hash: DnaHash,
}

#[hdk_extern]
pub fn create_or_join_chat(input: CreateOrJoinChatInput) -> ExternResult<ClonedCell> {
    let network_seed_bytes = input.network_seed
        .map(|s| s.as_bytes().to_vec())
        .unwrap_or(random_bytes(32)?.into_vec());

    let network_seed = std::str::from_utf8(&network_seed_bytes)
        .map_err(|e| wasm_error!(e.to_string()))?;

    let modifiers = DnaModifiersOpt::none()
        .with_network_seed(network_seed.into());

    let cell_id = CellId::new(input.chat_dna_hash, agent_info()?.agent_latest_pubkey);
    let create_clone_cell_input = CreateCloneCellInput {
        cell_id: cell_id,
        modifiers,
        membrane_proof: None,
        name: input.name.into(),
    };
    create_clone_cell(create_clone_cell_input)
}

The `create_clone_cell` host function requires the DNA hash and agent ID

Unlike the JS client’s createCloneCell API method, the HDK’s create_clone_cell host function can’t refer to a DNA by its role name. Instead, you have to specify the DNA hash and agent ID of an existing cell. Because it’s impossible to get the DNA hash of another role in the hApp using the HDK, you need to either hard-code it in the coordinator zome (which is not recommended because it leads to tight coupling) or pass it in from the client. Here’s a sample client-side function to get a DNA hash from a role name and pass it to the zome function we just defined:

import { CellType, type NetworkSeed, type ClonedCell } from "@holochain/client";

async function createOrJoinChat_zomeSide(name: string, network_seed?: NetworkSeed): Promise<ClonedCell> {
    let client = await getHolochainClient();
    let appInfo = await client.appInfo();
    // The first cell filling a role uses the 'prototype' (unmodified) DNA.
    let chat_cell = appInfo.cell_info?.["chat"]?.[0];
    if (!chat_cell) {
        throw new Error("Couldn't find a cell with the role name 'chat'.");
    }
    // The cell info type has one property, keyed by the cell type. Right now
    // `Provisioned` is the only fully supported type for a prototype cell,
    // but this will guard against the future when other provisioning
    // strategies are fully supported.
    let chat_dna_hash = chat_cell[CellType.Provisioned]?.cell_id[0]
        ?? chat_cell[CellType.Stem]?.dna
        ?? chat_cell[CellType.Cloned]?.cell_id[0];

    // This is the zome function we defined in the last code block.
    // Let's assume it's in a DNA whose role name is `chat_lobby`.
    return await client.callZome({
        role_name: "chat_lobby",
        zome_name: "chat_lobby",
        fn_name: "create_or_join_chat",
        payload: {
            name,
            network_seed,
            chat_dna_hash,
        }
    });
}

Clone limit

Remember that your hApp manifest can specify a clone limit for a given role. Any attempts to make more clones than the limit will fail. This value is per-agent: if the clone limit for a role is 3 and Alice creates clones with network seeds X, Y, and Z while Bob creates clones with network seeds W and X, Alice won’t be able to join Bob’s network W because she’s already reached her personal limit.

Get all the clones of a role

To get all the clones of a given role, use AppWebsocket.prototype.appInfo and take a look at the return value’s cell_info property, which is an object that maps roles to the cells belonging to those roles.

import { CellType, type ClonedCell } from "@holochain/client";

async function getChatCells(): Promise<Array<ClonedCell>> {
    let client = await getHolochainClient();
    let appInfo = await client.appInfo();
    let chats = appInfo.cell_info["chat"];
    if (typeof chats == "undefined") {
        throw new Error("The 'chat' role isn't defined in this hApp.");
    }

    return chats
        .filter((cell) => CellType.Cloned in cell)
        .map((cell) => cell[CellType.Cloned]);
}

Address a clone cell when calling a zome function

To use a clone cell, you can address it either by its new DNA hash or its clone ID, which is a concatenation of its role name and a clone index. This value is local to the agent’s hApp instance and is returned by the conductor when the cell has been cloned.

In the client

This example posts a message to a given chat. It assumes that any DNAs cloned from the chat role have a chat zome with a function called post_message that accepts a string and returns the message hash.

import { ActionHash, DnaHash } from "@holochain/client";

async function postMessageByCloneIndex(index: Number, message: String): Promise<ActionHash> {
    let client = await getHolochainClient();
    return await client.callZome({
        // A clone's role name is a concatenation of its parent's role name
        // and its clone index.
        role_name: `chat.${index}`,
        zome_name: "chat",
        fn_name: "post_message",
        payload: message,
    });
}

async function postMessageByDnaHash(dnaHash: DnaHash, message: String): Promise<ActionHash> {
    let client = await getHolochainClient();
    return await client.callZome({
        // No need to require a full CellId to be passed; we know the agent
        // ID will be the same for the whole app.
        cell_id: [dnaHash, client.cachedAppInfo.agent_pub_key],
        zome_name: "chat",
        fn_name: "post_message",
        payload: message,
    });
}

In a coordinator zome

The HDK’s call host function takes roughly the same arguments. This example sends a recurring status message to a given chat, perhaps triggered by a client that runs on a schedule.

use hdk::prelude::*;

#[hdk_extern]
pub fn send_status_message_to_chat_by_clone_index(index: u32) -> ExternResult<ActionHash> {
    send_status_message(CallTargetCell::OtherRole(format!("chat.{}", index)))
}

#[hdk_extern]
pub fn send_status_message_to_chat_by_dna_hash(dna_hash: DnaHash) -> ExternResult<ActionHash> {
    send_status_message(CallTargetCell::OtherCell(CellId::new(dna_hash, agent_info()?.agent_latest_pubkey)))
}

fn send_status_message(target: CallTargetCell) -> ExternResult<ActionHash> {
    let message = format!("The time is {} and I'm still running", sys_time()?);
    let response = call(
        target,
        "chat",
        "post_message".into(),
        None,
        message,
    )?;
    match response {
        ZomeCallResponse::Ok(payload) => payload.decode().map_err(|e| wasm_error!(e.to_string())),
        _ => Err(wasm_error!("Something unexpected happened: {}", response.to_string())),
    }
}

Disable a clone cell

When an agent no longer wants to be part of a network, they can disable the clone. The cell remains in the database but stops responding to zome calls and peer-to-peer network traffic. The front end can’t delete clone cells (to prevent malicious front ends from deleting data), but Holochain will clean up its data when the hApp is uninstalled. (You can also use the HDK to delete a clone cell.)

In the client

Use the AppWebsocket.prototype.disableCloneCell method to disable the cell from a front end.

import { DnaHash } from "@holochain/client";

async function pauseChatByCloneIndex(index: Number): Promise<void> {
    let client = await getHolochainClient();
    return client.disableCloneCell({
        clone_cell_id: `chat.${index}`
    });
}

async function pauseChatByDnaHash(dnaHash: DnaHash): Promise<void> {
    let client = await getHolochainClient();
    return client.disableCloneCell({
        clone_cell_id: dnaHash,
    });
}

In a coordinator zome

Use the disable_clone_cell host function to disable a cell from a coordinator zome within the same hApp.

use hdk::prelude::*;

#[hdk_extern]
pub fn pause_chat_by_clone_index(index: u32) -> ExternResult<()> {
    let clone_role_name = format!("chat.{}", index);
    let input = DisableCloneCellInput {
        clone_cell_id: CloneCellId::CloneId(CloneId(clone_role_name)),
    };
    disable_clone_cell(input)
}

#[hdk_extern]
pub fn pause_chat_by_dna_hash(dna_hash: DnaHash) -> ExternResult<()> {
    let input = DisableCloneCellInput {
        clone_cell_id: CloneCellId::DnaHash(dna_hash),
    };
    disable_clone_cell(input)
}

Re-enable a clone cell

If you’ve previously disabled a clone cell, you can re-enable it to access its functions and start participating in the network again. The input type of these functions is the same as for enabling a clone cell, and they return information about the cloned cell just like when you created it.

In the client

Use the AppWebsocket.prototype.enableCloneCell method to enable a clone cell from a front end.

import { DnaHash } from "@holochain/client";

async function restoreChatByCloneIndex(index: Number): Promise<ClonedCell> {
    let client = await getHolochainClient();
    return client.enableCloneCell({
        clone_cell_id: `chat.${index}`
    });
}

async function restoreChatByDnaHash(dnaHash: DnaHash): Promise<ClonedCell> {
    let client = await getHolochainClient();
    return client.enableCloneCell({
        clone_cell_id: dnaHash,
    });
}

In a coordinator zome

Use the enable_clone_cell host function to enable a cell from a coordinator zome within the same hApp.

use hdk::prelude::*;

#[hdk_extern]
pub fn restore_chat_by_clone_index(index: u32) -> ExternResult<ClonedCell> {
    let clone_role_name = format!("chat.{}", index);
    let input = EnableCloneCellInput {
        clone_cell_id: CloneCellId::CloneId(CloneId(clone_role_name)),
    };
    enable_clone_cell(input)
}

#[hdk_extern]
pub fn restore_chat_by_dna_hash(dna_hash: DnaHash) -> ExternResult<ClonedCell> {
    let input = EnableCloneCellInput {
        clone_cell_id: CloneCellId::DnaHash(dna_hash),
    };
    enable_clone_cell(input)
}

Delete a clone cell

While Holochain automatically deletes cell data when a hApp is uninstalled, you can also use the HDK to explicitly delete a clone with the delete_clone_cell host function. Again, the input is identical to disable_clone_cell and enable_clone_cell.

use hdk::prelude::*;

pub fn delete_chat_by_clone_index(index: u32) -> ExternResult<()> {
    let clone_role_name = format!("chat.{}", index);
    let input = DeleteCloneCellInput {
        clone_cell_id: CloneCellId::CloneId(CloneId(clone_role_name)),
    };
    delete_clone_cell(input)
}

#[hdk_extern]
pub fn delete_chat_by_dna_hash(dna_hash: DnaHash) -> ExternResult<()> {
    let input = DeleteCloneCellInput {
        clone_cell_id: CloneCellId::DnaHash(dna_hash),
    };
    delete_clone_cell(input)
}

Reference

Further reading

It looks like you are using Internet Explorer. While the basic content is available, this is no longer a supported browser by the manufacturer, and no attention is being given to having IE work well here.