Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 src/pages/_data/navigation/mainNav.json5
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
{ title: "Connecting the Parts", url: "/build/connecting-the-parts/", children: [
{ title: "Front End", url: "/build/connecting-a-front-end/" },
{ title: "Calling Zome Functions", url: "/build/calling-zome-functions" },
{ title: "Signals", url: "/build/signals/" },
]},
]
},
Expand Down
87 changes: 5 additions & 82 deletions src/pages/build/callbacks-and-lifecycle-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,93 +126,15 @@ Note that this can create "hot spots" where some agents have a heavier data stor

!!!

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). (Note that this can create a risk of spamming.) {#init-grant-unrestricted-access-to-recv-remote-signal}

```rust
use hdk::prelude::*;

#[hdk_extern]
pub fn init(_: ()) -> ExternResult<InitCallbackResult> {
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)
}
```
The `init` callback is often used to set up initial **capabilities**<!-- TODO: link-->, or access privileges to zome functions. You can see an example on the [Signals page](/build/signals/#remote-signals)

### Define a `recv_remote_signal` callback

<!-- TODO: move this to the signals page after it's written -->

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 a coordinator with the same name on the receiver's machine.

`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 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. Your app might have different kinds of signals for each,
// so you're free to define separate types for local vs remote.
// We recommend making your signal type an enum, so your hApp can define
// different kinds of signals.
#[derive(Serialize, Deserialize, Debug)]
// It's helpful to match the way Holochain serializes its own enums.
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
enum Signal {
Heartbeat(AgentPubKey),
}
`recv_remote_signal` takes a single argument of any type you like. 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.

#[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.
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 participant_registration_anchor_hash = get_participant_registration_anchor_hash()?;
let other_participants_keys = get_links(
GetLinksInputBuilder::try_new(
participant_registration_anchor_hash,
LinkTypes::ParticipantRegistration
)?
.get_options(GetStrategy::Network)
.build()
)?
.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()?;
send_remote_signal(
Signal::Heartbeat(my_pubkey),
other_participants_keys
)
}
```

!!! info Remote signals and privileges
If you grant unrestricted access to your remote signal callback like in the [previous example](#init-grant-unrestricted-access-to-recv-remote-signal), take care that it does as little as possible, to avoid people abusing it. Permissions and privileges are another topic which we'll talk about soon.<!-- TODO: link when the capabilities page is written -->
!!!
See the [Signals](/build/signals/#remote-signals) page for an example implementation.

### Define a `post_commit` callback

Expand Down Expand Up @@ -320,4 +242,5 @@ pub fn update_movie(input: UpdateMovieInput) -> ExternResult<ActionHash> {

* [Core Concepts: Lifecycle Events](/concepts/11_lifecycle_events/)
* [Core Concepts: Signals](/concepts/9_signals/)
* [Build Guide: Identifiers](/build/identifiers/)
* [Build Guide: Identifiers](/build/identifiers/)
* [Build Guide: Signals](/build/signals/)
2 changes: 1 addition & 1 deletion src/pages/build/calling-zome-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Just as with front ends hosted by a supporting Holochain runtime, calls made wit

If two agents have cells running the same DNA --- that is, they're part of the same network --- they can call each other's zome functions _in the same DNA_ using [`hdk::prelude::call_remote`](https://docs.rs/hdk/latest/hdk/p2p/fn.call_remote.html).

!!! info A remote cell might not be running the same coordinator zomes
!!! info A remote cell might not be running the same coordinator zomes {#remote-call-unknown-routing}
Holochain allows agents to add and remove coordinator zomes from their cells. This permits upgrading and customization. But it also means that the zomes and functions that you _think_ are on the other end might not actually be there.
!!!

Expand Down
6 changes: 3 additions & 3 deletions src/pages/build/connecting-the-parts.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ title: "Connecting the Parts"
* [Front end](/build/connecting-a-front-end/) --- establishing a WebSocket connection from JavaScript
* [Calling zome functions](/build/calling-zome-functions) --- examples for front ends, cell-to-cell, and agent-to-agent
* Capabilities (coming soon) --- how to manage access to a cell's zome functions
* Working with signals (coming soon) --- receiving notifications from cells
* [Signals](/build/signals/) --- receiving notifications from cells
:::

::: intro
Your hApp back end's public interface consists of all the [**zome functions**](/build/zome-functions/) of all the [**cells**](/concepts/2_application_architecture/#cell) instantiated from all the [**DNAs**](/build/dnas/) that fill the hApp's [**roles**](/build/application-structure/#happ). It is accessible to locally running processes and to network peers, and is secured by a form of **capability-based security**<!--TODO: link to that page when it's written. -->, adapted for peer-to-peer applications.

The back end can also send out [**signals**](/concepts/9_signals/)<!--TODO: change this to build guide link when signals is written--> that can be received either by UIs or remote peers.
The back end can also send out [**signals**](/build/signals/) that can be received either by UIs or remote peers.
:::

## What processes can connect to a hApp? {#what-processes-can-connect-to-a-happ}
Expand Down Expand Up @@ -45,7 +45,7 @@ This is a complex topic, so we're going to write a separate page about it soon.<

## Sending signals for reactive, event-based programming

Zome functions can send out signals, either locally to front ends or remotely to other agents in the same network. This lets you write programs that react to activity rather than having to poll a function for updates. We'll write more about this soon as well!<!--TODO: link when ready-->
Zome functions can [send out signals](/build/signals/), either locally to front ends or remotely to other agents in the same network. This lets you write programs that react to activity rather than having to poll a function for updates.

## Further reading

Expand Down
2 changes: 1 addition & 1 deletion src/pages/build/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ Now that you've got some basic concepts and the terms we use for them, it's time
* [Front end](/build/connecting-a-front-end/) --- establishing a WebSocket connection from JavaScript
* [Calling zome functions](/build/calling-zome-functions/) --- examples for front ends, cell-to-cell, and agent-to-agent
* Capabilities (coming soon) --- how to manage access to a cell's zome functions
* Working with signals (coming soon) --- receiving notifications from cells
* [Signals](/build/signals) --- receiving notifications from cells
172 changes: 172 additions & 0 deletions src/pages/build/signals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
title: "Signals"
---

::: intro
**Signals** are messages emitted by coordinator zomes, either locally to a front end or remotely to another agent cell in a DNA's network. They help you automate processes in your application and make it dynamic and responsive.
:::

## Send-and-forget messages, locally and across the network

There are two kinds of signals: [local](#local-signals) and [remote](#remote-signals). They are both **send-and-forget**; when you call the host function that sends the signal, they don't wait for confirmation from the receiver, and they don't store messages until the receiver is available.

## Local signals

**Local signals** are sent to [front ends](/build/connecting-a-front-end/) listening on the agent's local machine.

### Emit a signal

Your coordinator zome emits a signal with the [`emit_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.emit_signal.html) host function. It takes any serializable input and you can call this function from a regular [zome function](/build/zome-functions/) or the [`init`](/build/callbacks-and-lifecycle-hooks/#define-an-init-callback), [`recv_remote_signal`](/build/callbacks-and-lifecycle-hooks/#define-a-recv-remote-signal-callback), or [`post_commit`](/build/callbacks-and-lifecycle-hooks/#define-a-post-commit-callback) callbacks.

This example notifies the agent's local UI of any actions that their cell has written to their source chain, which is useful for building reactive front-end data stores, especially when some actions may be written by [remote calls](/build/calling-zome-functions/#call-a-zome-function-from-another-agent-in-the-network) rather than direct user action. You can see this pattern in any scaffolded hApp, in the file `dnas/<dna>/zomes/coordinator/<zome>/src/lib.rs`. ([Read about the `post_commit` callback](/build/callbacks-and-lifecycle-hooks/#define-a-post-commit-callback) to learn more about hooking into successful writes.)

```rust
use hdk::prelude::*;

// Because you'll probably end up defining multiple local signal message
// types, it's best to define your local signal as an enum of messages.
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum LocalSignal {
ActionWritten(ActionHash),
}

#[hdk_extern(infallible)]
pub fn post_commit(committed_actions: Vec<SignedActionHashed>) {
// Tell the UI about every action that any function in this zome has
// written.
for action in committed_actions {
let _ = emit_signal(LocalSignal::ActionWritten(action.action_address()));
}
}
```

### Listen for a signal

Holochain emits local signals over active app WebSocket connections, and a client should provide a way to receive these signals. For instance, with the TypeScript client, you can subscribe to signals with the [`AppWebsocket.prototype.on`](https://github.com/holochain/holochain-client-js/blob/main/docs/client.appwebsocket.on.md) method. The signal handler should expect signals from _any coordinator zome in any cell_ in the agent's hApp instance, and should discriminate between them by cell ID and zome name.

<!-- FIXME(0.5): does SignalType still exist? -->

```typescript
import type { Signal, AppSignal, AgentPubKey } from "@holochain/client";
import { SignalType, encodeHashToBase64 } from "@holochain/client";

// Represent your zome's signal types in the UI.
type MyZomeSignal =
| { type: "action_written"; value: ActionHash };

// Use the connection establishment function from
// https://developer.holochain.org/build/connecting-a-front-end/#connect-to-a-happ-with-the-javascript-client
getHolochainClient().then(client => {
// Subscribe to signals.
client.on("signal", (signal: Signal) => {
// Signals coming from a coordinator zome are of the `App` type.
if (!(SignalType.App in signal)) return;
const appSignal = signal[SignalType.App];

// For now, let's just assume this is a simple hApp with only one DNA
// (hence one cell), and all we need to discriminate by is the zome
// name.
if (appSignal.zome_name != "my_zome") return;

const payload: MyZomeSignal = appSignal.payload;
switch (appSignal.payload.type) {
case "action_written":
console.log(`action hash ${encodeHashToBase64(payload.value)} written`);
}
});
});
```

## Remote signals

Agents can also send remote signals to each other using the [`send_remote_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.send_remote_signal.html) host function and a [`recv_remote_signal` callback](/build/callbacks-and-lifecycle-hooks/#define-a-recv-remote-signal-callback), which takes a single argument of any type and returns `ExternResult<()>`.

This example implements a 'heartbeat' feature, where agents can periodically ping a small number of friends to let them know they're still online.

```rust
use hdk::prelude::*;

#[hdk_extern]
pub fn init(_: ()) -> ExternResult<InitCallbackResult> {
let mut fns = BTreeSet::new();
// Open up access for the remote signal handler callback to everyone on
// the network -- see the note after this example.
fns.insert((zome_info()?.name, "recv_remote_signal".into()));
create_cap_grant(CapGrantEntry {
tag: "remote signals".into(),
access: CapAccess::Unrestricted,
functions: GrantedFunctions::Listed(fns),
})?;
Ok(InitCallbackResult::Pass)
}

// Again, it's good practice to define your remote signal type as an enum so
// you can add more message types later.
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
enum RemoteSignal {
Heartbeat,
}

#[hdk_extern]
pub fn send_heartbeat(receivers: Vec<AgentPubKey>) -> ExternResult<()> {
// Now that we're using signals, we can send the same message to multiple
// remote agents at once.
send_remote_signal(
RemoteSignal::Heartbeat,
)
}

#[hdk_extern]
pub fn recv_remote_signal(payload: RemoteSignal) -> ExternResult<()> {
if let RemoteSignal::Heartbeat = payload {
let caller = call_info()?.provenance;
// On the receiving end we forward the remote signal to the front end
// by emitting a local signal.
// On the receiving end, we forward the remote signal to the front end by emitting a local signal.
emit_signal(LocalSignal::Heartbeat(caller))?;
}
Ok(())
}
```

!!! info Remote signal handlers are just zome functions
`send_remote_signal` is sugar for a [remote call](/build/calling-zome-functions/#call-a-zome-function-from-another-agent-in-the-network) to a zome function named `recv_remote_signal`. This target function exists by convention and must be given an `Unrestricted` capability grant for this to work. <!-- TODO: link to capabilities page -->. The only difference from a regular remote call is that `send_remote_signal` doesn't block execution waiting for a response, and it doesn't return an error if anything fails. Other than that, the following two are roughly equivalent.

```rust
fn send_heartbeat_via_remote_signal(agent: AgentPubKey) -> ExternResult<()> {
send_remote_signal(RemoteSignal::Heartbeat, vec![agent])
}

fn send_heartbeat_via_remote_call(agent: AgentPubKey) -> ExternResult<()> {
// Throw away the return value of `recv_remote_signal`, which shouldn't
// contain anything meaningful anyway.
let _ = call_remote(
agent,
zome_info()?.name,
"recv_remote_signal".into(),
None,
RemoteSignal::Heartbeat
)?;
Ok(())
}
```

Take care that `recv_remote_signal` does as little as possible, to avoid people abusing it. Permissions and privileges are another topic which we'll talk about soon.<!-- TODO: delete this sentence and link to capabilities page -->

It also means that `send_remote_signal` always routes the call to a coordinator zome of the same name as the caller. Because [the remote agent might map that name to a different coordinator zome, or no zome at all](/build/calling-zome-functions/#remote-call-unknown-routing), this function might be handled in unexpected ways on the receiver's end.

Finally, remote signals open up connections to peers, so they should be used sparingly. The above heartbeat example would be very costly if everyone in a large network were sending heartbeats to each other.
!!!

## Reference

* [`hdk::p2p::emit_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.emit_signal.html)
* [`@holochain/client.AppWebsocket.prototype.on`](https://github.com/holochain/holochain-client-js/blob/main/docs/client.appwebsocket.on.md)
* [`hdk::p2p::send_remote_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.send_remote_signal.html)

## Further reading

* [Core Concepts: Signals](/concepts/9_signals/)
<!-- TODO: reference capabilities page -->