-
Notifications
You must be signed in to change notification settings - Fork 31
Build guide: signals #534
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
Merged
Merged
Build guide: signals #534
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
903ec2f
feat: stub page for signals, added to navs
pdaoust 9c9ef87
feat: signals page all fleshed out
pdaoust 17f3a11
edit: move remote signal stuff from lifecycle hooks to signals page
pdaoust 0719f86
edit: simplify signal code examples
pdaoust 66bb64b
edit: fix bugs in signals code
pdaoust 821e377
chore: todo sweep for signals
pdaoust ff44ed1
Apply suggestions from code review
pdaoust dccbca5
edit: remove heartbeat-via-zome-call example
pdaoust 3f0af48
edit: send_remote_signal isn't exactly equivalent to call_zome
pdaoust ec26e49
chore: note about SignalType changing in 0.5
pdaoust f77a605
Update src/pages/build/signals.md
pdaoust 3d37b39
Update src/pages/build/signals.md
pdaoust d381f30
Update src/pages/build/signals.md
pdaoust 5e746c5
edit: send-and-forget paragraph
pdaoust fa1b8f7
edit: listening for signals
pdaoust d2421fb
edit: system signals aren't useless
pdaoust fc2d085
edit: heartbeat pattern could be costly
pdaoust 09e0473
edit: language about remote signals being sugar
pdaoust b623e47
edit: remove doubled-up mention of unrestricted grants for remote sig…
pdaoust 7c93df5
Update src/pages/build/signals.md
pdaoust File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())); | ||
} | ||
} | ||
``` | ||
|
||
pdaoust marked this conversation as resolved.
Show resolved
Hide resolved
|
||
### 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; | ||
matthme marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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<()>`. | ||
|
||
pdaoust marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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<()> { | ||
ThetaSinner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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))?; | ||
pdaoust marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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 --> |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.