From f1544880d0ee86c6ea8f81ad7b16b90baf77f148 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 13:33:48 -0800 Subject: [PATCH 01/33] lifecycle callbacks and zome functions pages --- .../build/lifecycle-events-and-callbacks.md | 221 ++++++++++++++++++ src/pages/build/zome-functions.md | 97 ++++++++ 2 files changed, 318 insertions(+) create mode 100644 src/pages/build/lifecycle-events-and-callbacks.md create mode 100644 src/pages/build/zome-functions.md diff --git a/src/pages/build/lifecycle-events-and-callbacks.md b/src/pages/build/lifecycle-events-and-callbacks.md new file mode 100644 index 000000000..e3c938c3d --- /dev/null +++ b/src/pages/build/lifecycle-events-and-callbacks.md @@ -0,0 +1,221 @@ +--- +title: "Lifecycle Events and Callbacks" +--- + +::: intro +A [cell](/concepts/2_application_architecture/#cell) can respond to various events in the life of a hApp by defining specially named **lifecycle callbacks**. This lets back-end code define and validate data, perform initialization tasks, respond to [remote signals](/concepts/9_signals), and follow up after successful writes. +::: + +All of the lifecycle callbacks must follow the [pattern for public functions](/build/zomes/#define-a-function) on the Zomes page. They must also have the specific input argument and return value types described below. + +## Integrity zomes + +Your [integrity zome](/build/zomes/#integrity) must define two callbacks, `validate` and `entry_defs`, and it may define an optional callback, `genesis_self_check`. All of these functions **cannot have side effects**; any attempt to write data will fail. They also cannot access data that changes over time or across participants, such as the cell's [agent ID](/build/identifiers/#agent) or a collection of [links](/build/links-paths-and-anchors/) in the [DHT](/concepts/4_dht). + + +### Define a `validate` callback + +In order to validate your data you'll need to define a `validate` callback. It must take a single argument of type [`Op`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/op/enum.Op.html) and return a value of type [`ValidateCallbackResult`](https://docs.rs/hdi/latest/hdi/prelude/enum.ValidateCallbackResult.html) wrapped in an `ExternResult`. + +The `validate` callback is called at two times: + +1. On an agent's device, when they try to author an [action](/build/working-with-data/#entries-actions-and-records-primary-data), and +2. On a peer's device, when they receive a [DHT operation](/concepts/4_dht/#a-cloud-of-witnesses) to store and serve as part of the shared database. + +The nature of validation is out of scope for this page (we'll write a page on it soon), but here's a very basic example of a validation callback that approves everything: + +```rust +#[hdk_extern] +pub fn validate(_: Op) -> ExternResult { + Ok(Valid) +} +``` + +And here's an example of one that rejects everything. You'll note that the outer result is `Ok`; you should generally reserve `Err` for unexpected failures such as inability to deserialize data. However, Holochain will treat both `Ok(Invalid)` and `Err` as invalid operations that should be rejected. + +```rust +#[hdk_extern] +pub fn validate(_: Op) -> ExternResult { + Ok(Invalid("I reject everything")) +} +``` + +### Define an `entry_defs` callback + +You don't need to define this callback by hand; you can let the `hdk_entry_types` macro do it for you. Read the [Define an entry type section](/build/entries/#define-an-entry-type) to find out how. + +### Define a `genesis_self_check` callback + +Holochain assumes that every participant in a network is able to self-validate all the data they create before storing it in their [source chain](/concepts/3_source_chain/) and publishing it to the [DHT](/concepts/4_dht/). But at **genesis** time, when their cell has just been instantiated but they haven't connected to other peers, they may not be able to fully validate their [**genesis records**](/concepts/3_source_chain/#source-chain-your-own-data-store) if their validity depends on shared data. So Holochain skips full self-validation for these records, only validating the basic structure of their [actions](/build/working-with-data/#entries-actions-and-records-primary-data). + +This creates a risk to the new participant; they may mistakenly publish malformed data and be rejected from the network. You can define a `genesis_self_check` function that checks the _content_ of genesis records before they're published. This function is limited --- it naturally doesn't have access to DHT data. But it can be a useful guard against a [membrane proof](/glossary/#membrane-proof) that the participant typed or pasted incorrectly, for example. + +`genesis_self_check` must take a single argument of type [`GenesisSelfCheckData`](https://docs.rs/hdi/latest/hdi/prelude/type.GenesisSelfCheckData.html) and return a value of type [`ValidateCallbackResult`](https://docs.rs/hdi/latest/hdi/prelude/enum.ValidateCallbackResult.html) wrapped in an `ExternResult`. + +Here's an example that checks that the membrane proof exists and is the right length: + +```rust +use hdi::prelude::{GenesisSelfCheckData, hdk_extern, ValidateCallbackResult}; + +#[hdk_extern] +pub fn genesis_self_check(data: GenesisSelfCheckData) -> ExternResult { + if let Some(membrane_proof) = data.membrane_proof { + if membrane_proof.bytes().len() == 32 { + Ok(Valid) + } + Ok(Invalid("Membrane proof is not the right length. Please check it and enter it again.")) + } + Ok(Invalid("This network needs a membrane proof to join.")) +} +``` + +## Coordinator zomes + +A [coordinator zome](/build/zomes/#coordinator) may define some optional lifecycle callbacks: `init`, `post_commit`, and `recv_remote_signal`. + +### Define an `init` callback + +If you want to run setup tasks when the cell is being initialized, define a callback function called `init` in your coordinator zome. Holochain will call it after a cell has been created for the DNA containing the zome, following the order of coordinator zomes in the DNA's manifest, calling each zome's `init` in serial rather than calling them all in parallel. + +!!! info `init` isn't called immediately on cell instantiation + +This callback is called 'lazily'; that is, it's not called _immediately_ after the cell has been instantiated. Instead, Holochain waits until the first zome function call is made, then calls `init` before calling the zome function. + +This gives a participant's Holochain runtime a little bit of time to connect to other peers, which makes various things you might want to do in `init` more likely to succeed if they depend on data in the DHT. + +You can force `init` to run eagerly by calling it as if it were a normal zome function. _Note that you can only do this in Holochain 0.5 and newer._ + +!!! + +Once `init` runs successfully for all coordinator zomes in a DNA, Holochain writes an [`InitZomesComplete` action](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/struct.InitZomesComplete.html). That ensures that this callback isn't called again. + +`init` must take an empty `()` input argument and return an [`InitCallbackResult`](https://docs.rs/hdk/latest/hdk/prelude/enum.InitCallbackResult.html) wrapped in an `ExternResult`. All zomes' `init` callbacks in a DNA must return a success result in order for cell initialization to succeed; otherwise any data written in these callbacks, along with the `InitZomesComplete` action, will be rolled back. _If any zome's init callback returns an `InitCallbackResult::Fail`, initialization will fail._ Otherwise, if any init callback returns an `InitCallbackResult::UnresolvedDependencies`, initialization will be retried at the next zome call attempt. + +Here's an `init` callback that [links](/build/links-paths-and-anchors/) the [agent's ID](/build/identifiers/#agent) to the [DNA hash](/build/identifiers/#dna) as a sort of "I'm here" note. (It assumes that you've written an integrity zome called `foo_integrity` that defines one type of link called `ParticipantRegistration`.) + +```rust +use foo_integrity::LinkTypes; +use hdk::prelude::*; + +#[hdk_extern] +pub fn init(_: ()) -> ExternResult { + let DnaInfoV2 { hash: dna_hash } = dna_info()?; + let AgentInfo { agent_latest_pubkey: my_pubkey } = agent_info()?; + create_link( + dna_hash, + my_pubkey, + LinkTypes::ParticipantRegistration, + () + )?; + + Ok(InitCallbackResult::Pass) +} +``` + +!!! info Why link the agent key to a well-known hash? + +Because there's no single source of truth in a Holochain network, it's impossible to get the full list of peers who have joined it. The above pattern is an easy way for newcomers to register themselves as active participants so others can find them. + +But the users are also the infrastructure, so this can create "hot spots" where a set of peers --- the ones responsible for storing the base address for all those links --- carry an outsized burden compared to other peers. Read the [anchors and paths section under Links, Paths, and Anchors](/build/links-paths-and-anchors/#anchors-and-paths) for more info. + +!!! + +This `init` callback also does something useful: it grants all other peers in the network permission to send messages to a participant's [remote signal receiver callback](#recv-remote-signal-callback). + +```rust +use hdk::prelude::*; + +#[hdk_extern] +pub fn init(_: ()) -> ExternResult { + let mut fns = BTreeSet::new(); + fns.insert((zome_info()?.name, "recv_remote_signal".into())); + create_cap_grant(CapGrantEntry { + tag: "".into(), + access: CapAccess::Unrestricted, + functions: GrantedFunctions::Listed(fns), + })?; + + Ok(InitCallbackResult::Pass) +} +``` + +### Define a `post_commit` callback + +After a zome function call completes, any actions that it created are validated, then written to the cell's source chain if all actions pass validation. While the function is running, nothing has been stored even if [CRUD](/build/working-with-data/#adding-and-modifying-data) function calls return `Ok`. (Read more about the [atomic, transactional nature](/build/zome-functions/#atomic-transactional-commits) of writes in a zome function call.) + +If you need to do any follow-up after a successful write beyond returning the function's return value to the caller, it's safer to do this in a lifecycle callback called `post_commit`, which is called after Holochain's [call-zome workflow](/build/zome-functions/#zome-function-call-lifecycle) successfully writes its actions to the source chain. (Function calls that don't write data won't trigger this event.) + +`post_commit` must take a single argument of type Vec<SignedActionHashed>, which contains all the actions the function call wrote, and it must return an empty `ExternResult<()>`. This callback must not write any data, but it may call other zome functions in the same cell or any other local or remote cell, and it may + +Here's an example that uses `post_commit` to tell the original author of a `Movie` entry that someone has edited it. It uses the integrity zome examples from [Entries](/build/entries/). + +```rust +use movie_integrity::{EntryTypes, Movie}; +use hdk::*; + +struct UpdateMovieInput { + original_hash: ActionHash, + data: Movie, +} + +#[hdk_extern] +pub fn update_movie(input: UpdateMovieInput) -> ExternResult { + +} +``` + +### Define a `recv_remote_signal` callback + + + +Peers in a network can send messages to each other via [remote signals](/concepts/9_signals/#remote-signals). In order to handle these signals, your coordinator zome needs to define a `recv_remote_signal` callback. Remote signals get routed from the emitting coordinator zome on Alice's machine to the same one on Bob's machine, so there's no need for a coordinator to handle message types it doesn't know about. + +`recv_remote_signal` takes a single argument of any type you like --- if your coordinator zome deals with multiple message types, consider creating an enum for all of them. It must return an empty `ExternResult<()>`, as this callback is not called as a result of direct interaction from the local agent and has nowhere to pass a return value. + +This zome function and remote signal receiver callback implement a "heartbeat" to let all network participants know who's currently online. It assumes that you'll combine the two `init` callback examples in the previous section, which set up the necessary links and permissions to make this work. + +```rust +use foo_integrity::LinkTypes; +use hdk::prelude::*; + +// We're using this type for both remote signals to other peers and local +// signals to the UI. +enum SignalType { + Heartbeat(AgentPubKey), +} + +// My UI calls this function at regular intervals to let other participants +// know I'm online. +#[hdk_extern] +pub fn heartbeat(_: ()) -> ExternResult<()> { + // Get all the registered participants from the DNA hash. + let DnaInfoV2 { hash: dna_hash } = dna_info()?; + let other_participants_keys = get_links( + GetLinksInputBuilder::try_new( + dna_hash, + LinkTypes::ParticipantRegistration + )? + .get_options(GetStrategy::Network) + .build() + )? + .filter_map(|l| l.target.into_agent_pub_key()); + + // Now send a heartbeat message to each of them. + // Holochain will send them in parallel and won't return an error for any + // failure. + let AgentInfo { agent_latest_pubkey: my_pubkey } = agent_info()?; + send_remote_signal( + SignalType::Heartbeat(my_pubkey), + other_participants_keys + ) +} + +#[hdk_extern] +pub fn recv_remote_signal(payload: SignalType) -> ExternResult<()> { + match payload { + // Pass the heartbeat along to my UI so it can update the other + // peer's online status. + SignalType::Heartbeat(peer_pubkey) => emit_signal(payload) + } +} +``` \ No newline at end of file diff --git a/src/pages/build/zome-functions.md b/src/pages/build/zome-functions.md new file mode 100644 index 000000000..ad4f23667 --- /dev/null +++ b/src/pages/build/zome-functions.md @@ -0,0 +1,97 @@ +--- +title: "Zome Functions" +--- + +::: intro +Besides [lifecycle callbacks](/build/lifecycle-callbacks), a zome defines public functions that acts as its API. These functions can read and write data, manage permissions, send signals, and call functions in other cells in the same Holochain instance or to remote peers in the same network. +::: + +As we touched on in the [Zomes page](/build/zomes/#how-a-zome-is-structured), a zome is just a WebAssembly module with some public functions. These functions have access to Holochain's host API. Some of them are [lifecycle callbacks](/build/lifecycle-events-and-callbacks/), and others are functions you create yourself to serve as your zome's API. This second kind of function is what we'll talk about in this page. + +Holochain sandboxes your zome, acting as an intermediary between it and the user's storage and UI at the local level, and between it and other peers at the network level. + +## Define a zome function + +A zome function is a public function that's tagged with the [`hdk_extern`](https://docs.rs/hdk/latest/hdk/prelude/attr.hdk_extern.html) procedural macro. It must follow the constraints described in the [Define a function section in the Zomes page](/build/zomes/#define-a-function). + +Here's a simple example of a zome function that takes a name and returns a greeting: + +```rust +use hdk::prelude::{hdk_extern, ExternResult}; + +#[hdk_extern] +pub fn say_hello(name: String) -> ExternResult { + Ok(format!("Hello {}!", name)) +} +``` + +## Atomic, transactional commits + +When a zome function wants to persist data, it stores it as [actions](/build/working-with-data/#entries-actions-and-records-primary-data) in the cell's [source chain](/build/working-with-data/#individual-state-histories-as-public-records). At the beginning of each call, Holochain prepares a **scratch space** containing the current source state, and all source chain read/write functions work on this scratch space rather than live source chain data. + +A zome function call's writes are _atomic_ and _transactional_; that is, **all the writes succeed or fail together**. If any of the actions fail validation, the scratch space is discarded and the validation error is returned to the caller. + +Zome function calls, and their transactions, can also run in parallel. A zome call transaction has one big difference from traditional database transactions: if one transaction begins, then another transaction begins and commits before the first transaction finishes, **the first transaction will roll back** and return a 'chain head moved' error to the caller. + +The possibility of a rollback means that any follow-up tasks with written data should happen in a [`post_commit` callback](/build/lifecycle-events-and-callbacks/#define-a-post-commit-callback). + +### Relaxed chain top ordering + +As you saw in step 7 above, parallel transactions can cause each other to fail and roll back. In limited cases, you can write all your data in a single transaction with **relaxed chain top ordering** to prevent rollbacks. It tries to 'rebase' all the actions in the transaction onto the _new live state_ of the source chain if another function call changed the source chain state while the function was running. + +You have to be careful, though, because the action hashes you get back from the host when you attempt to write data will be different after the rebase. If you want to try using relaxed chain top ordering, here are some guidelines: + +* Actions within the transaction shouldn't depend on each other's hashes; for instance, the hash of action 1 shouldn't be used in the data of action 2. +* Actions shouldn't depend too exactly on the snapshotted source chain state; that is, action 1 shouldn't fail validation if the action immediately before it is different from what's expected. + +You can find an example of writing data using relaxed chain top ordering on the [Entries page](/build/entries/#create-with-relaxed-chain-top-ordering). + +## Zome function call lifecycle + +A zome function call lifecycle begins when an external caller tries to call one of the functions in a zome of an active cell. The caller can be any one of: + +* Another peer in the [same DNA network](/build/application-structure/#dna), +* A client such as a UI, running on the same machine as the participant's Holochain instance, +* Another cell in the same Holochain instance, or +* The same cell. + +The caller must call the function by cell ID and zome name, and it must supply a valid [**capability claim**](/concepts/8_calls_capabilities/) in order to call the zome function. + +Here's how the **call-zome workflow** handles a zome function call: + + + +1. Check that the supplied capability claim matches a currently active capability grant. If it doesn't, return an unauthorized error to the caller. +3. Create a 'scratch space' containing a snapshot of the current state of the cell's source chain. +4. Dispatch the zome call payload to the correct cell/zome/function. At this point execution passes to the zome. + 1. The [HDK](https://crates.io/crates/hdk) attempts to deserialize the payload into the expected type. + 2. If deserialization fails, return an [`ExternResult::Err`](https://docs.rs/hdk/latest/hdk/map_extern/type.ExternResult.html#variant.Err) containing details of the error. Execution passes back to the call-zome workflow, which passes the error to the caller. + 3. The HDK passes the deserialized payload to the zome function. The function runs, calling the host API as needed. **Any functions that attempt to read from or write to the cell's source chain operate on the snapshot, not the source chain's current state.** + 4. The function returns a return value, and the HDK serializes it and passes it back to the call-zome workflow. +5. If there are no new writes in the scratch space, return the zome function's return value to the caller. +6. Generate [DHT operations](https://docs.rs/hdi/latest/hdi/prelude/enum.Op.html) from each action and dispatch them to the appropriate validation callbacks in the DNA's [integrity zome](/build/application-structure/#zome)(s). + * If the action is a [CRUD action](/build/working-with-data/#crud-metadata-graph) for application data, only the validation callback for the integrity zome that defined the data type is called. + * If the action is a system action, or a CRUD action for a system entry type, the validation callbacks in all integrity zomes in the DNA are called. + + If validation fails for _any_ of the operations, return the first validation error. +7. Check whether the source chain's snapshot in the scratch space is older than the live source chain state, which happens if another call-zome workflow started and completed while this workflow was running. + * If the snapshot is older, check if all the actions were written with relaxed chain top ordering. + * If they were, create a fresh scratch space with a snapshot of the _new_ source chain state, rebase all of the new actions on top of it, and return to step 6. + * If at least one wasn't, return a 'chain head moved' error to the caller. +8. Write the new actions to the source chain and store their corresponding DHT operations in the local DHT store. +9. Trigger the **publish** workflow in a separate thread, which will try to share the DHT operations with network peers. +10. Trigger the **post-commit** workflow in a separate thread, which will look for a [`post_commit` callback](/build/lifecycle-events-and-callbacks/#define-a-post-commit-callback) in the same zome as the called function, and pass the new actions to it. +11. Return the zome function's return value to the caller. + +## References + +* [`hdk_derive::hdk_extern`](https://docs.rs/hdk_derive/latest/hdk_derive/attr.hdk_extern.html) +* [`hdi::map_extern::ExternResult`](https://docs.rs/hdi/latest/hdi/map_extern/type.ExternResult.html) +* [`holochain_integrity_types::op::Op`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/op/enum.Op.html) + +## Further reading + +* [Core Concepts: Application Architecture](/concepts/2_application_architecture/) +* [Build Guide: Zomes](/build/zomes/) +* [Build Guide: Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) +* [Build Guide: Entries: Create with relaxed chain top ordering](/build/entries/#create-with-relaxed-chain-top-ordering) \ No newline at end of file From 8fc90cd38cba10a626d8dd10a4e8f5c7913cec46 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 13:37:40 -0800 Subject: [PATCH 02/33] add new pages to navs --- src/pages/_data/navigation/mainNav.json5 | 5 ++++- src/pages/build/application-structure.md | 2 ++ src/pages/build/index.md | 2 ++ src/pages/build/zomes.md | 8 ++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pages/_data/navigation/mainNav.json5 b/src/pages/_data/navigation/mainNav.json5 index db865b818..1ba29e462 100644 --- a/src/pages/_data/navigation/mainNav.json5 +++ b/src/pages/_data/navigation/mainNav.json5 @@ -26,7 +26,10 @@ }, { title: "Build", url: "/build/", children: [ { title: "Application Structure", url: "/build/application-structure/", children: [ - { title: "Zomes", url: "/build/zomes/" }, + { title: "Zomes", url: "/build/zomes/", children: [ + { title: "Lifecycle Events and Callbacks", url: "/build/lifecycle-events-and-callbacks/" }, + { title: "Zome Functions": url: "/build/zome-functions/" }, + ] }, ]}, { title: "Working with Data", url: "/build/working-with-data/", children: [ { title: "Identifiers", url: "/build/identifiers/" }, diff --git a/src/pages/build/application-structure.md b/src/pages/build/application-structure.md index 91e6eb2f4..b59f67790 100644 --- a/src/pages/build/application-structure.md +++ b/src/pages/build/application-structure.md @@ -7,6 +7,8 @@ title: Application Structure * Application Structure (this page) * [Zomes](/build/zomes/) --- integrity vs coordinator, how to structure and compile + * [Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) --- writing functions that respond to events in a hApp's lifecycle + * [Zome Functions](/build/zome-functions/) --- writing your hApp's back-end API * DNAs (coming soon) --- what they're used for, how to specify and bundle * hApps (coming soon) --- headless vs UI-based, how to bundle and distribute ::: diff --git a/src/pages/build/index.md b/src/pages/build/index.md index 2a0b96757..3aa59a6cd 100644 --- a/src/pages/build/index.md +++ b/src/pages/build/index.md @@ -15,6 +15,8 @@ This Build Guide organizes everything you need to know about developing Holochai ::: topic-list * [Overview](/build/application-structure/) --- an overview of Holochain's modularity and composability units * [Zomes](/build/zomes/) --- integrity vs coordinator, how to structure and compile + * [Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) --- writing functions that respond to events in a hApp's lifecycle + * [Zome Functions](/build/zome-functions/) --- writing your hApp's back-end API ::: ## Working with data diff --git a/src/pages/build/zomes.md b/src/pages/build/zomes.md index 60a2688b5..73334bc4f 100644 --- a/src/pages/build/zomes.md +++ b/src/pages/build/zomes.md @@ -2,6 +2,14 @@ title: "Zomes" --- +::: topic-list +### In this section {data-no-toc} + +* Zomes (this page) + * [Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) --- writing functions that respond to events in a hApp's lifecycle + * [Zome Functions](/build/zome-functions/) --- writing your hApp's back-end API +::: + ::: intro A **zome** (short for chromosome) is a module of executable code within a [**DNA**](/resources/glossary/#dna). It's the smallest unit of modularity in a Holochain application. ::: From ae585c06a7a4a87101142cea16fdca63063649d2 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 13:39:52 -0800 Subject: [PATCH 03/33] link up all references to two new pages --- src/pages/build/identifiers.md | 2 +- src/pages/build/zomes.md | 12 ++++++------ src/pages/concepts/3_source_chain.md | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/build/identifiers.md b/src/pages/build/identifiers.md index ca14ecb33..fbb543225 100644 --- a/src/pages/build/identifiers.md +++ b/src/pages/build/identifiers.md @@ -226,7 +226,7 @@ Read more about [entries](/build/entries/) and [links](/build/links-paths-and-an There are a few important things to know about action hashes: * You can't know an action's hash until you've written the action, because the action contains the current system time at the moment of writing. -* When you write an action, you can specify "relaxed chain top ordering". We won't go into the details here, but when you use it, the action hash may change after the function completes. +* When you write an action, you can specify "relaxed chain top ordering". We won't go into the details here (see [the section in the Zome Functions page](/build/zome-functions/#relaxed-chain-top-ordering),but when you use it, the action hash may change after the function completes. * A function that writes actions is _atomic_, which means that all writes fail or succeed together. Because of these three things, it's unsafe to depend on the value or even existence of an action hash within the same function that writes it. Here are some 'safe usage' notes: diff --git a/src/pages/build/zomes.md b/src/pages/build/zomes.md index 73334bc4f..40ce5dad1 100644 --- a/src/pages/build/zomes.md +++ b/src/pages/build/zomes.md @@ -16,7 +16,7 @@ A **zome** (short for chromosome) is a module of executable code within a [**DNA ## How a zome is structured -A zome is just a [WebAssembly module](https://webassembly.github.io/spec/core/syntax/modules.html) that exposes public functions. The **conductor** (the Holochain runtime) knows about these functions and calls them at different points in the application's lifetime. Some functions have special names and are called **lifecycle callbacks**, and you can also define arbitrarily named functions of your own to serve as your zome's API. +A zome is just a [WebAssembly module](https://webassembly.github.io/spec/core/syntax/modules.html) that exposes public functions. The **conductor** (the Holochain runtime) knows about these functions and calls them at different points in the application's lifetime. Some functions have special names and are called [lifecycle callbacks](/build/lifecycle-events-and-callbacks/), and you can also define arbitrarily named functions of your own to serve as your zome's API. For Rust developers, we've created an SDK called the [Holochain Development Kit (HDK)](https://crates.io/crates/hdk/). It lets you define functions, exchange data with the host (the Holochain VM), and access all of the host's functionality. @@ -38,7 +38,7 @@ Because these callbacks only need a small portion of Holochain's functionality, Your integrity zome tells Holochain about the types of [entries](/build/entries/) and [links](/build/links-paths-and-anchors/) it defines with macros called [`hdk_entry_types`](https://docs.rs/hdi/latest/hdi/attr.hdk_entry_types.html) and [`hdk_link_types`](https://docs.rs/hdi/latest/hdi/attr.hdk_link_types.html) added to enums of all the entry and link types. These create lifecycle callbacks that are run at DNA install time and give Holochain the info it needs. Read more in [Define an entry type](/build/entries/#define-an-entry-type) and [Define a link type](/build/links-paths-and-anchors/#define-a-link-type). -Finally, your integrity zome defines validation callbacks that check for correctness of data and actions. Holochain runs this on an agent's own device when they attempt to author data, and on other peers' devices when they're asked to store and serve data authored by others. +Finally, your integrity zome defines [validation callbacks](/build/lifecycle-events-and-callbacks/#define-a-validate-callback) that check for correctness of data and actions. Holochain runs this on an agent's own device when they attempt to author data, and on other peers' devices when they're asked to store and serve data authored by others. #### Create an integrity zome @@ -58,7 +58,7 @@ Then add some necessary dependencies to your new `Cargo.toml` file: +serde = "1.0" ``` -At the very minimum, make sure your code exposes a `validate` callback and [defines some entry types](/build/entries/#define-an-entry-type). +At the very minimum, make sure your code exposes a [`validate` callback](/build/lifecycle-events-and-callbacks/#define-a-validate-callback) and [defines some entry types](/build/entries/#define-an-entry-type). Compile your zome using `cargo`: @@ -68,7 +68,7 @@ cargo build --release --target wasm32-unknown-unknown ### Coordinator -Coordinator zomes hold your back-end logic --- the functions that read and write data or communicate with peers. In addition to some optional, specially named lifecycle callbacks , you can also write your own **zome functions** that serve as your zome's API. +Coordinator zomes hold your back-end logic --- the functions that read and write data or communicate with peers. In addition to some optional, specially named [lifecycle callbacks](/build/lifecycle-events-and-callbacks/#coordinator-zomes), you can also write your own **zome functions** that serve as your zome's API. #### Create a coordinator zome @@ -146,7 +146,7 @@ pub fn check_age_for_18a_movie(age: u32) -> ExternResult<()> { ## Further reading - - +* [Build Guide: Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) +* [Build Guide: Zome Functions](/build/zome-functions/) * [WebAssembly](https://webassembly.org/) * [serde](https://serde.rs/) diff --git a/src/pages/concepts/3_source_chain.md b/src/pages/concepts/3_source_chain.md index 8248c74e7..bd8ac35d2 100644 --- a/src/pages/concepts/3_source_chain.md +++ b/src/pages/concepts/3_source_chain.md @@ -122,7 +122,7 @@ Holochain's answer is simple --- _somebody will notice_. More on that in the nex * Data is stored in the source chain as records, which consist of actions and sometimes entries. * Data can be linked together. * Every entry and link has a type, validated by a function that checks its integrity in the context of its place in the source chain. -* The first four entries are called genesis entries, and are special system types that contain the DNA hash, the agent's membrane proof, their public key, and an init-complete marker. +* The first four records on a source chain are called genesis records, and are special system types that contain the DNA hash, the agent's membrane proof, their public key, and an init-complete marker. * Entries are just binary data, and MessagePack is a good way to give them structure. * The source chain and all of its data is tamper-evident; validators can detect third-party attempts to modify it. From e20390ee2c52fac2dae2bb6d2dc82a99ae98daf3 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 13:40:11 -0800 Subject: [PATCH 04/33] remove redundant descriptions of lifecycles, add examples for relaxed ordering --- src/pages/build/entries.md | 153 +++++++++++++++++++++++++------------ 1 file changed, 106 insertions(+), 47 deletions(-) diff --git a/src/pages/build/entries.md b/src/pages/build/entries.md index d5f5a651e..7c9f42866 100644 --- a/src/pages/build/entries.md +++ b/src/pages/build/entries.md @@ -119,36 +119,59 @@ let create_action_hash = create_entry( )?; ``` +### Create with relaxed chain top ordering + +If your entry doesn't have any dependencies on other data, you can use [relaxed chain top ordering](/build/zome-functions/#relaxed-chain-top-ordering) to prevent possible transaction rollbacks (we'll let that page explain when this could happen and how to design around it). + +To use this feature, you'll need to use the more low-level [`create`](https://docs.rs/hdk/latest/hdk/entry/fn.create.html) host function, which requires you to build a more complex input. This example batches updates to director entries, which don't have reference other data including each other, so they're a good candidate for relaxed ordering. + +```rust +use movie_integrity::{Director, EntryTypes}; +use hdk::prelude::*; + +let directors = vec![/* construct a vector of `Director` structs here */]; +for director in directors.iter() { + // To specify chain top ordering other than the default Strict, we + // need to use the `create` host function which requires a bit more + // setup. + let entry = EntryTypes::Director(director); + let ScopedEntryDefIndex { + zome_index, + zome_type: entry_def_index, + } = (&entry).try_into()?; + let visibility = EntryVisibility::from(&entry); + let create_input = CreateInput::new( + EntryDefLocation::app(zome_index, entry_def_index), + visibility, + entry.try_into()?, + ChainTopOrdering::Relaxed, + ); + create(create_input))?; +} +``` + ### Create under the hood -When the client calls a zome function that calls `create_entry`, Holochain does the following: +When a zome function calls `create`, Holochain does the following: -1. Prepare a **scratch space** for making an atomic set of changes to the source chain for the agent's cell. -2. Build an entry creation action called [`Create`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/struct.Create.html) that includes: +1. Build an entry creation action called [`Create`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/struct.Create.html) that includes: * the author's public key, * a timestamp, * the action's sequence in the source chain and the previous action's hash, * the entry type (integrity zome index and entry type index), and * the hash of the serialized entry data. -3. Write the `Create` action and the serialized entry data to the scratch space. -4. Return the `ActionHash` of the `Create` action to the calling zome function. (At this point, the action hasn't been persisted to the source chain.) -5. Wait for the zome function to complete. -6. Convert the action to DHT operations. -7. Run the validation callback for all DHT operations. - * If successful, continue. - * If unsuccessful, return the validation error to the client instead of the zome function's return value. -8. Compare the scratch space against the actual state of the source chain. - * If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a `HeadMoved` error is returned to the caller. - * If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is 'rebased' on top of the new source chain state as it's being written. - * If the source chain has not diverged, the data in the scratch space is written to the source chain state. -9. Return the zome function's return value to the client. -10. In the background, publish all newly created DHT operations to their respective authority agents. +2. Write the `Create` action and the serialized entry data to the scratch space. +3. Return the `ActionHash` of the pending `Create` action to the calling zome function. + +At this point, the action hasn't been persisted to the source chain. Read the [zome function call lifecycle](/build/zome-functions/#zome-function-call-lifecycle) section to find out more about persistence. ## Update an entry Update an entry creation action by calling [`hdk::prelude::update_entry`](https://docs.rs/hdk/latest/hdk/prelude/fn.update_entry.html) with the old action hash and the new entry data: + + ```rust use hdk::prelude::*; use movie_integrity::*; @@ -174,28 +197,57 @@ let update_action_hash = update_entry( An [`Update` action](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/struct.Update.html) operates on an entry creation action (either a `Create` or an `Update`), not just an entry by itself. It also doesn't remove the original data from the DHT; instead, it gets attached to both the original entry and its entry creation action. As an entry creation action itself, it references the hash of the new entry so it can be retrieved from the DHT. +### Update with relaxed chain top ordering + +If you want to use relaxed chain top ordering, use the low-level [`update`](https://docs.rs/hdk/latest/hdk/entry/fn.update.html) instead: + +```rust +use hdk::prelude::*; +use movie_integrity::*; +use chrono::DateTime; + +// A simple struct to keep a mapping to an old director action hash to new +// entry content. +struct OldToNewDirector { + old_action_hash: ActionHash, + new_entry: Director, +} + +let old_to_new_directors = vec![ + /* construct a vector of old director action hashes and updated content */ +]; + +for director in old_to_new_directors.iter() { + // To specify chain top ordering other than the default Strict, we + // need to use the `create` host function which requires a bit more + // setup. + let entry = EntryTypes::Director(&director.new_entry); + let ScopedEntryDefIndex { + zome_index, + zome_type: entry_def_index, + } = (&entry).try_into()?; + let visibility = EntryVisibility::from(&entry); + let update_input: UpdateInput = { + original_action_address: &director.old_action_hash, + entry: entry.try_into()?, + chain_top_ordering: ChainTopOrdering::Relaxed, + }; + update(update_input)?; +} +``` + ### Update under the hood -Calling `update_entry` does the following: +When a zome function calls `create`, Holochain does the following: -1. Prepare a **scratch space** for making an atomic set of changes to the source chain for the agent's cell. -2. Build an `Update` action that contains everything in a `Create` action, plus: +1. Build an entry creation action called `Update` that contains everything in a `Create` action, plus: * the hash of the original action and * the hash of the original action's serialized entry data. - (Note that the entry type is automatically retrieved from the original action.) -3. Write an `Update` action to the scratch space. -4. Return the `ActionHash` of the `Update` action to the calling zome function. (At this point, the action hasn't been persisted to the source chain.) -5. Wait for the zome function to complete. -6. Convert the action to DHT operations. -7. Run the validation callback for all DHT operations. - * If successful, continue. - * If unsuccessful, return the validation error to the client instead of the zome function's return value. -8. Compare the scratch space against the actual state of the source chain. - * If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a `HeadMoved` error is returned to the caller. - * If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is 'rebased' on top of the new source chain state as it's being written. - * If the source chain has not diverged, the data in the scratch space is written to the source chain state. -9. Return the zome function's return value to the client. -10. In the background, publish all newly created DHT operations to their respective authority agents. + (Note that the entry type and visibility are automatically retrieved from the original action.) +2. Write the `Update` action and the serialized entry data to the scratch space. +3. Return the `ActionHash` of the pending `Update` action to the calling zome function. + +As with `Create`, the action hasn't been persisted to the source chain yet. Read the [zome function call lifecycle](/build/zome-functions/#zome-function-call-lifecycle) section to find out more about persistence. ### Update patterns @@ -248,24 +300,31 @@ In the future we plan to include a 'purge' functionality. This will give agents Remember that, even once purge is implemented, it is impossible to force another person to delete data once they have seen it. Be deliberate about choosing what data becomes public in your app. +### Delete with relaxed chain top ordering + +To delete with relaxed chain top ordering, use the low-level [`delete`](https://docs.rs/hdk/latest/hdk/entry/fn.delete.html) instead. + +```rust +use hdk::prelude::*; + +let actions_to_delete: Vec = vec![/* construct vector here */]; +for action in actions_to_delete.iter() { + let delete_input: DeleteInput = { + deletes_action_hash: action, + chain_top_ordering: ChainTopOrdering::Relaxed, + } + delete(delete_input)?; +} +``` + ### Delete under the hood Calling `delete_entry` does the following: -1. Prepare a **scratch space** for making an atomic set of changes to the source chain for the agent's cell. -2. Write a `Delete` action to the scratch space. -3. Return the `ActionHash` of the `Delete` action to the calling zome function. (At this point, the action hasn't been persisted to the source chain.) -4. Wait for the zome function to complete. -5. Convert the action to DHT operations. -6. Run the validation callback for all DHT operations. - * If successful, continue. - * If unsuccessful, return the validation error to the client instead of the zome function's return value. -7. Compare the scratch space against the actual state of the source chain. - * If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a `HeadMoved` error is returned to the caller. - * If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is 'rebased' on top of the new source chain state as it's being written. - * If the source chain has not diverged, the data in the scratch space is written to the source chain state. -8. Return the zome function's return value to the client. -9. In the background, publish all newly created DHT operations to their respective authority agents. +1. Write a `Delete` action to the scratch space. +2. Return the pending `ActionHash` of the `Delete` action to the calling zome function. + +As with `Create` and `Delete`, the action hasn't been persisted to the source chain yet. Read the [zome function call lifecycle](/build/zome-functions/#zome-function-call-lifecycle) section to find out more about persistence. ## Identifiers on the DHT From f16a4a542ef4416313170d3e6332665caedf6d6d Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 13:44:07 -0800 Subject: [PATCH 05/33] add under-the-hood for CreateLink and DeleteLink --- src/pages/build/links-paths-and-anchors.md | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/pages/build/links-paths-and-anchors.md b/src/pages/build/links-paths-and-anchors.md index 66354a7d9..051aba523 100644 --- a/src/pages/build/links-paths-and-anchors.md +++ b/src/pages/build/links-paths-and-anchors.md @@ -73,6 +73,21 @@ let create_link_action_hash = create_link( Links can't be updated; they can only be created or deleted. Multiple links with the same base, target, type, and tag can be created, and they'll be considered separate links for retrieval and deletion purposes. +### Creating a link, under the hood + +When a zome function calls `create_link`, Holochain does the following: + +1. Build an action called [`CreateLink`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/struct.CreateLink.html) that includes: + * the author's public key, + * a timestamp, + * the action's sequence in the source chain and the previous action's hash, and + * the link type, base, target, and tag. + +2. Write the action to the scratch space. +3. Return the `ActionHash` of the pending action to the calling zome function. + +At this point, the action hasn't been persisted to the source chain. Read the [zome function call lifecycle](/build/zome-functions/#zome-function-call-lifecycle) section to find out more about persistence. + ## Delete a link Delete a link by calling [`hdk::prelude::delete_link`](https://docs.rs/hdk/latest/hdk/link/fn.delete_link.html) with the create-link action's hash. @@ -87,6 +102,21 @@ let delete_link_action_hash = delete_link( A link is considered ["dead"](/build/working-with-data/#deleted-dead-data) (deleted but retrievable if asked for explicitly) once its creation action has at least one delete-link action associated with it. As with entries, dead links can still be retrieved with [`hdk::prelude::get_details`](https://docs.rs/hdk/latest/hdk/prelude/fn.get_details.html) or [`hdk::prelude::get_link_details`](https://docs.rs/hdk/latest/hdk/link/fn.get_link_details.html) (see next section). +### Deleting a link, under the hood + +When a zome function calls `delete_link`, Holochain does the following: + +1. Build an action called [`DeleteLink`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/struct.DeleteLink.html) that includes: + * the author's public key, + * a timestamp, + * the action's sequence in the source chain and the previous action's hash, and + * the link type, base, target, and tag. + +2. Write the action to the scratch space. +3. Return the `ActionHash` of the pending action to the calling zome function. + +At this point, the action hasn't been persisted to the source chain. Read the [zome function call lifecycle](/build/zome-functions/#zome-function-call-lifecycle) section to find out more about persistence. + ## Retrieve links Get all the _live_ (undeleted) links attached to a hash with the [`hdk::prelude::get_links`](https://docs.rs/hdk/latest/hdk/link/fn.get_links.html) function. The input is complicated, so use [`hdk::link::builder::GetLinksInputBuilder`](https://docs.rs/hdk/latest/hdk/link/builder/struct.GetLinksInputBuilder.html) to build it. From cd3eee3f547f5d0e4b3222161824916175eb8223 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 13:48:53 -0800 Subject: [PATCH 06/33] add snapshotted to dict --- .cspell/words-that-should-exist.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.cspell/words-that-should-exist.txt b/.cspell/words-that-should-exist.txt index 0eeef0b29..c1ff70429 100644 --- a/.cspell/words-that-should-exist.txt +++ b/.cspell/words-that-should-exist.txt @@ -25,6 +25,7 @@ runtimes sandboxed sandboxing scaffolder +snapshotted spacebar todo todos From 2ccb02d5f55aa10e2a469864a910436fed3e1d22 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 13:49:45 -0800 Subject: [PATCH 07/33] fix broken JSON --- src/pages/_data/navigation/mainNav.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/_data/navigation/mainNav.json5 b/src/pages/_data/navigation/mainNav.json5 index 1ba29e462..3575ce223 100644 --- a/src/pages/_data/navigation/mainNav.json5 +++ b/src/pages/_data/navigation/mainNav.json5 @@ -28,7 +28,7 @@ { title: "Application Structure", url: "/build/application-structure/", children: [ { title: "Zomes", url: "/build/zomes/", children: [ { title: "Lifecycle Events and Callbacks", url: "/build/lifecycle-events-and-callbacks/" }, - { title: "Zome Functions": url: "/build/zome-functions/" }, + { title: "Zome Functions", url: "/build/zome-functions/" }, ] }, ]}, { title: "Working with Data", url: "/build/working-with-data/", children: [ From 5cb51e18091811d1d49f9cd5b98e702dc9e57864 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 13:53:49 -0800 Subject: [PATCH 08/33] fix broken links --- src/pages/build/lifecycle-events-and-callbacks.md | 4 ++-- src/pages/build/zome-functions.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/build/lifecycle-events-and-callbacks.md b/src/pages/build/lifecycle-events-and-callbacks.md index e3c938c3d..e54344e19 100644 --- a/src/pages/build/lifecycle-events-and-callbacks.md +++ b/src/pages/build/lifecycle-events-and-callbacks.md @@ -48,7 +48,7 @@ You don't need to define this callback by hand; you can let the `hdk_entry_types Holochain assumes that every participant in a network is able to self-validate all the data they create before storing it in their [source chain](/concepts/3_source_chain/) and publishing it to the [DHT](/concepts/4_dht/). But at **genesis** time, when their cell has just been instantiated but they haven't connected to other peers, they may not be able to fully validate their [**genesis records**](/concepts/3_source_chain/#source-chain-your-own-data-store) if their validity depends on shared data. So Holochain skips full self-validation for these records, only validating the basic structure of their [actions](/build/working-with-data/#entries-actions-and-records-primary-data). -This creates a risk to the new participant; they may mistakenly publish malformed data and be rejected from the network. You can define a `genesis_self_check` function that checks the _content_ of genesis records before they're published. This function is limited --- it naturally doesn't have access to DHT data. But it can be a useful guard against a [membrane proof](/glossary/#membrane-proof) that the participant typed or pasted incorrectly, for example. +This creates a risk to the new participant; they may mistakenly publish malformed data and be rejected from the network. You can define a `genesis_self_check` function that checks the _content_ of genesis records before they're published. This function is limited --- it naturally doesn't have access to DHT data. But it can be a useful guard against a [membrane proof](/resources/glossary/#membrane-proof) that the participant typed or pasted incorrectly, for example. `genesis_self_check` must take a single argument of type [`GenesisSelfCheckData`](https://docs.rs/hdi/latest/hdi/prelude/type.GenesisSelfCheckData.html) and return a value of type [`ValidateCallbackResult`](https://docs.rs/hdi/latest/hdi/prelude/enum.ValidateCallbackResult.html) wrapped in an `ExternResult`. @@ -120,7 +120,7 @@ But the users are also the infrastructure, so this can create "hot spots" where !!! -This `init` callback also does something useful: it grants all other peers in the network permission to send messages to a participant's [remote signal receiver callback](#recv-remote-signal-callback). +This `init` callback also does something useful: it grants all other peers in the network permission to send messages to a participant's [remote signal receiver callback](#define-a-recv-remote-signal-callback). ```rust use hdk::prelude::*; diff --git a/src/pages/build/zome-functions.md b/src/pages/build/zome-functions.md index ad4f23667..4d3eed14d 100644 --- a/src/pages/build/zome-functions.md +++ b/src/pages/build/zome-functions.md @@ -3,7 +3,7 @@ title: "Zome Functions" --- ::: intro -Besides [lifecycle callbacks](/build/lifecycle-callbacks), a zome defines public functions that acts as its API. These functions can read and write data, manage permissions, send signals, and call functions in other cells in the same Holochain instance or to remote peers in the same network. +Besides [lifecycle callbacks](/build/lifecycle-events-and-callbacks), a zome defines public functions that acts as its API. These functions can read and write data, manage permissions, send signals, and call functions in other cells in the same Holochain instance or to remote peers in the same network. ::: As we touched on in the [Zomes page](/build/zomes/#how-a-zome-is-structured), a zome is just a WebAssembly module with some public functions. These functions have access to Holochain's host API. Some of them are [lifecycle callbacks](/build/lifecycle-events-and-callbacks/), and others are functions you create yourself to serve as your zome's API. This second kind of function is what we'll talk about in this page. From 60cf3469b2b69066acb4ab9f0491502c5a3a12eb Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 15:39:41 -0800 Subject: [PATCH 09/33] fix hdk_entry_defs, whoops --- src/pages/build/entries.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/build/entries.md b/src/pages/build/entries.md index 7c9f42866..c967c6338 100644 --- a/src/pages/build/entries.md +++ b/src/pages/build/entries.md @@ -43,13 +43,13 @@ pub struct Movie { This implements a host of [`TryFrom` conversions](https://docs.rs/hdi/latest/src/hdi/entry.rs.html#120-209) that your type is expected to implement, along with serialization and deserialization functions. -In order to dispatch validation to the proper integrity zome, Holochain needs to know about all the entry types that your integrity zome defines. This is done by implementing a callback in your zome called `entry_defs`, but it's easier to use the [`hdi::prelude::hdk_entry_defs`](https://docs.rs/hdi/latest/hdi/prelude/attr.hdk_entry_defs.html) macro on an enum of all the entry types: +In order to dispatch validation to the proper integrity zome, Holochain needs to know about all the entry types that your integrity zome defines. This is done by implementing a callback in your zome called `entry_defs`, but it's easier to use the [`hdi::prelude::hdk_entry_types`](https://docs.rs/hdi/latest/hdi/prelude/attr.hdk_entry_types.html) macro on an enum of all the entry types: ```rust use hdi::prelude::*; -#[hdk_entry_defs] -// This macro is required by hdk_entry_defs. +#[hdk_entry_types] +// This macro is required by hdk_entry_types. #[unit_enum(UnitEntryTypes)] enum EntryTypes { Director(Director), @@ -70,7 +70,7 @@ Each variant in the enum should hold the Rust type that corresponds to it, and i ```rust use hdi::prelude::*; -#[hdk_entry_defs] +#[hdk_entry_types] #[unit_enum(UnitEntryTypes)] enum EntryTypes { Director(Director), @@ -91,7 +91,7 @@ enum EntryTypes { Most of the time you'll want to define your create, read, update, and delete (CRUD) functions in a [**coordinator zome**](/resources/glossary/#coordinator-zome) rather than the integrity zome that defines it. This is because a coordinator zome is easier to update in the wild than an integrity zome. -Create an entry by calling [`hdk::prelude::create_entry`](https://docs.rs/hdk/latest/hdk/entry/fn.create_entry.html). If you used `hdk_entry_helper` and `hdk_entry_defs` macro in your integrity zome (see [Define an entry type](#define-an-entry-type)), you can use the entry types enum you defined, and the entry will be serialized and have the correct integrity zome and entry type indexes added to it. +Create an entry by calling [`hdk::prelude::create_entry`](https://docs.rs/hdk/latest/hdk/entry/fn.create_entry.html). If you used `hdk_entry_helper` and `hdk_entry_types` macro in your integrity zome (see [Define an entry type](#define-an-entry-type)), you can use the entry types enum you defined, and the entry will be serialized and have the correct integrity zome and entry type indexes added to it. ```rust use hdk::prelude::*; @@ -113,7 +113,7 @@ let movie = Movie { let create_action_hash = create_entry( // The value you pass to `create_entry` needs a lot of traits to tell // Holochain which entry type from which integrity zome you're trying to - // create. The `hdk_entry_defs` macro will have set this up for you, so all + // create. The `hdk_entry_types` macro will have set this up for you, so all // you need to do is wrap your movie in the corresponding enum variant. &EntryTypes::Movie(movie), )?; @@ -463,7 +463,7 @@ There are some community-maintained libraries that offer opinionated and high-le ## Reference * [`hdi::prelude::hdk_entry_helper`](https://docs.rs/hdi/latest/hdi/attr.hdk_entry_helper.html) -* [`hdi::prelude::hdk_entry_defs`](https://docs.rs/hdi/latest/hdi/prelude/attr.hdk_entry_defs.html) +* [`hdi::prelude::hdk_entry_types`](https://docs.rs/hdi/latest/hdi/prelude/attr.hdk_entry_types.html) * [`hdi::prelude::entry_def`](https://docs.rs/hdi/latest/hdi/prelude/entry_def/index.html) * [`hdk::prelude::create_entry`](https://docs.rs/hdk/latest/hdk/entry/fn.create_entry.html) * [`hdk::prelude::update_entry`](https://docs.rs/hdk/latest/hdk/entry/fn.update_entry.html) From 060a7d862e59f6a929793cbd0839d518a8d88145 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 15 Jan 2025 15:40:08 -0800 Subject: [PATCH 10/33] add example for post_commit --- .../build/lifecycle-events-and-callbacks.md | 112 ++++++++++++++---- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/src/pages/build/lifecycle-events-and-callbacks.md b/src/pages/build/lifecycle-events-and-callbacks.md index e54344e19..54eac2b60 100644 --- a/src/pages/build/lifecycle-events-and-callbacks.md +++ b/src/pages/build/lifecycle-events-and-callbacks.md @@ -139,31 +139,6 @@ pub fn init(_: ()) -> ExternResult { } ``` -### Define a `post_commit` callback - -After a zome function call completes, any actions that it created are validated, then written to the cell's source chain if all actions pass validation. While the function is running, nothing has been stored even if [CRUD](/build/working-with-data/#adding-and-modifying-data) function calls return `Ok`. (Read more about the [atomic, transactional nature](/build/zome-functions/#atomic-transactional-commits) of writes in a zome function call.) - -If you need to do any follow-up after a successful write beyond returning the function's return value to the caller, it's safer to do this in a lifecycle callback called `post_commit`, which is called after Holochain's [call-zome workflow](/build/zome-functions/#zome-function-call-lifecycle) successfully writes its actions to the source chain. (Function calls that don't write data won't trigger this event.) - -`post_commit` must take a single argument of type Vec<SignedActionHashed>, which contains all the actions the function call wrote, and it must return an empty `ExternResult<()>`. This callback must not write any data, but it may call other zome functions in the same cell or any other local or remote cell, and it may - -Here's an example that uses `post_commit` to tell the original author of a `Movie` entry that someone has edited it. It uses the integrity zome examples from [Entries](/build/entries/). - -```rust -use movie_integrity::{EntryTypes, Movie}; -use hdk::*; - -struct UpdateMovieInput { - original_hash: ActionHash, - data: Movie, -} - -#[hdk_extern] -pub fn update_movie(input: UpdateMovieInput) -> ExternResult { - -} -``` - ### Define a `recv_remote_signal` callback @@ -218,4 +193,91 @@ pub fn recv_remote_signal(payload: SignalType) -> ExternResult<()> { SignalType::Heartbeat(peer_pubkey) => emit_signal(payload) } } +``` + +### Define a `post_commit` callback + +After a zome function call completes, any actions that it created are validated, then written to the cell's source chain if all actions pass validation. While the function is running, nothing has been stored even if [CRUD](/build/working-with-data/#adding-and-modifying-data) function calls return `Ok`. (Read more about the [atomic, transactional nature](/build/zome-functions/#atomic-transactional-commits) of writes in a zome function call.) That means that any follow-up you do within the same function, like pinging other peers, might point to data that doesn't exist if the function fails at a later step. + +If you need to do any follow-up, it's safer to do this in a lifecycle callback called `post_commit`, which is called after Holochain's [call-zome workflow](/build/zome-functions/#zome-function-call-lifecycle) successfully writes its actions to the source chain. (Function calls that don't write data won't trigger this event.) + +`post_commit` must take a single argument of type Vec<SignedActionHashed>, which contains all the actions the function call wrote, and it must return an empty `ExternResult<()>`. This callback must not write any data, but it may call other zome functions in the same cell or any other local or remote cell, and it may send local or remote signals. + +Here's an example that uses `post_commit` to tell someone a movie loan has been created for them. It uses the integrity zome examples from [Identifiers](/build/identifiers/#in-dht-data). + +```rust +use movie_integrity::{EntryTypes, Movie, UnitEntryTypes}; +use hdk::*; + +enum RemoteSignalType { + MovieLoanHasBeenCreatedForYou(ActionHash), +} + +#[hdk_extern] +pub fn post_commit(actions: Vec) -> ExternResult<()> { + for action in actions.iter() { + // Only handle cases where an entry is being created. + if let Action::Create(create) = action.action() { + let movie_loan = get_movie_loan(action.action_address())?; + send_remote_signal( + RemoteSignalType::MovieLoanHasBeenCreatedForYou(action.action_address()), + vec![movie_loan.lent_to] + ); + } + } +} + +enum LocalSignalType { + NewMovieLoan(MovieLoan), +} + +#[hdk_extern] +pub fn recv_remote_signal(payload: RemoteSignalType) -> ExternResult<()> { + if let MovieLoanHasBeenCreatedForYou(action_hash) = payload { + let movie_loan = get_movie_loan(action_hash)?; + // Send the new movie loan data to the borrower's UI! + emit_signal(LocalSignalType::NewMovieLoan(movie_loan))?; + } +} + +fn get_movie_loan(action_hash: ActionHash) -> ExternResult { + let maybe_record = get( + action_hash, + GetOptions::network() + )?; + + if let Some(record) = maybe_record { + if let Some(movie_loan) = record.entry().to_app_option()? { + Ok(movie_loan) + } else { + Err(wasm_error!("Entry wasn't a movie loan")) + } + } else { + Err(wasm_error!("Couldn't retrieve movie loan")) + } +} + +struct UpdateMovieInput { + original_hash: ActionHash, + data: Movie, +} + +#[hdk_extern] +pub fn update_movie(input: UpdateMovieInput) -> ExternResult { + let maybe_original_record = get( + input.original_hash, + GetOptions::network() + )?; + match maybe_original_record { + // We don't need to know the contents of the original; we just need + // to know it exists before trying to update it. + Some(_) => { + update_entry( + input.original_hash, + &EntryTypes::Movie(input.data) + )? + } + None => Err(wasm_error!("Original movie record not found")), + } +} ``` \ No newline at end of file From 1010a44e02085a8870361b02c59f85dd030fdcab Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 16 Jan 2025 08:36:50 -0800 Subject: [PATCH 11/33] simplify/elaborate post_commit example --- src/pages/build/lifecycle-events-and-callbacks.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/build/lifecycle-events-and-callbacks.md b/src/pages/build/lifecycle-events-and-callbacks.md index 54eac2b60..1a29eff5c 100644 --- a/src/pages/build/lifecycle-events-and-callbacks.md +++ b/src/pages/build/lifecycle-events-and-callbacks.md @@ -241,13 +241,11 @@ pub fn recv_remote_signal(payload: RemoteSignalType) -> ExternResult<()> { } fn get_movie_loan(action_hash: ActionHash) -> ExternResult { - let maybe_record = get( + if let Some(record) = get( action_hash, GetOptions::network() - )?; - - if let Some(record) = maybe_record { - if let Some(movie_loan) = record.entry().to_app_option()? { + )? { + if let Some(movie_loan) = record.entry().to_app_option()? { Ok(movie_loan) } else { Err(wasm_error!("Entry wasn't a movie loan")) @@ -269,8 +267,10 @@ pub fn update_movie(input: UpdateMovieInput) -> ExternResult { GetOptions::network() )?; match maybe_original_record { - // We don't need to know the contents of the original; we just need - // to know it exists before trying to update it. + // We don't need to know the contents of the original; we just need to + // know it exists before trying to update it. + // A more robust app would at least check that the original was of the + // correct type. Some(_) => { update_entry( input.original_hash, From 51039884a5180382707b3c69d38f0cdcdef823d5 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 17 Jan 2025 13:30:46 -0800 Subject: [PATCH 12/33] improve language around callbacks and lifecycle hooks, plus a couple tiny edits --- src/pages/_data/navigation/mainNav.json5 | 2 +- src/pages/build/application-structure.md | 2 +- ...ks.md => callbacks-and-lifecycle-hooks.md} | 34 +++++++++++-------- src/pages/build/index.md | 2 +- src/pages/build/zome-functions.md | 10 +++--- src/pages/build/zomes.md | 12 +++---- src/pages/concepts/3_source_chain.md | 2 +- 7 files changed, 34 insertions(+), 30 deletions(-) rename src/pages/build/{lifecycle-events-and-callbacks.md => callbacks-and-lifecycle-hooks.md} (78%) diff --git a/src/pages/_data/navigation/mainNav.json5 b/src/pages/_data/navigation/mainNav.json5 index 3575ce223..52c82c1f6 100644 --- a/src/pages/_data/navigation/mainNav.json5 +++ b/src/pages/_data/navigation/mainNav.json5 @@ -27,7 +27,7 @@ { title: "Build", url: "/build/", children: [ { title: "Application Structure", url: "/build/application-structure/", children: [ { title: "Zomes", url: "/build/zomes/", children: [ - { title: "Lifecycle Events and Callbacks", url: "/build/lifecycle-events-and-callbacks/" }, + { title: "Lifecycle Events and Callbacks", url: "/build/callbacks-and-lifecycle-hooks/" }, { title: "Zome Functions", url: "/build/zome-functions/" }, ] }, ]}, diff --git a/src/pages/build/application-structure.md b/src/pages/build/application-structure.md index c0533e659..e324c4929 100644 --- a/src/pages/build/application-structure.md +++ b/src/pages/build/application-structure.md @@ -7,7 +7,7 @@ title: Application Structure * Application Structure (this page) * [Zomes](/build/zomes/) --- integrity vs coordinator, how to structure and compile - * [Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) --- writing functions that respond to events in a hApp's lifecycle + * [Lifecycle Events and Callbacks](/build/callbacks-and-lifecycle-hooks/) --- writing functions that respond to events in a hApp's lifecycle * [Zome Functions](/build/zome-functions/) --- writing your hApp's back-end API * DNAs (coming soon) --- what they're used for, how to specify and bundle * hApps (coming soon) --- headless vs UI-based, how to bundle and distribute diff --git a/src/pages/build/lifecycle-events-and-callbacks.md b/src/pages/build/callbacks-and-lifecycle-hooks.md similarity index 78% rename from src/pages/build/lifecycle-events-and-callbacks.md rename to src/pages/build/callbacks-and-lifecycle-hooks.md index 1a29eff5c..1cb3ab765 100644 --- a/src/pages/build/lifecycle-events-and-callbacks.md +++ b/src/pages/build/callbacks-and-lifecycle-hooks.md @@ -1,16 +1,16 @@ --- -title: "Lifecycle Events and Callbacks" +title: "Callbacks and Lifecycle Hooks" --- ::: intro -A [cell](/concepts/2_application_architecture/#cell) can respond to various events in the life of a hApp by defining specially named **lifecycle callbacks**. This lets back-end code define and validate data, perform initialization tasks, respond to [remote signals](/concepts/9_signals), and follow up after successful writes. +A [cell](/concepts/2_application_architecture/#cell) can respond to various events in the life of a hApp by defining specially named **callbacks**, including **lifecycle hooks**. These functions may define and validate data, perform initialization tasks, respond to [remote signals](/concepts/9_signals), or follow up after successful writes. ::: -All of the lifecycle callbacks must follow the [pattern for public functions](/build/zomes/#define-a-function) on the Zomes page. They must also have the specific input argument and return value types described below. +All of the callbacks must follow the [pattern for public functions](/build/zomes/#define-a-function) we introduced on the Zomes page. They must also have the specific input argument and return value types we describe below. ## Integrity zomes -Your [integrity zome](/build/zomes/#integrity) must define two callbacks, `validate` and `entry_defs`, and it may define an optional callback, `genesis_self_check`. All of these functions **cannot have side effects**; any attempt to write data will fail. They also cannot access data that changes over time or across participants, such as the cell's [agent ID](/build/identifiers/#agent) or a collection of [links](/build/links-paths-and-anchors/) in the [DHT](/concepts/4_dht). +Your [integrity zome](/build/zomes/#integrity) may define three callbacks, `validate`, `entry_defs`, and `genesis_self_check`. All of these functions **cannot have side effects**; any attempt to write data will fail. They also cannot access data that changes over time or across agents, such as the current cell's [agent ID](/build/identifiers/#agent) or a collection of [links](/build/links-paths-and-anchors/) in the [DHT](/concepts/4_dht). ### Define a `validate` callback @@ -19,36 +19,40 @@ In order to validate your data you'll need to define a `validate` callback. It m The `validate` callback is called at two times: -1. On an agent's device, when they try to author an [action](/build/working-with-data/#entries-actions-and-records-primary-data), and -2. On a peer's device, when they receive a [DHT operation](/concepts/4_dht/#a-cloud-of-witnesses) to store and serve as part of the shared database. +1. When an agent tries to author an [action](/build/working-with-data/#entries-actions-and-records-primary-data), and +2. When an agent receives a [DHT operation](/concepts/4_dht/#a-cloud-of-witnesses) to store and serve as part of the shared database. The nature of validation is out of scope for this page (we'll write a page on it soon), but here's a very basic example of a validation callback that approves everything: ```rust +use hdi::prelude::*; + #[hdk_extern] pub fn validate(_: Op) -> ExternResult { - Ok(Valid) + Ok(ValidateCallbackResult::Valid) } ``` And here's an example of one that rejects everything. You'll note that the outer result is `Ok`; you should generally reserve `Err` for unexpected failures such as inability to deserialize data. However, Holochain will treat both `Ok(Invalid)` and `Err` as invalid operations that should be rejected. ```rust +use hdi::prelude::*; + #[hdk_extern] pub fn validate(_: Op) -> ExternResult { - Ok(Invalid("I reject everything")) + Ok(ValidateCallbackResult::Invalid("I reject everything")) } ``` ### Define an `entry_defs` callback -You don't need to define this callback by hand; you can let the `hdk_entry_types` macro do it for you. Read the [Define an entry type section](/build/entries/#define-an-entry-type) to find out how. +You don't need to write this callback by hand; you can let the `hdk_entry_types` macro do it for you. Read the [Define an entry type section](/build/entries/#define-an-entry-type) to find out how. ### Define a `genesis_self_check` callback -Holochain assumes that every participant in a network is able to self-validate all the data they create before storing it in their [source chain](/concepts/3_source_chain/) and publishing it to the [DHT](/concepts/4_dht/). But at **genesis** time, when their cell has just been instantiated but they haven't connected to other peers, they may not be able to fully validate their [**genesis records**](/concepts/3_source_chain/#source-chain-your-own-data-store) if their validity depends on shared data. So Holochain skips full self-validation for these records, only validating the basic structure of their [actions](/build/working-with-data/#entries-actions-and-records-primary-data). +Holochain assumes that every agent is able to self-validate all the data they create before storing it in their [source chain](/concepts/3_source_chain/) and publishing it to the [DHT](/concepts/4_dht/). But at **genesis** time, when their cell has just been instantiated but they haven't connected to other peers, they may not be able to fully validate their [**genesis records**](/concepts/3_source_chain/#source-chain-your-own-data-store) if their validity depends on shared data. So Holochain skips full self-validation for these records, only validating the basic structure of their [actions](/build/working-with-data/#entries-actions-and-records-primary-data). -This creates a risk to the new participant; they may mistakenly publish malformed data and be rejected from the network. You can define a `genesis_self_check` function that checks the _content_ of genesis records before they're published. This function is limited --- it naturally doesn't have access to DHT data. But it can be a useful guard against a [membrane proof](/resources/glossary/#membrane-proof) that the participant typed or pasted incorrectly, for example. +This creates a risk to the new agent; they may mistakenly publish malformed data and be rejected from the network. You can define a `genesis_self_check` function that checks the _content_ of genesis records before they're published. This function is limited --- it naturally doesn't have access to DHT data. But it can be a useful guard against a [membrane proof](/resources/glossary/#membrane-proof) that the participant typed or pasted incorrectly, for example. `genesis_self_check` must take a single argument of type [`GenesisSelfCheckData`](https://docs.rs/hdi/latest/hdi/prelude/type.GenesisSelfCheckData.html) and return a value of type [`ValidateCallbackResult`](https://docs.rs/hdi/latest/hdi/prelude/enum.ValidateCallbackResult.html) wrapped in an `ExternResult`. @@ -71,7 +75,7 @@ pub fn genesis_self_check(data: GenesisSelfCheckData) -> ExternResult ExternResult<()> { After a zome function call completes, any actions that it created are validated, then written to the cell's source chain if all actions pass validation. While the function is running, nothing has been stored even if [CRUD](/build/working-with-data/#adding-and-modifying-data) function calls return `Ok`. (Read more about the [atomic, transactional nature](/build/zome-functions/#atomic-transactional-commits) of writes in a zome function call.) That means that any follow-up you do within the same function, like pinging other peers, might point to data that doesn't exist if the function fails at a later step. -If you need to do any follow-up, it's safer to do this in a lifecycle callback called `post_commit`, which is called after Holochain's [call-zome workflow](/build/zome-functions/#zome-function-call-lifecycle) successfully writes its actions to the source chain. (Function calls that don't write data won't trigger this event.) +If you need to do any follow-up, it's safer to do this in a lifecycle hook called `post_commit`, which is called after Holochain's [call-zome workflow](/build/zome-functions/#zome-function-call-lifecycle) successfully writes its actions to the source chain. (Function calls that don't write data won't trigger this event.) `post_commit` must take a single argument of type Vec<SignedActionHashed>, which contains all the actions the function call wrote, and it must return an empty `ExternResult<()>`. This callback must not write any data, but it may call other zome functions in the same cell or any other local or remote cell, and it may send local or remote signals. diff --git a/src/pages/build/index.md b/src/pages/build/index.md index d32aa517c..cb3956934 100644 --- a/src/pages/build/index.md +++ b/src/pages/build/index.md @@ -27,7 +27,7 @@ Now that you've got some basic concepts and the terms we use for them, it's time ::: topic-list * [Overview](/build/application-structure/) --- an overview of Holochain's modularity and composability units * [Zomes](/build/zomes/) --- integrity vs coordinator, how to structure and compile - * [Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) --- writing functions that respond to events in a hApp's lifecycle + * [Lifecycle Events and Callbacks](/build/callbacks-and-lifecycle-hooks/) --- writing functions that respond to events in a hApp's lifecycle * [Zome Functions](/build/zome-functions/) --- writing your hApp's back-end API ::: diff --git a/src/pages/build/zome-functions.md b/src/pages/build/zome-functions.md index 4d3eed14d..e4518732d 100644 --- a/src/pages/build/zome-functions.md +++ b/src/pages/build/zome-functions.md @@ -3,10 +3,10 @@ title: "Zome Functions" --- ::: intro -Besides [lifecycle callbacks](/build/lifecycle-events-and-callbacks), a zome defines public functions that acts as its API. These functions can read and write data, manage permissions, send signals, and call functions in other cells in the same Holochain instance or to remote peers in the same network. +Besides [lifecycle hooks](/build/callbacks-and-lifecycle-hooks), a zome defines public functions that acts as its API. These functions can read and write data, manage permissions, send signals, and call functions in other cells in the same Holochain instance or to remote peers in the same network. ::: -As we touched on in the [Zomes page](/build/zomes/#how-a-zome-is-structured), a zome is just a WebAssembly module with some public functions. These functions have access to Holochain's host API. Some of them are [lifecycle callbacks](/build/lifecycle-events-and-callbacks/), and others are functions you create yourself to serve as your zome's API. This second kind of function is what we'll talk about in this page. +As we touched on in the [Zomes page](/build/zomes/#how-a-zome-is-structured), a zome is just a WebAssembly module with some public functions. These functions have access to Holochain's host API. Some of them are [lifecycle hooks](/build/callbacks-and-lifecycle-hooks/), and others are functions you create yourself to serve as your zome's API. This second kind of function is what we'll talk about in this page. Holochain sandboxes your zome, acting as an intermediary between it and the user's storage and UI at the local level, and between it and other peers at the network level. @@ -33,7 +33,7 @@ A zome function call's writes are _atomic_ and _transactional_; that is, **all t Zome function calls, and their transactions, can also run in parallel. A zome call transaction has one big difference from traditional database transactions: if one transaction begins, then another transaction begins and commits before the first transaction finishes, **the first transaction will roll back** and return a 'chain head moved' error to the caller. -The possibility of a rollback means that any follow-up tasks with written data should happen in a [`post_commit` callback](/build/lifecycle-events-and-callbacks/#define-a-post-commit-callback). +The possibility of a rollback means that any follow-up tasks with written data should happen in a [`post_commit` callback](/build/callbacks-and-lifecycle-hooks/#define-a-post-commit-callback). ### Relaxed chain top ordering @@ -80,7 +80,7 @@ Here's how the **call-zome workflow** handles a zome function call: * If at least one wasn't, return a 'chain head moved' error to the caller. 8. Write the new actions to the source chain and store their corresponding DHT operations in the local DHT store. 9. Trigger the **publish** workflow in a separate thread, which will try to share the DHT operations with network peers. -10. Trigger the **post-commit** workflow in a separate thread, which will look for a [`post_commit` callback](/build/lifecycle-events-and-callbacks/#define-a-post-commit-callback) in the same zome as the called function, and pass the new actions to it. +10. Trigger the **post-commit** workflow in a separate thread, which will look for a [`post_commit` callback](/build/callbacks-and-lifecycle-hooks/#define-a-post-commit-callback) in the same zome as the called function, and pass the new actions to it. 11. Return the zome function's return value to the caller. ## References @@ -93,5 +93,5 @@ Here's how the **call-zome workflow** handles a zome function call: * [Core Concepts: Application Architecture](/concepts/2_application_architecture/) * [Build Guide: Zomes](/build/zomes/) -* [Build Guide: Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) +* [Build Guide: Lifecycle Events and Callbacks](/build/callbacks-and-lifecycle-hooks/) * [Build Guide: Entries: Create with relaxed chain top ordering](/build/entries/#create-with-relaxed-chain-top-ordering) \ No newline at end of file diff --git a/src/pages/build/zomes.md b/src/pages/build/zomes.md index b5066e39f..b171b56d3 100644 --- a/src/pages/build/zomes.md +++ b/src/pages/build/zomes.md @@ -6,7 +6,7 @@ title: "Zomes" ### In this section {data-no-toc} * Zomes (this page) - * [Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) --- writing functions that respond to events in a hApp's lifecycle + * [Lifecycle Events and Callbacks](/build/callbacks-and-lifecycle-hooks/) --- writing functions that respond to events in a hApp's lifecycle * [Zome Functions](/build/zome-functions/) --- writing your hApp's back-end API ::: @@ -16,7 +16,7 @@ A **zome** (short for chromosome) is a module of executable code within a [**DNA ## How a zome is structured -A zome is just a [WebAssembly module](https://webassembly.github.io/spec/core/syntax/modules.html) that exposes public functions. The **conductor** (the Holochain runtime) calls these functions at different points in the application's lifetime. Some functions have special names and serve as [**callbacks**](/build/lifecycle-events-and-callbacks/) that are called by the Holochain system. Others are ones you define yourself, and they become your zome's API that external processes such as a UI can call. +A zome is just a [WebAssembly module](https://webassembly.github.io/spec/core/syntax/modules.html) that exposes public functions. The **conductor** (the Holochain runtime) calls these functions at different points in the application's lifetime. Some functions have special names and serve as [**callbacks**](/build/callbacks-and-lifecycle-hooks/) that are called by the Holochain system. Others are ones you define yourself, and they become your zome's API that external processes such as a UI can call. ## How a zome is written @@ -40,7 +40,7 @@ When you're writing an integrity zome, use the smaller [`hdi`](https://crates.io Your integrity zome tells Holochain about the types of [entries](/build/entries/) and [links](/build/links-paths-and-anchors/) it defines with macros called [`hdk_entry_types`](https://docs.rs/hdi/latest/hdi/attr.hdk_entry_types.html) and [`hdk_link_types`](https://docs.rs/hdi/latest/hdi/attr.hdk_link_types.html) added to enums of all the entry and link types. These create callbacks that are run at DNA install time. Read more in [Define an entry type](/build/entries/#define-an-entry-type) and [Define a link type](/build/links-paths-and-anchors/#define-a-link-type). -Finally, your integrity zome defines [**validation callbacks**](/build/lifecycle-events-and-callbacks/#define-a-validate-callback) that check for correctness of data and actions. Holochain runs this on an agent's own device when they try to author data, and when they're asked to store and serve data authored by others. +Finally, your integrity zome defines [**validation callbacks**](/build/callbacks-and-lifecycle-hooks/#define-a-validate-callback) that check for correctness of data and actions. Holochain runs this on an agent's own device when they try to author data, and when they're asked to store and serve data authored by others. #### Create an integrity zome @@ -60,7 +60,7 @@ Then add some necessary dependencies to your new `Cargo.toml` file: +serde = "1.0" ``` -Now you can write a [`validate` callback](/build/lifecycle-events-and-callbacks/#define-a-validate-callback) and [define some entry types](/build/entries/#define-an-entry-type). +Now you can write a [`validate` callback](/build/callbacks-and-lifecycle-hooks/#define-a-validate-callback) and [define some entry types](/build/entries/#define-an-entry-type). When you've written some code, compile your zome using `cargo`: @@ -70,7 +70,7 @@ cargo build --release --target wasm32-unknown-unknown ### Coordinator -Coordinator zomes hold your back-end logic --- the functions that read and write data or communicate with peers. In addition to some optional lifecycle callbacks [lifecycle callbacks](/build/lifecycle-events-and-callbacks/#coordinator-zomes), you'll also write your own **zome functions** that serve as your zome's API. +Coordinator zomes hold your back-end logic --- the functions that read and write data or communicate with peers. In addition to some optional lifecycle hooks [lifecycle hooks](/build/callbacks-and-lifecycle-hooks/#coordinator-zomes), you'll also write your own **zome functions** that serve as your zome's API. #### Create a coordinator zome @@ -143,7 +143,7 @@ pub fn check_age_for_18a_movie(age: u32) -> ExternResult<()> { ## Further reading -* [Build Guide: Lifecycle Events and Callbacks](/build/lifecycle-events-and-callbacks/) +* [Build Guide: Lifecycle Events and Callbacks](/build/callbacks-and-lifecycle-hooks/) * [Build Guide: Zome Functions](/build/zome-functions/) * [WebAssembly](https://webassembly.org/) * [serde](https://serde.rs/) diff --git a/src/pages/concepts/3_source_chain.md b/src/pages/concepts/3_source_chain.md index bd8ac35d2..4a4c69837 100644 --- a/src/pages/concepts/3_source_chain.md +++ b/src/pages/concepts/3_source_chain.md @@ -63,7 +63,7 @@ This journal starts with three special system records, followed optionally by so 1. The **DNA hash**. Because the DNA's executable code constitutes the 'rules of the game' for a network of participants, this record claims that your Holochain runtime has seen and is abiding by those rules. 2. The agent's **membrane proof**. When a cell tries to join a network, it shares this entry with the existing peers, who check it and determine whether the cell should be allowed to join them. Examples: an invite code, an employee ID signed by the HR department, or a proof of paid subscription fees. 3. The **agent ID**. This contains your public key as your digital identity. The signatures on all subsequent records must match this public key in order to be valid. -4. Zero or more records containing application data that was written during the init process --- that is, by the `init` [lifecycle callback](../11_lifecycle_events) of any coordinator zomes that define one. +4. Zero or more records containing application data that was written during the init process --- that is, by the `init` [lifecycle hook](../11_lifecycle_events) of any coordinator zomes that define one. 4. The **init complete** action. This is a record meant for internal use, simply helping the conductor remind itself that it's completely activated the cell by running all the coordinator zomes' `init` callbacks. After this come the records that record the user's actions. These include: From 0cc2fca40d66464bfb99f8e54f6a1c999060c817 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 17 Jan 2025 13:32:12 -0800 Subject: [PATCH 13/33] little bit more of the same --- src/pages/build/zome-functions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/build/zome-functions.md b/src/pages/build/zome-functions.md index e4518732d..41445cfcc 100644 --- a/src/pages/build/zome-functions.md +++ b/src/pages/build/zome-functions.md @@ -3,10 +3,10 @@ title: "Zome Functions" --- ::: intro -Besides [lifecycle hooks](/build/callbacks-and-lifecycle-hooks), a zome defines public functions that acts as its API. These functions can read and write data, manage permissions, send signals, and call functions in other cells in the same Holochain instance or to remote peers in the same network. +Besides special [callbacks](/build/callbacks-and-lifecycle-hooks), a zome defines public functions that acts as its API. These functions can read and write data, manage permissions, send signals, and call functions in other cells in the same Holochain instance or to remote peers in the same network. ::: -As we touched on in the [Zomes page](/build/zomes/#how-a-zome-is-structured), a zome is just a WebAssembly module with some public functions. These functions have access to Holochain's host API. Some of them are [lifecycle hooks](/build/callbacks-and-lifecycle-hooks/), and others are functions you create yourself to serve as your zome's API. This second kind of function is what we'll talk about in this page. +As we touched on in the [Zomes page](/build/zomes/#how-a-zome-is-structured), a zome is just a WebAssembly module with some public functions. These functions have access to Holochain's host API. Some of them are [callbacks](/build/callbacks-and-lifecycle-hooks/) with special purposes, and others are functions you create yourself to serve as your zome's API. This second kind of function is what we'll talk about in this page. Holochain sandboxes your zome, acting as an intermediary between it and the user's storage and UI at the local level, and between it and other peers at the network level. From a1c3af3101f96b249bac903227052230febf8421 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 20 Jan 2025 09:54:28 -0800 Subject: [PATCH 14/33] test/fix all code samples in callbacks page --- .../build/callbacks-and-lifecycle-hooks.md | 138 +++++++++++------- src/pages/build/zome-functions.md | 2 +- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/src/pages/build/callbacks-and-lifecycle-hooks.md b/src/pages/build/callbacks-and-lifecycle-hooks.md index 1cb3ab765..f8b21b021 100644 --- a/src/pages/build/callbacks-and-lifecycle-hooks.md +++ b/src/pages/build/callbacks-and-lifecycle-hooks.md @@ -40,7 +40,7 @@ use hdi::prelude::*; #[hdk_extern] pub fn validate(_: Op) -> ExternResult { - Ok(ValidateCallbackResult::Invalid("I reject everything")) + Ok(ValidateCallbackResult::Invalid("I reject everything".into())) } ``` @@ -59,17 +59,17 @@ This creates a risk to the new agent; they may mistakenly publish malformed data Here's an example that checks that the membrane proof exists and is the right length: ```rust -use hdi::prelude::{GenesisSelfCheckData, hdk_extern, ValidateCallbackResult}; +use hdi::prelude::*; #[hdk_extern] pub fn genesis_self_check(data: GenesisSelfCheckData) -> ExternResult { if let Some(membrane_proof) = data.membrane_proof { if membrane_proof.bytes().len() == 32 { - Ok(Valid) + return Ok(ValidateCallbackResult::Valid); } - Ok(Invalid("Membrane proof is not the right length. Please check it and enter it again.")) + return Ok(ValidateCallbackResult::Invalid("Membrane proof is not the right length. Please check it and enter it again.".into())); } - Ok(Invalid("This network needs a membrane proof to join.")) + Ok(ValidateCallbackResult::Invalid("This network needs a membrane proof to join.".into())) } ``` @@ -95,18 +95,18 @@ Once `init` runs successfully for all coordinator zomes in a DNA, Holochain writ `init` must take an empty `()` input argument and return an [`InitCallbackResult`](https://docs.rs/hdk/latest/hdk/prelude/enum.InitCallbackResult.html) wrapped in an `ExternResult`. All zomes' `init` callbacks in a DNA must return a success result in order for cell initialization to succeed; otherwise any data written in these callbacks, along with the `InitZomesComplete` action, will be rolled back. _If any zome's init callback returns an `InitCallbackResult::Fail`, initialization will fail._ Otherwise, if any init callback returns an `InitCallbackResult::UnresolvedDependencies`, initialization will be retried at the next zome call attempt. -Here's an `init` callback that [links](/build/links-paths-and-anchors/) the [agent's ID](/build/identifiers/#agent) to the [DNA hash](/build/identifiers/#dna) as a sort of "I'm here" note. (It assumes that you've written an integrity zome called `foo_integrity` that [defines one type of link](/build/links-paths-and-anchors/#define-a-link-type) called `ParticipantRegistration`.) +Here's an `init` callback that [links](/build/links-paths-and-anchors/) the [agent's ID](/build/identifiers/#agent) to the [DNA hash](/build/identifiers/#dna) as a sort of "I'm here" note. (It depends on a couple things being defined in your integrity zome; we'll show the integrity zome after this sample for completeness.) ```rust -use foo_integrity::LinkTypes; +use foo_integrity::{get_participant_registration_anchor, LinkTypes}; use hdk::prelude::*; #[hdk_extern] pub fn init(_: ()) -> ExternResult { - let DnaInfoV2 { hash: dna_hash } = dna_info()?; - let AgentInfo { agent_latest_pubkey: my_pubkey } = agent_info()?; + let participant_registration_anchor_hash = get_participant_registration_anchor_hash()?; + let AgentInfo { agent_latest_pubkey: my_pubkey, ..} = agent_info()?; create_link( - dna_hash, + participant_registration_anchor_hash, my_pubkey, LinkTypes::ParticipantRegistration, () @@ -116,6 +116,32 @@ pub fn init(_: ()) -> ExternResult { } ``` +Here's the integrity zome code needed to make this work: + +```rust +use hdi::prelude::* + +#[hdk_link_types] +pub enum LinkTypes { + ParticipantRegistration, +} + +// This is a very simple implementation of the Anchor pattern, which you can +// read about in https://developer.holochain.org/build/links-paths-and-anchors/ +// You don't need to tell Holochain about it with the `hdk_entry_types` macro, +// because it never gets stored -- we only use it to calculate a hash. +#[hdk_entry_helper] +pub struct Anchor(pub Vec); + +pub fn get_participant_registration_anchor_hash() -> ExternResult { + hash_entry(Anchor( + "_participants_" + .as_bytes() + .to_owned() + )) +} +``` + !!! info Why link the agent key to a well-known hash? Because there's no single source of truth in a Holochain network, it's impossible to get the full list of peers who have joined it. The above pattern is an easy way for newcomers to register themselves as active participants so others can find them. @@ -157,46 +183,51 @@ This zome function and remote signal receiver callback implement a "heartbeat" t use foo_integrity::LinkTypes; use hdk::prelude::*; -// We're using this type for both remote signals to other peers and local -// signals to the UI. -enum SignalType { +// We're creating this type for both remote signals to other peers and local +// signals to the UI. It's a good idea to define your signal type as an enum, +// in case you want to add new message types later. +#[derive(Serialize, Deserialize, Debug)] +enum Signal { Heartbeat(AgentPubKey), } +#[hdk_extern] +pub fn recv_remote_signal(payload: Signal) -> ExternResult<()> { + if let Signal::Heartbeat(agent_id) = payload { + // Pass the heartbeat along to my UI so it can update the other + // peer's online status. + return emit_signal(Signal::Heartbeat(agent_id)); + } + Ok(()) +} + // My UI calls this function at regular intervals to let other participants // know I'm online. #[hdk_extern] pub fn heartbeat(_: ()) -> ExternResult<()> { // Get all the registered participants from the DNA hash. - let DnaInfoV2 { hash: dna_hash } = dna_info()?; + let participant_registration_anchor_hash = get_participant_registration_anchor_hash()?; let other_participants_keys = get_links( GetLinksInputBuilder::try_new( - dna_hash, + participant_registration_anchor_hash, LinkTypes::ParticipantRegistration )? .get_options(GetStrategy::Network) .build() )? - .filter_map(|l| l.target.into_agent_pub_key()); + .iter() + .filter_map(|l| l.target.clone().into_agent_pub_key()) + .collect(); // Now send a heartbeat message to each of them. // Holochain will send them in parallel and won't return an error for any // failure. - let AgentInfo { agent_latest_pubkey: my_pubkey } = agent_info()?; + let AgentInfo { agent_latest_pubkey: my_pubkey, .. } = agent_info()?; send_remote_signal( - SignalType::Heartbeat(my_pubkey), + Signal::Heartbeat(my_pubkey), other_participants_keys ) } - -#[hdk_extern] -pub fn recv_remote_signal(payload: SignalType) -> ExternResult<()> { - match payload { - // Pass the heartbeat along to my UI so it can update the other - // peer's online status. - SignalType::Heartbeat(peer_pubkey) => emit_signal(payload) - } -} ``` ### Define a `post_commit` callback @@ -210,10 +241,11 @@ If you need to do any follow-up, it's safer to do this in a lifecycle hook calle Here's an example that uses `post_commit` to tell someone a movie loan has been created for them. It uses the integrity zome examples from [Identifiers](/build/identifiers/#in-dht-data). ```rust -use movie_integrity::{EntryTypes, Movie, UnitEntryTypes}; -use hdk::*; +use movie_integrity::{EntryTypes, Movie, MovieLoan, UnitEntryTypes}; +use hdk::prelude::*; -enum RemoteSignalType { +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum RemoteSignal { MovieLoanHasBeenCreatedForYou(ActionHash), } @@ -221,27 +253,30 @@ enum RemoteSignalType { pub fn post_commit(actions: Vec) -> ExternResult<()> { for action in actions.iter() { // Only handle cases where an entry is being created. - if let Action::Create(create) = action.action() { - let movie_loan = get_movie_loan(action.action_address())?; - send_remote_signal( - RemoteSignalType::MovieLoanHasBeenCreatedForYou(action.action_address()), + if let Action::Create(_) = action.action() { + let movie_loan = get_movie_loan(action.action_address().clone())?; + return send_remote_signal( + RemoteSignal::MovieLoanHasBeenCreatedForYou(action.action_address().clone()), vec![movie_loan.lent_to] ); } } + Ok(()) } -enum LocalSignalType { +#[derive(Serialize, Deserialize, Debug)] +enum LocalSignal { NewMovieLoan(MovieLoan), } #[hdk_extern] -pub fn recv_remote_signal(payload: RemoteSignalType) -> ExternResult<()> { - if let MovieLoanHasBeenCreatedForYou(action_hash) = payload { +pub fn recv_remote_signal(payload: RemoteSignal) -> ExternResult<()> { + if let RemoteSignal::MovieLoanHasBeenCreatedForYou(action_hash) = payload { let movie_loan = get_movie_loan(action_hash)?; // Send the new movie loan data to the borrower's UI! - emit_signal(LocalSignalType::NewMovieLoan(movie_loan))?; + emit_signal(LocalSignal::NewMovieLoan(movie_loan))?; } + Ok(()) } fn get_movie_loan(action_hash: ActionHash) -> ExternResult { @@ -249,25 +284,28 @@ fn get_movie_loan(action_hash: ActionHash) -> ExternResult { action_hash, GetOptions::network() )? { - if let Some(movie_loan) = record.entry().to_app_option()? { - Ok(movie_loan) + let maybe_movie_loan: Option = record.entry() + .to_app_option() + .map_err(|e| wasm_error!("Couldn't deserialize entry into movie loan: {}", e))?; + if let Some(movie_loan) = maybe_movie_loan { + return Ok(movie_loan); } else { - Err(wasm_error!("Entry wasn't a movie loan")) + return Err(wasm_error!("Couldn't retrieve movie loan entry")); } - } else { - Err(wasm_error!("Couldn't retrieve movie loan")) } + Err(wasm_error!("Couldn't retrieve movie loan")) } -struct UpdateMovieInput { - original_hash: ActionHash, - data: Movie, +#[derive(Serialize, Deserialize, Debug)] +pub struct UpdateMovieInput { + pub original_hash: ActionHash, + pub data: Movie, } #[hdk_extern] pub fn update_movie(input: UpdateMovieInput) -> ExternResult { let maybe_original_record = get( - input.original_hash, + input.original_hash.clone(), GetOptions::network() )?; match maybe_original_record { @@ -276,10 +314,10 @@ pub fn update_movie(input: UpdateMovieInput) -> ExternResult { // A more robust app would at least check that the original was of the // correct type. Some(_) => { - update_entry( - input.original_hash, + return update_entry( + input.original_hash.clone(), &EntryTypes::Movie(input.data) - )? + ); } None => Err(wasm_error!("Original movie record not found")), } diff --git a/src/pages/build/zome-functions.md b/src/pages/build/zome-functions.md index 41445cfcc..7d1fec9fa 100644 --- a/src/pages/build/zome-functions.md +++ b/src/pages/build/zome-functions.md @@ -17,7 +17,7 @@ A zome function is a public function that's tagged with the [`hdk_extern`](https Here's a simple example of a zome function that takes a name and returns a greeting: ```rust -use hdk::prelude::{hdk_extern, ExternResult}; +use hdk::prelude::*; #[hdk_extern] pub fn say_hello(name: String) -> ExternResult { From 26b63dd8830514aa528c391710373752199b23e6 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 20 Jan 2025 10:45:18 -0800 Subject: [PATCH 15/33] small edits to callbacks page --- .../build/callbacks-and-lifecycle-hooks.md | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/pages/build/callbacks-and-lifecycle-hooks.md b/src/pages/build/callbacks-and-lifecycle-hooks.md index f8b21b021..30e05c528 100644 --- a/src/pages/build/callbacks-and-lifecycle-hooks.md +++ b/src/pages/build/callbacks-and-lifecycle-hooks.md @@ -46,7 +46,7 @@ pub fn validate(_: Op) -> ExternResult { ### Define an `entry_defs` callback -You don't need to write this callback by hand; you can let the `hdk_entry_types` macro do it for you. Read the [Define an entry type section](/build/entries/#define-an-entry-type) to find out how. +You don't need to write this callback by hand; you can let the `hdk_entry_types` macro do it for you. Read the [Define an entry type section under Entries](/build/entries/#define-an-entry-type) to find out how. ### Define a `genesis_self_check` callback @@ -87,7 +87,7 @@ This callback is called 'lazily'; that is, it's not called immediately after the This gives a participant's Holochain runtime a little bit of time to connect to other peers, which makes various things you might want to do in `init` more likely to succeed if they depend on data in the DHT. -You can force `init` to run eagerly by calling it as if it were a normal zome function. _Note that you can only do this in Holochain 0.5 and newer._ +You can force `init` to run eagerly by calling it as if it were a normal zome function. Note that it might fail with `UnresolvedDependencies` if it needs dependencies from the DHT. _You can only do this in Holochain 0.5 and newer._ !!! @@ -116,7 +116,7 @@ pub fn init(_: ()) -> ExternResult { } ``` -Here's the integrity zome code needed to make this work: +Here's the `foo_integrity` zome code needed to make this work: ```rust use hdi::prelude::* @@ -144,13 +144,13 @@ pub fn get_participant_registration_anchor_hash() -> ExternResult { !!! info Why link the agent key to a well-known hash? -Because there's no single source of truth in a Holochain network, it's impossible to get the full list of peers who have joined it. The above pattern is an easy way for newcomers to register themselves as active participants so others can find them. +Because there's no single source of truth in a Holochain network, it's impossible to get the full list of agents who have joined it. The above pattern is an easy way for newcomers to register themselves as active participants so others can find them. -But the users are also the infrastructure, so this can create "hot spots" where a set of peers --- the ones responsible for storing the base address for all those links --- carry an outsized burden compared to other peers. Read the [anchors and paths section under Links, Paths, and Anchors](/build/links-paths-and-anchors/#anchors-and-paths) for more info. +But if there are a lot of agents in the network, this can create "hot spots" where one set of agents --- the ones responsible for storing the base address for all those links --- carry an outsized burden compared to other agents. Read the [anchors and paths section under Links, Paths, and Anchors](/build/links-paths-and-anchors/#anchors-and-paths) for more info. !!! -This `init` callback also does something useful: it grants all other peers in the network permission to send messages to a participant's [remote signal receiver callback](#define-a-recv-remote-signal-callback). +This `init` callback also does something useful: it grants all peers in the network permission to send messages to an agent's [remote signal receiver callback](#define-a-recv-remote-signal-callback). ```rust use hdk::prelude::*; @@ -173,19 +173,18 @@ pub fn init(_: ()) -> ExternResult { -Peers in a network can send messages to each other via [remote signals](/concepts/9_signals/#remote-signals). In order to handle these signals, your coordinator zome needs to define a `recv_remote_signal` callback. Remote signals get routed from the emitting coordinator zome on Alice's machine to the same one on Bob's machine, so there's no need for a coordinator to handle message types it doesn't know about. +Agents in a network can send messages to each other via [remote signals](/concepts/9_signals/#remote-signals). In order to handle these signals, your coordinator zome needs to define a `recv_remote_signal` callback. Remote signals get routed from the emitting coordinator zome on the sender's machine to the same one on the receiver's machine, so there's no need for a coordinator to handle message types it doesn't know about. `recv_remote_signal` takes a single argument of any type you like --- if your coordinator zome deals with multiple message types, consider creating an enum for all of them. It must return an empty `ExternResult<()>`, as this callback is not called as a result of direct interaction from the local agent and has nowhere to pass a return value. -This zome function and remote signal receiver callback implement a "heartbeat" to let all network participants know who's currently online. It assumes that you'll combine the two `init` callback examples in the previous section, which set up the necessary links and permissions to make this work. +This zome function and remote signal receiver callback implement a "heartbeat" to let everyone keep track of who's currently online. It assumes that you'll combine the two `init` callback examples in the previous section, which set up the necessary links and permissions to make this work. ```rust use foo_integrity::LinkTypes; use hdk::prelude::*; // We're creating this type for both remote signals to other peers and local -// signals to the UI. It's a good idea to define your signal type as an enum, -// in case you want to add new message types later. +// signals to the UI. #[derive(Serialize, Deserialize, Debug)] enum Signal { Heartbeat(AgentPubKey), @@ -232,11 +231,11 @@ pub fn heartbeat(_: ()) -> ExternResult<()> { ### Define a `post_commit` callback -After a zome function call completes, any actions that it created are validated, then written to the cell's source chain if all actions pass validation. While the function is running, nothing has been stored even if [CRUD](/build/working-with-data/#adding-and-modifying-data) function calls return `Ok`. (Read more about the [atomic, transactional nature](/build/zome-functions/#atomic-transactional-commits) of writes in a zome function call.) That means that any follow-up you do within the same function, like pinging other peers, might point to data that doesn't exist if the function fails at a later step. +After a zome function call completes, any actions that it created are validated, then written to the cell's source chain if all actions pass validation. While the function is running, nothing has been stored yet, even if [CRUD](/build/working-with-data/#adding-and-modifying-data) function calls return `Ok` with the [hash of the newly written action](/build/identifiers/#the-unpredictability-of-action-hashes). (Read more about the [atomic, transactional nature](/build/zome-functions/#atomic-transactional-commits) of writes in a zome function call.) That means that any follow-up you do within the same function, like pinging other peers, might point to data that doesn't exist if the function fails at a later step. If you need to do any follow-up, it's safer to do this in a lifecycle hook called `post_commit`, which is called after Holochain's [call-zome workflow](/build/zome-functions/#zome-function-call-lifecycle) successfully writes its actions to the source chain. (Function calls that don't write data won't trigger this event.) -`post_commit` must take a single argument of type Vec<SignedActionHashed>, which contains all the actions the function call wrote, and it must return an empty `ExternResult<()>`. This callback must not write any data, but it may call other zome functions in the same cell or any other local or remote cell, and it may send local or remote signals. +`post_commit` must take a single argument of type Vec<SignedActionHashed>, which contains all the actions the function call wrote, and it must return an empty `ExternResult<()>`. This callback **must not write any data**, but it may call other zome functions in the same cell or any other local or remote cell, and it may send local or remote signals. Here's an example that uses `post_commit` to tell someone a movie loan has been created for them. It uses the integrity zome examples from [Identifiers](/build/identifiers/#in-dht-data). From e2c3f7b22d0225c3c6059eaf6d4d73048191a213 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 20 Jan 2025 10:50:37 -0800 Subject: [PATCH 16/33] link from identifiers to post_commit page (plus a typo fix) --- src/pages/build/identifiers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/build/identifiers.md b/src/pages/build/identifiers.md index fbb543225..6415e2809 100644 --- a/src/pages/build/identifiers.md +++ b/src/pages/build/identifiers.md @@ -226,14 +226,14 @@ Read more about [entries](/build/entries/) and [links](/build/links-paths-and-an There are a few important things to know about action hashes: * You can't know an action's hash until you've written the action, because the action contains the current system time at the moment of writing. -* When you write an action, you can specify "relaxed chain top ordering". We won't go into the details here (see [the section in the Zome Functions page](/build/zome-functions/#relaxed-chain-top-ordering),but when you use it, the action hash may change after the function completes. +* When you write an action, you can specify "relaxed chain top ordering". We won't go into the details here (see [the section in the Zome Functions page](/build/zome-functions/#relaxed-chain-top-ordering), but when you use it, the action hash may change after the function completes. * A function that writes actions is _atomic_, which means that all writes fail or succeed together. Because of these three things, it's unsafe to depend on the value or even existence of an action hash within the same function that writes it. Here are some 'safe usage' notes: * You may safely use the hash of an action you've just written as data in another action in the same function (e.g., in a link or an entry that contains the hash in a field), as long as you're not using relaxed chain top ordering. * The same is also true of action hashes in your function's return value. -* Don't communicate the action hash with the front end, another cell, or another peer on the network via a remote function call or [signal](/concepts/9_signals/) _from within the same function that writes it_, in case the write fails. Instead, do your communicating in a follow-up step. The easiest way to do this is by implementing [a callback called `post_commit`](https://docs.rs/hdk/latest/hdk/#internal-callbacks) which receives a vector of all the actions that the function wrote. +* Don't communicate the action hash with the front end, another cell, or another peer on the network via a remote function call or [signal](/concepts/9_signals/) _from within the same function that writes it_, in case the write fails. Instead, do your communicating in a follow-up step. The easiest way to do this is by [implementing a callback called `post_commit`](/build/callbacks-and-lifecycle-hooks/#define-a-post-commit-callback) which receives a vector of all the actions that the function wrote. From 2a74d82135082b0356139d5807cf9af6cbb674b2 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 20 Jan 2025 11:15:30 -0800 Subject: [PATCH 17/33] reference/further reading for zomes and callbacks pages --- .../build/callbacks-and-lifecycle-hooks.md | 18 +++++++++++++++++- src/pages/build/zome-functions.md | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/pages/build/callbacks-and-lifecycle-hooks.md b/src/pages/build/callbacks-and-lifecycle-hooks.md index 30e05c528..36f40e4d0 100644 --- a/src/pages/build/callbacks-and-lifecycle-hooks.md +++ b/src/pages/build/callbacks-and-lifecycle-hooks.md @@ -321,4 +321,20 @@ pub fn update_movie(input: UpdateMovieInput) -> ExternResult { None => Err(wasm_error!("Original movie record not found")), } } -``` \ No newline at end of file +``` + +## Reference + +* [`holochain_integrity_types::Op`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/op/enum.Op.html) +* [`holochain_integrity_types::validate::ValidateCallbackResult`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/validate/enum.ValidateCallbackResult.html) +* [`holochain_integrity_types::genesis::GenesisSelfCheckData`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/genesis/type.GenesisSelfCheckData.html) +* [`holochain_integrity_types::action::InitZomesComplete`](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/action/struct.InitZomesComplete.html) +* [`holochain_zome_types::init::InitCallbackResult`](https://docs.rs/holochain_zome_types/latest/holochain_zome_types/init/enum.InitCallbackResult.html) +* [`hdk::p2p::send_remote_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.send_remote_signal.html) +* [`hdk::p2p::emit_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.emit_signal.html) + +## Further reading + +* [Core Concepts: Lifecycle Events](/concepts/11_lifecycle_events/) +* [Core Concepts: Signals](/concepts/9_signals/) +* [Build Guide: Identifiers](/build/identifiers/) \ No newline at end of file diff --git a/src/pages/build/zome-functions.md b/src/pages/build/zome-functions.md index 7d1fec9fa..94015547c 100644 --- a/src/pages/build/zome-functions.md +++ b/src/pages/build/zome-functions.md @@ -92,6 +92,7 @@ Here's how the **call-zome workflow** handles a zome function call: ## Further reading * [Core Concepts: Application Architecture](/concepts/2_application_architecture/) +* [Core Concepts: Calls and Capabilities](/concepts/8_calls_capabilities/) * [Build Guide: Zomes](/build/zomes/) * [Build Guide: Lifecycle Events and Callbacks](/build/callbacks-and-lifecycle-hooks/) * [Build Guide: Entries: Create with relaxed chain top ordering](/build/entries/#create-with-relaxed-chain-top-ordering) \ No newline at end of file From 05fbd84159400a6b9c772ef0a500a465fbaa8fc9 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 21 Jan 2025 12:50:17 -0800 Subject: [PATCH 18/33] DNAs page --- .cspell/words-that-should-exist.txt | 1 + src/pages/build/dnas.md | 125 ++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/pages/build/dnas.md diff --git a/.cspell/words-that-should-exist.txt b/.cspell/words-that-should-exist.txt index c1ff70429..c5513e3e0 100644 --- a/.cspell/words-that-should-exist.txt +++ b/.cspell/words-that-should-exist.txt @@ -17,6 +17,7 @@ ified interoperating lifecycle lifecycles +passcode permissioned permissivity redistributable diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md new file mode 100644 index 000000000..29cbd2499 --- /dev/null +++ b/src/pages/build/dnas.md @@ -0,0 +1,125 @@ +--- +title: "DNA" +--- + +::: intro +A **DNA** is a bundle of one or more [**zomes**](/build/zomes/), along with optional **DNA modifiers**. Together, the zomes and DNA modifiers define the executable code and settings for a single **peer-to-peer network**. +::: + +## DNAs: the 'rules of the game' for a network + +Holochain supports multiple, separate peer-to-peer networks, each with its own membership and shared [graph database](/build/working-with-data/). Each network is backed by its own DNA, whose executable code and settings create the 'rules of the game' for the network. + +The **DNA hash** is the unique identifier for a network. The **DNA integrity modifiers** contribute to this hash; the rest of the DNA does not. That means that any change to integrity modifiers will change the DNA hash and form a new network if agents install and run it. + +The contents of a DNA are specified with a **manifest file**. + +## Create a DNA + +If you use the [scaffolding tool](/get-started/3-forum-app-tutorial/), it'll scaffold a working directory for every DNA you scaffold. + +You can also use the `hc` command in the [Holonix dev shell](/get-started/#2-installing-holochain-development-environment) to create a bare working directory: + +```bash +hc dna init movies +``` + +You'll be prompted to enter a name and [**network seed**](#network-seed). After that it'll create a folder called `movies` that contains a basic `dna.yaml` file with your responses to the prompts. + +## Specify a DNA manifest + +A DNA manifest is written in [YAML](https://yaml.org/). It contains details about the DNA, the above integrity modifiers, and a list of coordinator zomes for interacting with the DNA. + +If you want to write your own manifest file, name it `dna.yaml` and give it the following structure. This example assumes that all of your zomes are in a folder called `zomes/`. Afterwards we'll explain what the fields mean. + +```yaml +manifest_version: '1' +name: movies +integrity: + network_seed: null + properties: + foo: bar + baz: 123 + origin_time: 1735841273312901 + zomes: + - name: movies_integrity + hash: null + bundled: 'zomes/movies_integrity/target/wasm32-unknown-unknown/release/movies_integrity.wasm' +coordinator: + zomes: + - name: movies + hash: null + bundled: 'zomes/movies/target/wasm32-unknown-unknown/release/movies.wasm' + dependencies: + - name: movies_integrity +``` + +### DNA manifest structure at a glance + +* `name`: A string for humans to read. This might get used in the admin panel of Holochain [conductors](/concepts/2_application_architecture/#conductor) like [Holochain Launcher](https://github.com/holochain/launcher) or [Moss](https://theweave.social/moss/). +* `integrity`: Contains all the integrity modifiers for the DNA, the things that **change the DNA hash**. + * `network_seed`: A string that serves only to change the DNA hash without affecting behavior. It acts like a network-wide passcode. {#network-seed} + * `properties`: Arbitrary, application-specific constants. The integrity code can access this, deserialize it, and change its runtime behavior. Think of it as configuration for the DNA. + * `origin_time`: The earliest possible timestamp for any data; serves as a basis for coordinating network communication. Pick a date that's guaranteed to be slightly earlier than you expect that the app will start to get used. The scaffolding tool and `hc dna init` will both pick the date you created the DNA. + * `zomes`: A list of all the integrity zomes in the DNA. + * `name`: A unique name for the zome, to be used for dependencies. + * `hash`: Optional. If the hash of the zome at the specified location doesn't match this value, installation will fail. + * Location: The place to find the zome's WebAssembly bytecode. The three options are: + * `bundled`: Expect the file to be part of this [bundle](#bundle-a-dna). The value is a path relative to the manifest file. + * `path`: Get the file from the local filesystem. The value is a filesystem path. + * `url`: Get the file from the web. The value is a URL, of course. +* `coordinator`: Contains all the coordinator bits for the DNA, which do not change the DNA hash and can be modified after the DNA is installed and being used in a [cell](/concepts/2_app_architecture/#cell). + * `zomes`: Currently the only field in `coordinator`. A list of coordinator zomes. Each item in the list is the same as in `integrity.zomes` above, except that the following field is added: + * `dependencies`: The integrity zomes that this coordinator zome depends on. Note that you can leave this field out if there's only one integrity zome (it'll be automatically treated as a dependency). For each dependency in the list, there's one field: + * `name`: A string matching the `name` field of the integrity zome the coordinator zome depends on. + +## Bundle a DNA + +To roll a DNA manifest and all its zomes into a **DNA bundle**, use the `hc` command on a folder that contains a `dna.yaml` file: + +```bash +hc dna pack my_dna/ +``` + +This will create a file in the same folder as the `dna.yaml`, called `.dna`, where `` comes from the `name` field at the top of the manifest. + +## Make a coordinator zome depend on an integrity zome + + + +In order for a coordinator zome to read and write the entry and link types defined by an integrity zome, you'll need to specify the dependency in a few places. + +1. In your coordinator zome's `Cargo.toml` file, specify a dependency on the integrity zome's crate just like you would any Cargo dependency. You can see how to do this in the [Create a coordinator zome section](/build/zomes/#create-a-coordinator-zome) on the Zomes page. +2. In your DNA manifest file, specify the dependency in the `coordinator` section by referencing the integrity zome's `name` field. You can see an example [above](#specify-a-dna-manifest), where the `movies` zome depends on the `movies_integrity` zome. (Remember that you don't need to do this if there's only one integrity zome.) + +Then, in your coordinator zome's code, import the integrity zome's entry and link types enums and entry structs/enums as needed: + +```rust +use movies_integrity::{EntryTypes, LinkTypes, Movie, Director}; +``` + +!!! info Why do I need to specify the dependency twice? + +It's probably clear to you why you'd need to specify an integrity zome as a Cargo dependency. But why would you need to duplicate that relationship in your DNA manifest? + +When you write an entry, its type is stored in the [entry creation action](/build/entries/#entries-and-actions) as a tuple of `(integrity_zome_index, entry_type_index)`, which are just numbers rather than human-readable identifiers. The integrity zomes are indexed by the order they appear in the manifest file, and an integrity zome's entry types are indexed by the order they appear in [an enum with the `#[hdk_entry_types]` macro](/build/entries/#define-an-entry-type). + +When your coordinator zome depends on an integrity zome, it doesn't know what that zome's index in the DNA is, so it addresses the zome by its own internal zero-based indexing. Holochain needs to map this to the proper zome index, so it expects your DNA manifest file to tell it about the integrity zome it depends on. + + + +!!! + +## Next steps + +Now that you've created a bare DNA, it's time to [fill it with zomes](/build/zomes/), [define some data types](/build/working-with-data), and write some [callbacks](/build/callbacks-and-lifecycle-hooks/) and an [API](/build/zome-functions/). + +## Reference + +* [`holochain_types::dna::DnaManifestCurrent`](https://docs.rs/holochain_types/latest/holochain_types/dna/struct.DnaManifestCurrent.html), the underlying type that the DNA manifest gets parsed into. It has a lot of good documentation on the manifest format. + +## Further reading + +* [Get Started: Installing Holochain Development Environment](/get/started/#2-installing-holochain-development-environment) +* [Core Concepts: Application Architecture](/concepts/2_application_architecture/) +* [Build Guide: Zomes](/build/zomes/) \ No newline at end of file From 445e7996d19f92ef5f8c6eec1823c4a8b57e8172 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 21 Jan 2025 12:50:57 -0800 Subject: [PATCH 19/33] add DNAs page to nav and other references --- src/pages/_data/navigation/mainNav.json5 | 1 + src/pages/build/application-structure.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/_data/navigation/mainNav.json5 b/src/pages/_data/navigation/mainNav.json5 index 65ee5a24e..da87eac01 100644 --- a/src/pages/_data/navigation/mainNav.json5 +++ b/src/pages/_data/navigation/mainNav.json5 @@ -30,6 +30,7 @@ { title: "Lifecycle Events and Callbacks", url: "/build/callbacks-and-lifecycle-hooks/" }, { title: "Zome Functions", url: "/build/zome-functions/" }, ] }, + { title: "DNAs", url: "/build/dnas/" } ]}, { title: "Working with Data", url: "/build/working-with-data/", children: [ { title: "Identifiers", url: "/build/identifiers/" }, diff --git a/src/pages/build/application-structure.md b/src/pages/build/application-structure.md index e324c4929..160311ca9 100644 --- a/src/pages/build/application-structure.md +++ b/src/pages/build/application-structure.md @@ -9,7 +9,7 @@ title: Application Structure * [Zomes](/build/zomes/) --- integrity vs coordinator, how to structure and compile * [Lifecycle Events and Callbacks](/build/callbacks-and-lifecycle-hooks/) --- writing functions that respond to events in a hApp's lifecycle * [Zome Functions](/build/zome-functions/) --- writing your hApp's back-end API - * DNAs (coming soon) --- what they're used for, how to specify and bundle + * [DNAs](/build/dnas/) --- what they're used for, how to specify and bundle * hApps (coming soon) --- headless vs UI-based, how to bundle and distribute ::: @@ -63,6 +63,8 @@ This means you can hot-swap coordinators as you fix bugs and add features, witho Because each DNA has its own separate peer network and data store, you can use the DNA concept to come up with creative approaches to [privacy](https://dialnet.unirioja.es/servlet/articulo?codigo=8036267) and access, separation of responsibilities, or data retention. +[Read more on the DNAs page](/build/dnas/). + ### hApp One or more DNAs come together in a **hApp** (Holochain app). Each DNA fills a named **role** in the hApp, and you can think of it like a [microservice](https://en.wikipedia.org/wiki/Microservices). From f618460d8769c740a1216c9ef98a63e7a6748968 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 21 Jan 2025 12:51:09 -0800 Subject: [PATCH 20/33] mention using a Cargo workspace --- src/pages/build/zomes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/build/zomes.md b/src/pages/build/zomes.md index a25ed728e..66f1ba0e1 100644 --- a/src/pages/build/zomes.md +++ b/src/pages/build/zomes.md @@ -96,6 +96,10 @@ Again, **the easiest way to create a coordinator zome** is to let the scaffoldin +my_integrity_zome = { path = "../my_integrity_zome" } ``` +!!! info Consider using a Cargo workspace +As your codebase grows, it might be useful to maintain all your mutual and external dependencies in one place. Consider putting all of the zomes in a DNA into one [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html). For an example, scaffold a basic hApp with one integrity/coordinator zome pair and take a look at the hApp's root `Cargo.toml` file. +!!! + ## Define a function You expose a callback or zome function to the host by making it a `pub fn` and adding a macro called [`hdk_extern`](https://docs.rs/hdk/latest/hdk/prelude/attr.hdk_extern.html). This handles the task of passing data back and forth between the host and the zome, which is complicated and involves pointers to shared memory. From 994c997b08ee8f1eab0c47cfc463e24dfff779bd Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 21 Jan 2025 13:33:53 -0800 Subject: [PATCH 21/33] fix broken URLs and a typo --- src/pages/_data/navigation/mainNav.json5 | 2 +- src/pages/build/dnas.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/_data/navigation/mainNav.json5 b/src/pages/_data/navigation/mainNav.json5 index da87eac01..15bd21681 100644 --- a/src/pages/_data/navigation/mainNav.json5 +++ b/src/pages/_data/navigation/mainNav.json5 @@ -30,7 +30,7 @@ { title: "Lifecycle Events and Callbacks", url: "/build/callbacks-and-lifecycle-hooks/" }, { title: "Zome Functions", url: "/build/zome-functions/" }, ] }, - { title: "DNAs", url: "/build/dnas/" } + { title: "DNAs", url: "/build/dnas/" }, ]}, { title: "Working with Data", url: "/build/working-with-data/", children: [ { title: "Identifiers", url: "/build/identifiers/" }, diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index 29cbd2499..7697f89d0 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -1,5 +1,5 @@ --- -title: "DNA" +title: "DNAs" --- ::: intro @@ -68,7 +68,7 @@ coordinator: * `bundled`: Expect the file to be part of this [bundle](#bundle-a-dna). The value is a path relative to the manifest file. * `path`: Get the file from the local filesystem. The value is a filesystem path. * `url`: Get the file from the web. The value is a URL, of course. -* `coordinator`: Contains all the coordinator bits for the DNA, which do not change the DNA hash and can be modified after the DNA is installed and being used in a [cell](/concepts/2_app_architecture/#cell). +* `coordinator`: Contains all the coordinator bits for the DNA, which do not change the DNA hash and can be modified after the DNA is installed and being used in a [cell](/concepts/2_application_architecture/#cell). * `zomes`: Currently the only field in `coordinator`. A list of coordinator zomes. Each item in the list is the same as in `integrity.zomes` above, except that the following field is added: * `dependencies`: The integrity zomes that this coordinator zome depends on. Note that you can leave this field out if there's only one integrity zome (it'll be automatically treated as a dependency). For each dependency in the list, there's one field: * `name`: A string matching the `name` field of the integrity zome the coordinator zome depends on. @@ -120,6 +120,6 @@ Now that you've created a bare DNA, it's time to [fill it with zomes](/build/zom ## Further reading -* [Get Started: Installing Holochain Development Environment](/get/started/#2-installing-holochain-development-environment) +* [Get Started: Installing Holochain Development Environment](/get-started/#2-installing-holochain-development-environment) * [Core Concepts: Application Architecture](/concepts/2_application_architecture/) * [Build Guide: Zomes](/build/zomes/) \ No newline at end of file From 854c45be122f2df32a1aa54f2d8f1bc47859532e Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 22 Jan 2025 13:43:43 -0800 Subject: [PATCH 22/33] add section to DNAs on remote calling --- src/pages/build/dnas.md | 95 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index 7697f89d0..798a67ac4 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -110,6 +110,101 @@ When your coordinator zome depends on an integrity zome, it doesn't know what th !!! +## Single vs multiple DNAs + +When do you decide whether a hApp should have more than one DNA? Whenever it makes sense to have multiple separate networks or databases within the hApp. These are the most common use cases: + +* **Dividing responsibilities.** For instance, a video sharing hApp may have one group of peers who are willing to index video metadata and offer search services and another group of peers who are willing to host and serve videos, along with people who just want to watch them. This DNA could have `search` and `storage` DNAs, along with a main DNA that allows video watchers to look up peers that are offering services and query them. +* **Creating privileged spaces.** A chat hApp may have both public and private rooms, all [cloned](/resources/glossary/#cloning) from a single `chat_room` DNA. This is a special case, as they all use just one base DNA, but they change just one [integrity modifier](#dna-manifest-structure-at-a-glance) such as the network seed to create new DNAs. +* **Discarding or archiving data.** Because no data is ever deleted in a cell or the network it belongs to, a lot of old data can accumulate. Creating clones of a single storage-heavy DNA, bounded by topic or time period, allows agents to participate in only the networks that contain the information they need. As agents leave networks, unwanted data naturally disappears. + +### Call from one cell to another + +Agents can make **remote calls** within a single DNA's network with the [`call_remote`](https://docs.rs/hdk/latest/hdk/p2p/fn.call_remote.html) host function, and they can make **bridge calls** to other cells in the same hApp instance on their own device with the [`call`](https://docs.rs/hdk/latest/hdk/p2p/fn.call.html) host function. + +Here's an example using both of these functions to implement the dividing-responsibilities pattern described above. It assumes a hApp with two DNAs -- a main one and another one called `search`, which people enable if they want to become a search provider. We won't show the `search` DNA's code here; just imagine it has a coordinator zome called `search` with a function called `do_search_query`. + +```rust +use hdk::prelude::*; + +#[derive(Deserialize, Serialize, Debug)] +pub struct SearchQuery { + pub terms: String, + pub keywords: Vec, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct SearchInput { + pub query: SearchQuery, + // An agent must ask a specific peer for search results. + // A full app would also contain code for finding out what agents are + // offering search services. + pub peer: AgentPubKey, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct SearchResult { + pub title: String, + pub description: String, + pub video_hash: EntryHash, +} + +// Video watcher agents use this function to query a search service provider +// agent. +#[hdk_extern] +pub fn search(input: SearchInput) -> ExternResult> { + let response = call_remote( + input.peer, + // The function is in the same zome. + zome_info()?.name, + "handle_search_query".into(), + // No capability secret is required to call this function. + // This assumes that, somewhere else in the code, there's a way for + // agents who want to become search providers to assign an + // unrestricted capability grant to the `handle_search_query` + // function. + None, + input.query, + )?; + match response { + ZomeCallResponse::Ok(data) => data + .decode() + .map_err(|e| wasm_error!("Couldn't deserialize response into search results: {}", e)), + ZomeCallResponse::Unauthorized(_, _, _, _, agent) => Err(wasm_error!("The remote peer {} rejected your search query", agent)), + ZomeCallResponse::NetworkError(message) => Err(wasm_error!(message)), + _ => Err(wasm_error!("An unknown error just happened")) + } +} + +// Search provider agents use this function to access their `search` cell, +// which is responsible for indexing and querying video metadata. +#[hdk_extern] +pub fn handle_search_query(query: SearchQuery) -> ExternResult> { + let response = call( + CallTargetCell::OtherRole("search".into()), + "search", + "do_search_query".into(), + // Agents don't need a cap secret to call other cells in their own + // hApp instance. + None, + query, + )?; + match response { + ZomeCallResponse::Ok(data) => data + .decode() + .map_err(|e| wasm_error!("Couldn't deserialize response into search results: {}", e)), + _ => Err(wasm_error!("An unknown error just happened")) + } +} +``` + +Note that **bridging between different cells only happens within one agent's hApp instance**, and **remote calls only happens between two agents in one DNA's network**. For two agents, Alice and Bob, Alice can do this: + +| ↓ want to call → | Alice `main` | Alice `search` | Bob `main` | Bob `search` | +| ------------------ | :-----------: | :------------: | :-----------: | :-----------: | +| Alice `main` | `call` | `call` | `call_remote` | ⛔ | +| Alice `search` | `call` | `call` | ⛔ | `call_remote` | + ## Next steps Now that you've created a bare DNA, it's time to [fill it with zomes](/build/zomes/), [define some data types](/build/working-with-data), and write some [callbacks](/build/callbacks-and-lifecycle-hooks/) and an [API](/build/zome-functions/). From 7239d697147d6f3a478096e9e681cb5cb09c9b82 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 22 Jan 2025 14:02:48 -0800 Subject: [PATCH 23/33] add DNAs and hApps to build guide overview --- src/pages/build/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/build/index.md b/src/pages/build/index.md index 270e07ec7..e5428f086 100644 --- a/src/pages/build/index.md +++ b/src/pages/build/index.md @@ -29,6 +29,8 @@ Now that you've got some basic concepts and the terms we use for them, it's time * [Zomes](/build/zomes/) --- integrity vs coordinator, how to structure and compile * [Lifecycle Events and Callbacks](/build/callbacks-and-lifecycle-hooks/) --- writing functions that respond to events in a hApp's lifecycle * [Zome Functions](/build/zome-functions/) --- writing your hApp's back-end API + * [DNAs](/build/dnas/) --- what they're used for, how to specify and bundle + * hApps (coming soon) --- headless vs UI-based, how to bundle and distribute ::: ## Working with data From 6de7e31fa5b9584f1a2a6062fddec55d24456ed8 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 22 Jan 2025 14:03:30 -0800 Subject: [PATCH 24/33] typo in remote/bridge call table --- src/pages/build/dnas.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index 798a67ac4..09a3ebf2b 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -200,10 +200,10 @@ pub fn handle_search_query(query: SearchQuery) -> ExternResult Note that **bridging between different cells only happens within one agent's hApp instance**, and **remote calls only happens between two agents in one DNA's network**. For two agents, Alice and Bob, Alice can do this: -| ↓ want to call → | Alice `main` | Alice `search` | Bob `main` | Bob `search` | -| ------------------ | :-----------: | :------------: | :-----------: | :-----------: | -| Alice `main` | `call` | `call` | `call_remote` | ⛔ | -| Alice `search` | `call` | `call` | ⛔ | `call_remote` | +| ↓ wants to call → | Alice `main` | Alice `search` | Bob `main` | Bob `search` | +| ----------------- | :-----------: | :------------: | :-----------: | :-----------: | +| Alice `main` | `call` | `call` | `call_remote` | ⛔ | +| Alice `search` | `call` | `call` | ⛔ | `call_remote` | ## Next steps From ae3614a3c33ff9f738272b435549925c95b5118d Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 22 Jan 2025 15:04:36 -0800 Subject: [PATCH 25/33] fix lost mention of integrity modifiers --- src/pages/build/dnas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index 09a3ebf2b..8894c6f03 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -28,7 +28,7 @@ You'll be prompted to enter a name and [**network seed**](#network-seed). After ## Specify a DNA manifest -A DNA manifest is written in [YAML](https://yaml.org/). It contains details about the DNA, the above integrity modifiers, and a list of coordinator zomes for interacting with the DNA. +A DNA manifest is written in [YAML](https://yaml.org/). It contains metadata about the DNA, a section for **integrity modifiers**, and a list of coordinator zomes for interacting with the DNA. If you want to write your own manifest file, name it `dna.yaml` and give it the following structure. This example assumes that all of your zomes are in a folder called `zomes/`. Afterwards we'll explain what the fields mean. From f0556bbc0f6776054efc3f3372ab072cb8fecc54 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 27 Jan 2025 10:44:57 -0800 Subject: [PATCH 26/33] replace hc dna init with hc scaffold dna --- src/pages/build/dnas.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index 8894c6f03..4a7e2ddc0 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -16,15 +16,21 @@ The contents of a DNA are specified with a **manifest file**. ## Create a DNA -If you use the [scaffolding tool](/get-started/3-forum-app-tutorial/), it'll scaffold a working directory for every DNA you scaffold. - -You can also use the `hc` command in the [Holonix dev shell](/get-started/#2-installing-holochain-development-environment) to create a bare working directory: +If you use the scaffolding tool, it'll scaffold a working directory for every DNA you scaffold. In the root folder of a hApp project that you've scaffolded, type: ```bash -hc dna init movies +hc scaffold dna movies ``` -You'll be prompted to enter a name and [**network seed**](#network-seed). After that it'll create a folder called `movies` that contains a basic `dna.yaml` file with your responses to the prompts. +This will create a folder called `dnas/movies`, with these contents: + +* `workdir/`: The place for your manifest; it's also where your built and bundled DNA will appear. + * `dna.yaml`: The manifest for your DNA (see the next section). +* `zomes/`: The place where all your zomes should go. + * `integrity/`: The place where your [integrity zomes](/build/zomes/#integrity) should go. + * `coordinator/`: The place where your [coordinator zomes](/build/zomes/#coordinator) should go. + +It'll also add the new DNA to `workdir/happ.yaml`. ## Specify a DNA manifest From 86fcefb68ca36c08ffc7c5dac115ccee11fb5f57 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 27 Jan 2025 14:29:02 -0800 Subject: [PATCH 27/33] change DNA bundling to use npm --- src/pages/build/dnas.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index 4a7e2ddc0..b3eea66a4 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -81,10 +81,27 @@ coordinator: ## Bundle a DNA -To roll a DNA manifest and all its zomes into a **DNA bundle**, use the `hc` command on a folder that contains a `dna.yaml` file: +DNAs are distributed in a `.dna` file that contains the manifest and all the compiled zomes. +If you've used the scaffolding tool to create your DNA in a hApp, you can build all the DNAs at once with the npm script that was scaffolded for you. In your project's root folder, in the dev shell, type: + +```bash +npm run build:happ +``` + +To roll a single DNA manifest and all its zomes into a DNA bundle, first compile all of the zomes: + +```bash +npm run build:zomes +``` + +Then go to the `workdir` folder of the DNA you want to bundle, and use the `hc dna pack` command: + +```bash +cd dnas/movies/workdir +``` ```bash -hc dna pack my_dna/ +hc dna pack ``` This will create a file in the same folder as the `dna.yaml`, called `.dna`, where `` comes from the `name` field at the top of the manifest. From a111ffbab345b8cc7cb2c7dd523e3ac4bbd91ac1 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 27 Jan 2025 14:35:56 -0800 Subject: [PATCH 28/33] Apply suggestions from code review Co-authored-by: Jost <28270981+jost-s@users.noreply.github.com> --- src/pages/build/dnas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index b3eea66a4..64d0b2f39 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -221,7 +221,7 @@ pub fn handle_search_query(query: SearchQuery) -> ExternResult } ``` -Note that **bridging between different cells only happens within one agent's hApp instance**, and **remote calls only happens between two agents in one DNA's network**. For two agents, Alice and Bob, Alice can do this: +Note that **bridging between different cells only happens within one agent's hApp instance**, and **remote calls only happen between two agents in one DNA's network**. For two agents, Alice and Bob, Alice can do this: | ↓ wants to call → | Alice `main` | Alice `search` | Bob `main` | Bob `search` | | ----------------- | :-----------: | :------------: | :-----------: | :-----------: | From 676328061eed7bbf5a19e4f2cb8b94982b983a12 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 27 Jan 2025 14:37:22 -0800 Subject: [PATCH 29/33] remove mention of Moss from DNAs --- src/pages/build/dnas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index b3eea66a4..f0740a82e 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -62,7 +62,7 @@ coordinator: ### DNA manifest structure at a glance -* `name`: A string for humans to read. This might get used in the admin panel of Holochain [conductors](/concepts/2_application_architecture/#conductor) like [Holochain Launcher](https://github.com/holochain/launcher) or [Moss](https://theweave.social/moss/). +* `name`: A string for humans to read. This might get used in the admin panel of Holochain [conductors](/concepts/2_application_architecture/#conductor) like [Holochain Launcher](https://github.com/holochain/launcher). * `integrity`: Contains all the integrity modifiers for the DNA, the things that **change the DNA hash**. * `network_seed`: A string that serves only to change the DNA hash without affecting behavior. It acts like a network-wide passcode. {#network-seed} * `properties`: Arbitrary, application-specific constants. The integrity code can access this, deserialize it, and change its runtime behavior. Think of it as configuration for the DNA. From 7b7e133191bb7413bcc5b74edcc2203db2685f59 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Mon, 27 Jan 2025 14:38:59 -0800 Subject: [PATCH 30/33] clarify wording about throwaway DHTs --- src/pages/build/dnas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index a46ef0917..805d3c435 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -139,7 +139,7 @@ When do you decide whether a hApp should have more than one DNA? Whenever it mak * **Dividing responsibilities.** For instance, a video sharing hApp may have one group of peers who are willing to index video metadata and offer search services and another group of peers who are willing to host and serve videos, along with people who just want to watch them. This DNA could have `search` and `storage` DNAs, along with a main DNA that allows video watchers to look up peers that are offering services and query them. * **Creating privileged spaces.** A chat hApp may have both public and private rooms, all [cloned](/resources/glossary/#cloning) from a single `chat_room` DNA. This is a special case, as they all use just one base DNA, but they change just one [integrity modifier](#dna-manifest-structure-at-a-glance) such as the network seed to create new DNAs. -* **Discarding or archiving data.** Because no data is ever deleted in a cell or the network it belongs to, a lot of old data can accumulate. Creating clones of a single storage-heavy DNA, bounded by topic or time period, allows agents to participate in only the networks that contain the information they need. As agents leave networks, unwanted data naturally disappears. +* **Discarding or archiving data.** Because no data is ever deleted in a cell or the network it belongs to, a lot of old data can accumulate. Creating clones of a single storage-heavy DNA, bounded by topic or time period, allows agents to participate in only the networks that contain the information they need. As agents leave networks, unwanted data naturally disappears from their machines. ### Call from one cell to another From 84da3748dc175b2f46b164ae03a63c8d69546e4a Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 29 Jan 2025 13:50:02 -0800 Subject: [PATCH 31/33] update language about coordinator/integrity deps w/ bug warning --- src/pages/build/dnas.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index 805d3c435..d6e3aff1a 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -79,6 +79,8 @@ coordinator: * `dependencies`: The integrity zomes that this coordinator zome depends on. Note that you can leave this field out if there's only one integrity zome (it'll be automatically treated as a dependency). For each dependency in the list, there's one field: * `name`: A string matching the `name` field of the integrity zome the coordinator zome depends on. + Note that currently [a coordinator zome can only depend on one integrity zome](#multiple-deps-warning). + ## Bundle a DNA DNAs are distributed in a `.dna` file that contains the manifest and all the compiled zomes. @@ -123,14 +125,13 @@ use movies_integrity::{EntryTypes, LinkTypes, Movie, Director}; !!! info Why do I need to specify the dependency twice? -It's probably clear to you why you'd need to specify an integrity zome as a Cargo dependency. But why would you need to duplicate that relationship in your DNA manifest? - -When you write an entry, its type is stored in the [entry creation action](/build/entries/#entries-and-actions) as a tuple of `(integrity_zome_index, entry_type_index)`, which are just numbers rather than human-readable identifiers. The integrity zomes are indexed by the order they appear in the manifest file, and an integrity zome's entry types are indexed by the order they appear in [an enum with the `#[hdk_entry_types]` macro](/build/entries/#define-an-entry-type). +It's probably clear to you why you'd need to specify an integrity zome as a Cargo dependency --- so your coordinator code can work with the types that the integrity zome defines. But why would you need to duplicate that relationship in your DNA manifest? -When your coordinator zome depends on an integrity zome, it doesn't know what that zome's index in the DNA is, so it addresses the zome by its own internal zero-based indexing. Holochain needs to map this to the proper zome index, so it expects your DNA manifest file to tell it about the integrity zome it depends on. +When you construct an entry or link, Holochain needs to know the numeric ID of the integrity zome that should validate it. (It's a numeric ID so that it's nice and small.) But because your coordinator and integrity zome can be reused in another DNA with a different manifest structure, you can't know the integrity zome's ID at compile time. - +So Holochain manages the dependency mapping for you, allowing you to write code without thinking about zome IDs at all. But at the DNA level, you need to tell Holochain what integrity zome it needs, so it knows how to satisfy the dependency. +**Note that there's currently a couple bugs in this dependency mapping.** If your DNA has more than one integrity zome, its coordinator zomes should have **one dependency at most** and should **always list that dependency explicitly** in the DNA manifest. {#multiple-deps-warning} !!! ## Single vs multiple DNAs From 7a713961164ca01bb7b5f81d9b1596e424484192 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 29 Jan 2025 14:33:35 -0800 Subject: [PATCH 32/33] tiny text edit --- src/pages/build/dnas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index d6e3aff1a..325b09cc0 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -129,7 +129,7 @@ It's probably clear to you why you'd need to specify an integrity zome as a Carg When you construct an entry or link, Holochain needs to know the numeric ID of the integrity zome that should validate it. (It's a numeric ID so that it's nice and small.) But because your coordinator and integrity zome can be reused in another DNA with a different manifest structure, you can't know the integrity zome's ID at compile time. -So Holochain manages the dependency mapping for you, allowing you to write code without thinking about zome IDs at all. But at the DNA level, you need to tell Holochain what integrity zome it needs, so it knows how to satisfy the dependency. +So Holochain manages the dependency mapping for you, allowing you to write code without thinking about zome IDs at all. But at the DNA level, you need to tell Holochain what integrity zome the coordinator needs, so it knows how to satisfy the dependency. **Note that there's currently a couple bugs in this dependency mapping.** If your DNA has more than one integrity zome, its coordinator zomes should have **one dependency at most** and should **always list that dependency explicitly** in the DNA manifest. {#multiple-deps-warning} !!! From 26462761391bcfde8429291d35d7ca8f1b889556 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 30 Jan 2025 12:01:44 -0800 Subject: [PATCH 33/33] Apply suggestions from code review this was supposed to already happen; got lost somehow Co-authored-by: Jost <28270981+jost-s@users.noreply.github.com> --- src/pages/build/dnas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/dnas.md b/src/pages/build/dnas.md index 325b09cc0..065acc4a4 100644 --- a/src/pages/build/dnas.md +++ b/src/pages/build/dnas.md @@ -65,7 +65,7 @@ coordinator: * `name`: A string for humans to read. This might get used in the admin panel of Holochain [conductors](/concepts/2_application_architecture/#conductor) like [Holochain Launcher](https://github.com/holochain/launcher). * `integrity`: Contains all the integrity modifiers for the DNA, the things that **change the DNA hash**. * `network_seed`: A string that serves only to change the DNA hash without affecting behavior. It acts like a network-wide passcode. {#network-seed} - * `properties`: Arbitrary, application-specific constants. The integrity code can access this, deserialize it, and change its runtime behavior. Think of it as configuration for the DNA. + * `properties`: Arbitrary, application-specific constants. The zome code can read this at runtime. Think of it as configuration for the DNA. * `origin_time`: The earliest possible timestamp for any data; serves as a basis for coordinating network communication. Pick a date that's guaranteed to be slightly earlier than you expect that the app will start to get used. The scaffolding tool and `hc dna init` will both pick the date you created the DNA. * `zomes`: A list of all the integrity zomes in the DNA. * `name`: A unique name for the zome, to be used for dependencies.