Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build guide: zome functions and lifecycle callbacks #512

Merged
merged 38 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f154488
lifecycle callbacks and zome functions pages
pdaoust Jan 15, 2025
8fc90cd
add new pages to navs
pdaoust Jan 15, 2025
ae585c0
link up all references to two new pages
pdaoust Jan 15, 2025
e20390e
remove redundant descriptions of lifecycles, add examples for relaxed
pdaoust Jan 15, 2025
f16a4a5
add under-the-hood for CreateLink and DeleteLink
pdaoust Jan 15, 2025
cd3eee3
add snapshotted to dict
pdaoust Jan 15, 2025
2ccb02d
fix broken JSON
pdaoust Jan 15, 2025
5cb51e1
fix broken links
pdaoust Jan 15, 2025
60cf346
fix hdk_entry_defs, whoops
pdaoust Jan 15, 2025
060a7d8
add example for post_commit
pdaoust Jan 15, 2025
1010a44
simplify/elaborate post_commit example
pdaoust Jan 16, 2025
39813c0
Merge branch 'feat/guide/app-structure-zomes' into feat/guide/app-str…
pdaoust Jan 17, 2025
5103988
improve language around callbacks and lifecycle hooks, plus a couple …
pdaoust Jan 17, 2025
0cc2fca
little bit more of the same
pdaoust Jan 17, 2025
a1c3af3
test/fix all code samples in callbacks page
pdaoust Jan 20, 2025
26b63dd
small edits to callbacks page
pdaoust Jan 20, 2025
e2c3f7b
link from identifiers to post_commit page (plus a typo fix)
pdaoust Jan 20, 2025
2a74d82
reference/further reading for zomes and callbacks pages
pdaoust Jan 20, 2025
8220034
make post_commit infallible
pdaoust Jan 30, 2025
efc514f
don't mention entry_defs callback
pdaoust Jan 30, 2025
6d4e2fe
Apply suggestions from code review
pdaoust Jan 30, 2025
18aceae
warn about spam/abuse vectors for unrestricted grants and recv_remote…
pdaoust Jan 30, 2025
daba5ef
Merge branch 'feat/guide/app-structure-zome-functions' of github.com:…
pdaoust Jan 30, 2025
6ffdd0a
missing semicolon
pdaoust Jan 30, 2025
2287611
simplify users anchor pattern
pdaoust Jan 30, 2025
cbd6c3c
Merge branch 'feat/guide/app-structure-zome-functions' of github.com:…
pdaoust Jan 30, 2025
c1751e0
simplify language about users anchor pattern
pdaoust Jan 30, 2025
ead2e90
fix some mistakes re: callback
pdaoust Jan 30, 2025
93d3064
fix typo
pdaoust Jan 30, 2025
0867f2f
remove reject-all validation example and bad advice
pdaoust Jan 30, 2025
16c71c7
Merge branch 'feat/guide/app-structure-zome-functions' of github.com:…
pdaoust Jan 30, 2025
a35aab4
fix broken fragment identifier
pdaoust Jan 30, 2025
27ac11c
Merge branch 'main' into feat/guide/app-structure-zome-functions
pdaoust Jan 30, 2025
ff25cda
change wording re: remote signal routing
pdaoust Jan 31, 2025
93c1511
correct mistakes about the definition of genesis records
pdaoust Feb 3, 2025
b671529
make genesis_self_check less scary
pdaoust Feb 3, 2025
9523efb
Merge branch 'feat/guide/app-structure-zome-functions' of github.com:…
pdaoust Feb 3, 2025
fd6e76c
tiny text tweaks
pdaoust Feb 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell/words-that-should-exist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ runtimes
sandboxed
sandboxing
scaffolder
snapshotted
spacebar
todo
todos
Expand Down
6 changes: 6 additions & 0 deletions code_test_4/web-happ.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
manifest_version: '1'
name: blup
ui:
bundled: ./path/to/my/ui.zip
happ_manifest:
bundled: ./path/to/my/happ-bundle.happ
5 changes: 4 additions & 1 deletion src/pages/_data/navigation/mainNav.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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/callbacks-and-lifecycle-hooks/" },
{ title: "Zome Functions", url: "/build/zome-functions/" },
] },
]},
{ title: "Working with Data", url: "/build/working-with-data/", children: [
{ title: "Identifiers", url: "/build/identifiers/" },
Expand Down
2 changes: 2 additions & 0 deletions src/pages/build/application-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/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
:::
Expand Down
321 changes: 321 additions & 0 deletions src/pages/build/callbacks-and-lifecycle-hooks.md

Large diffs are not rendered by default.

167 changes: 113 additions & 54 deletions src/pages/build/entries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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::*;
Expand All @@ -113,42 +113,65 @@ 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),
)?;
```

### Create with relaxed chain top ordering
pdaoust marked this conversation as resolved.
Show resolved Hide resolved

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 to 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.
pdaoust marked this conversation as resolved.
Show resolved Hide resolved
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.
<!-- * a calculated weight value for rate limiting -->
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:

<!-- FIXME: this won't compile as written; write something that gets the old movie and its action hash from somewhere. -->

```rust
use hdk::prelude::*;
use movie_integrity::*;
Expand All @@ -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

Expand Down Expand Up @@ -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<ActionHash> = 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

Expand Down Expand Up @@ -404,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)
Expand Down
4 changes: 2 additions & 2 deletions src/pages/build/identifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, <!-- TODO: fill this in when I write about zome call lifecycles -->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.

<!-- TODO: write about the front end -->

Expand Down
2 changes: 2 additions & 0 deletions src/pages/build/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ 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/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
:::

## Working with data
Expand Down
Loading