From 903ec2fcd9955f7de6ae15c60fd6fc1bf868d8c1 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 13 Feb 2025 13:27:47 -0800 Subject: [PATCH 01/20] feat: stub page for signals, added to navs --- src/pages/_data/navigation/mainNav.json5 | 1 + src/pages/build/connecting-the-parts.md | 2 +- src/pages/build/index.md | 2 +- src/pages/build/signals.md | 3 +++ 4 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/pages/build/signals.md diff --git a/src/pages/_data/navigation/mainNav.json5 b/src/pages/_data/navigation/mainNav.json5 index 547978023..b0fcb1156 100644 --- a/src/pages/_data/navigation/mainNav.json5 +++ b/src/pages/_data/navigation/mainNav.json5 @@ -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/" }, ]}, ] }, diff --git a/src/pages/build/connecting-the-parts.md b/src/pages/build/connecting-the-parts.md index aee9709b7..fe88be7d7 100644 --- a/src/pages/build/connecting-the-parts.md +++ b/src/pages/build/connecting-the-parts.md @@ -9,7 +9,7 @@ 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 diff --git a/src/pages/build/index.md b/src/pages/build/index.md index c2fac32e9..040b214cc 100644 --- a/src/pages/build/index.md +++ b/src/pages/build/index.md @@ -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 \ No newline at end of file +* [Signals](/build/signals) --- receiving notifications from cells \ No newline at end of file diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md new file mode 100644 index 000000000..841661709 --- /dev/null +++ b/src/pages/build/signals.md @@ -0,0 +1,3 @@ +--- +title: "Signals" +--- \ No newline at end of file From 9c9ef87ec6d81d53b931f7bea67fae0db8f6f13b Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 18 Feb 2025 10:21:02 -0800 Subject: [PATCH 02/20] feat: signals page all fleshed out --- .../build/callbacks-and-lifecycle-hooks.md | 19 +- src/pages/build/calling-zome-functions.md | 2 +- src/pages/build/signals.md | 168 +++++++++++++++++- 3 files changed, 169 insertions(+), 20 deletions(-) diff --git a/src/pages/build/callbacks-and-lifecycle-hooks.md b/src/pages/build/callbacks-and-lifecycle-hooks.md index c2235efde..86fcdb2af 100644 --- a/src/pages/build/callbacks-and-lifecycle-hooks.md +++ b/src/pages/build/callbacks-and-lifecycle-hooks.md @@ -126,24 +126,7 @@ 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 { - 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**, 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 diff --git a/src/pages/build/calling-zome-functions.md b/src/pages/build/calling-zome-functions.md index 11c6f6c3f..474b21d17 100644 --- a/src/pages/build/calling-zome-functions.md +++ b/src/pages/build/calling-zome-functions.md @@ -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. !!! diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 841661709..ec8c7352e 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -1,3 +1,169 @@ --- title: "Signals" ---- \ No newline at end of file +--- + +::: 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. +::: + +## 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. 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. + +```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")] +pub enum LocalSignal { + Initializing, + Heartbeat(AgentPubKey), + ActionWritten(ActionHash), +} + +#[hdk_extern] +pub fn init() -> ExternResult { + // Emit a signal saying that this cell is starting up. + emit_signal(LocalSignal::Initializing)?; + Ok(InitCallbackResult::Pass) +} + +// This function might be called by another agent in the network to show +// they're still online, in which case the local UI will want to be notified. +#[hdk_extern] +pub fn heartbeat() -> ExternResult<()> { + // Find out who called our heartbeat function. + let caller = call_info()?.provenance; + // Tell the UI that the caller is still online. + emit_signal(LocalSignal::Heartbeat(caller))?; + Ok(()) +} + +#[hdk_extern(infallible)] +pub fn post_commit(committed_actions: Vec) { + // 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 + +The UI subscribes 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 can discriminate between them by cell ID and zome name. + +```typescript +import type { Signal, AppSignal, AgentPubKey } from "@holochain/client"; +import { SignalType, encodeHashToBase64 } from "@holochain/client"; + +// Duplicate your zome's signal types in the UI. +type MyZomeSignal = + { type: "initializing" } + | { type: "heartbeat"; content: AgentPubKey } + | { type: "action_written"; content: 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) => { + // There's currently only one useful signal type to listen for -- an + // app signal. + 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; + switch (appSignal.payload.type) { + case "initializing": + console.log("my_zome is initializing"); + break; + case "heartbeat": + console.log(`agent ${encodeHashToBase64(appSignal.payload.content)} is still online`); + break; + case "action_written": + console.log(`action hash ${encodeHashToBase64(appSignal.payload.content)} written`); + } + }); +}); +``` + +## Remote signals for it, so other agents can call it. + +`send_remote_signal` works differently from a usual remote call, in that it's 'send-and-forget' --- it won't return an error if anything fails. + +!!! info Remote signals are routed to a coordinator zome of the same name +While you can remote-call any coordinator zome's functions, `send_remote_signal` always routes the call to a coordinator zome of the same name as the caller. Because [the remote agent might map a different coordinator zome to that name, 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. +!!! + +This example rewrites and expands the `heartbeat` function above to use remote signals. + +```rust +use hdk::prelude::*; + +#[hdk_extern] +pub fn init(_: ()) -> ExternResult { + let mut fns = BTreeSet::new(); + // Open up access for the remote signal handler callback to everyone on + // the network. + 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) +} + +// 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")] +enum RemoteSignal { + Heartbeat, +} + +#[hdk_extern] +pub fn recv_remote_signal(payload: Signal) -> ExternResult<()> { + if let Signal::Heartbeat = payload { + // Pass the heartbeat along to my UI so it can update my peer's + // online status. + let caller = call_info()?.provenance; + emit_signal(LocalSignal::Heartbeat(caller))?; + } + Ok(()) +} + +// The UI calls this function at regular intervals, for all the peers that the +// agent wants to stay connected to. +#[hdk_extern] +pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { + send_remote_signal( + RemoteSignal::Heartbeat, + agents: receivers + ) +} +``` + +## 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/) +* [Build Guide: Callbacks and Lifecycle Hooks: `recv_remote_signal` callback](/build/callbacks-and-lifecycle-hooks/#define-a-recv-remote-signal-callback) + \ No newline at end of file From 17f3a11822ef9489a77f4322e4836b3d3c5e23cc Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 18 Feb 2025 10:46:22 -0800 Subject: [PATCH 03/20] edit: move remote signal stuff from lifecycle hooks to signals page --- .../build/callbacks-and-lifecycle-hooks.md | 66 ++----------------- src/pages/build/signals.md | 42 +++++++++--- 2 files changed, 36 insertions(+), 72 deletions(-) diff --git a/src/pages/build/callbacks-and-lifecycle-hooks.md b/src/pages/build/callbacks-and-lifecycle-hooks.md index 86fcdb2af..aad20e910 100644 --- a/src/pages/build/callbacks-and-lifecycle-hooks.md +++ b/src/pages/build/callbacks-and-lifecycle-hooks.md @@ -134,68 +134,9 @@ The `init` callback is often used to set up initial **capabilities** -!!! +See the [Signals](/build/signals/#remote-signals) page for an example implementation. ### Define a `post_commit` callback @@ -303,4 +244,5 @@ pub fn update_movie(input: UpdateMovieInput) -> ExternResult { * [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 +* [Build Guide: Identifiers](/build/identifiers/) +* [Build Guide: Signals](/build/signals/) \ No newline at end of file diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index ec8c7352e..04ff4025c 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -97,15 +97,9 @@ getHolochainClient().then(client => { }); ``` -## Remote signals for it, so other agents can call it. - -`send_remote_signal` works differently from a usual remote call, in that it's 'send-and-forget' --- it won't return an error if anything fails. - -!!! info Remote signals are routed to a coordinator zome of the same name -While you can remote-call any coordinator zome's functions, `send_remote_signal` always routes the call to a coordinator zome of the same name as the caller. Because [the remote agent might map a different coordinator zome to that name, 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. -!!! +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 rewrites and expands the `heartbeat` function above to use remote signals. @@ -116,7 +110,7 @@ use hdk::prelude::*; pub fn init(_: ()) -> ExternResult { let mut fns = BTreeSet::new(); // Open up access for the remote signal handler callback to everyone on - // the network. + // the network -- see the note after this example. fns.insert((zome_info()?.name, "recv_remote_signal".into())); create_cap_grant(CapGrantEntry { tag: "".into(), @@ -156,6 +150,35 @@ pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { } ``` +!!! 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` with no capability secret. It works differently from a usual remote call, though, in that it's 'send-and-forget' --- it won't return an error if anything fails. In practice, these two are equivalent: + +```rust +use hdk::prelude::*; + +fn send_remote_hello(hello: String, agent: AgentPubKey) -> ExternResult<()> { + send_remote_signal(hello, agent) +} + +fn send_remote_hello_via_remote_call(hello: String, 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", + None, + hello + )?; + Ok(()) +} +``` + +This means an agent needs to set up an [`Unrestricted` capability grant](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/capability/enum.CapAccess.html#variant.Unrestricted) for it, so other agents can call it. Take care that this function does as little as possible, to avoid people abusing it. Permissions and privileges are another topic which we'll talk about soon. + +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. +!!! + ## Reference * [`hdk::p2p::emit_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.emit_signal.html) @@ -165,5 +188,4 @@ pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { ## Further reading * [Core Concepts: Signals](/concepts/9_signals/) -* [Build Guide: Callbacks and Lifecycle Hooks: `recv_remote_signal` callback](/build/callbacks-and-lifecycle-hooks/#define-a-recv-remote-signal-callback) \ No newline at end of file From 0719f8631de35fd192b899efd676807fbb8076f1 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 18 Feb 2025 11:51:39 -0800 Subject: [PATCH 04/20] edit: simplify signal code examples --- src/pages/build/signals.md | 106 +++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 04ff4025c..6ed691fb7 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -14,45 +14,77 @@ title: "Signals" Your coordinator zome emits a signal with the [`emit_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.emit_signal.html) host function. 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. ([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")] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] pub enum LocalSignal { - Initializing, - Heartbeat(AgentPubKey), ActionWritten(ActionHash), } +#[hdk_extern(infallible)] +pub fn post_commit(committed_actions: Vec) { + // 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())); + } +} +``` + +This example sets up a 'heartbeat' feature, where peers can periodically ping each other to let them know they're still online. + +```rust +use hdk::prelude::*; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum LocalSignal { + Heartbeat(AgentPubKey), +} + #[hdk_extern] pub fn init() -> ExternResult { - // Emit a signal saying that this cell is starting up. - emit_signal(LocalSignal::Initializing)?; + // Let all agents send heartbeat messages to each other. + let mut fns = BTreeSet::new(); + fns.insert((zome_info()?.name, "receive_heartbeat".into())); + create_cap_grant(CapGrantEntry { + tag: "".into(), + access: CapAccess::Unrestricted, + functions: GrantedFunctions::Listed(fns), + })?; Ok(InitCallbackResult::Pass) } -// This function might be called by another agent in the network to show -// they're still online, in which case the local UI will want to be notified. +// An agent's UI calls this function periodically to let others know they're +// online. #[hdk_extern] -pub fn heartbeat() -> ExternResult<()> { +pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { + for agent in receivers { + call_remote( + agent, + zome_info()?.name, + "receive_heartbeat", + None, + (), + )?; + } + Ok(()) +} + +#[hdk_extern] +pub fn receive_heartbeat() -> ExternResult<()> { // Find out who called our heartbeat function. let caller = call_info()?.provenance; // Tell the UI that the caller is still online. emit_signal(LocalSignal::Heartbeat(caller))?; Ok(()) } - -#[hdk_extern(infallible)] -pub fn post_commit(committed_actions: Vec) { - // 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 @@ -65,7 +97,6 @@ import { SignalType, encodeHashToBase64 } from "@holochain/client"; // Duplicate your zome's signal types in the UI. type MyZomeSignal = - { type: "initializing" } | { type: "heartbeat"; content: AgentPubKey } | { type: "action_written"; content: ActionHash }; @@ -84,9 +115,6 @@ getHolochainClient().then(client => { // name. if (appSignal.zome_name != "my_zome") return; switch (appSignal.payload.type) { - case "initializing": - console.log("my_zome is initializing"); - break; case "heartbeat": console.log(`agent ${encodeHashToBase64(appSignal.payload.content)} is still online`); break; @@ -123,44 +151,40 @@ pub fn init(_: ()) -> ExternResult { // 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")] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] enum RemoteSignal { Heartbeat, } -#[hdk_extern] -pub fn recv_remote_signal(payload: Signal) -> ExternResult<()> { - if let Signal::Heartbeat = payload { - // Pass the heartbeat along to my UI so it can update my peer's - // online status. - let caller = call_info()?.provenance; - emit_signal(LocalSignal::Heartbeat(caller))?; - } - Ok(()) -} - -// The UI calls this function at regular intervals, for all the peers that the -// agent wants to stay connected to. #[hdk_extern] pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { + // Now that we're using signals, we can send the same message to multiple + // remote agents at once. send_remote_signal( RemoteSignal::Heartbeat, agents: receivers ) } + +#[hdk_extern] +pub fn recv_remote_signal(payload: Signal) -> ExternResult<()> { + if let RemoteSignal::Heartbeat = payload { + let caller = call_info()?.provenance; + 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` with no capability secret. It works differently from a usual remote call, though, in that it's 'send-and-forget' --- it won't return an error if anything fails. In practice, these two are equivalent: ```rust -use hdk::prelude::*; - -fn send_remote_hello(hello: String, agent: AgentPubKey) -> ExternResult<()> { - send_remote_signal(hello, agent) +fn send_heartbeat(agent: AgentPubKey) -> ExternResult<()> { + send_remote_signal(RemoteSignal::Heartbeat, agent) } -fn send_remote_hello_via_remote_call(hello: String, agent: AgentPubKey) -> ExternResult<()> { +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( @@ -168,7 +192,7 @@ fn send_remote_hello_via_remote_call(hello: String, agent: AgentPubKey) -> Exter zome_info()?.name, "recv_remote_signal", None, - hello + RemoteSignal::Heartbeat )?; Ok(()) } From 66bb64bc0c83334af86f3075696101a31d885edf Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 18 Feb 2025 14:04:50 -0800 Subject: [PATCH 05/20] edit: fix bugs in signals code --- src/pages/build/signals.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 6ed691fb7..dfc581389 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -14,7 +14,7 @@ title: "Signals" Your coordinator zome emits a signal with the [`emit_signal`](https://docs.rs/hdk/latest/hdk/p2p/fn.emit_signal.html) host function. 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. ([Read about the `post_commit` callback](/build/callbacks-and-lifecycle-hooks/#define-a-post-commit-callback) to learn more about hooking into successful writes.) +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//zomes/coordinator//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::*; @@ -37,7 +37,7 @@ pub fn post_commit(committed_actions: Vec) { } ``` -This example sets up a 'heartbeat' feature, where peers can periodically ping each other to let them know they're still online. +This example shows a 'heartbeat' feature, where peers can periodically ping each other to let them know they're still online. ```rust use hdk::prelude::*; @@ -69,7 +69,7 @@ pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { call_remote( agent, zome_info()?.name, - "receive_heartbeat", + "receive_heartbeat".into(), None, (), )?; @@ -97,8 +97,8 @@ import { SignalType, encodeHashToBase64 } from "@holochain/client"; // Duplicate your zome's signal types in the UI. type MyZomeSignal = - | { type: "heartbeat"; content: AgentPubKey } - | { type: "action_written"; content: ActionHash }; + | { type: "heartbeat"; value: AgentPubKey } + | { 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 @@ -114,12 +114,14 @@ getHolochainClient().then(client => { // (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 "heartbeat": - console.log(`agent ${encodeHashToBase64(appSignal.payload.content)} is still online`); + console.log(`agent ${encodeHashToBase64(payload.value)} is still online`); break; case "action_written": - console.log(`action hash ${encodeHashToBase64(appSignal.payload.content)} written`); + console.log(`action hash ${encodeHashToBase64(payload.value)} written`); } }); }); @@ -162,12 +164,11 @@ pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { // remote agents at once. send_remote_signal( RemoteSignal::Heartbeat, - agents: receivers ) } #[hdk_extern] -pub fn recv_remote_signal(payload: Signal) -> ExternResult<()> { +pub fn recv_remote_signal(payload: RemoteSignal) -> ExternResult<()> { if let RemoteSignal::Heartbeat = payload { let caller = call_info()?.provenance; emit_signal(LocalSignal::Heartbeat(caller))?; @@ -181,7 +182,7 @@ pub fn recv_remote_signal(payload: Signal) -> ExternResult<()> { ```rust fn send_heartbeat(agent: AgentPubKey) -> ExternResult<()> { - send_remote_signal(RemoteSignal::Heartbeat, agent) + send_remote_signal(RemoteSignal::Heartbeat, vec![agent]) } fn send_heartbeat_via_remote_call(agent: AgentPubKey) -> ExternResult<()> { @@ -190,7 +191,7 @@ fn send_heartbeat_via_remote_call(agent: AgentPubKey) -> ExternResult<()> { let _ = call_remote( agent, zome_info()?.name, - "recv_remote_signal", + "recv_remote_signal".into(), None, RemoteSignal::Heartbeat )?; From 821e377042454f7ec3a09867b5d1dd9279b8cddb Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Tue, 18 Feb 2025 14:07:19 -0800 Subject: [PATCH 06/20] chore: todo sweep for signals --- src/pages/build/callbacks-and-lifecycle-hooks.md | 2 -- src/pages/build/connecting-the-parts.md | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/build/callbacks-and-lifecycle-hooks.md b/src/pages/build/callbacks-and-lifecycle-hooks.md index aad20e910..3701496be 100644 --- a/src/pages/build/callbacks-and-lifecycle-hooks.md +++ b/src/pages/build/callbacks-and-lifecycle-hooks.md @@ -130,8 +130,6 @@ The `init` callback is often used to set up initial **capabilities** - 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. 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. diff --git a/src/pages/build/connecting-the-parts.md b/src/pages/build/connecting-the-parts.md index fe88be7d7..f796eb509 100644 --- a/src/pages/build/connecting-the-parts.md +++ b/src/pages/build/connecting-the-parts.md @@ -15,7 +15,7 @@ title: "Connecting the Parts" ::: 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**, adapted for peer-to-peer applications. -The back end can also send out [**signals**](/concepts/9_signals/) 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} @@ -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! +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 From ff44ed1c2d58bc43efd0cbb3264b3c98f37b6871 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 21 Feb 2025 10:21:55 -0800 Subject: [PATCH 07/20] Apply suggestions from code review Co-authored-by: matthme <36768177+matthme@users.noreply.github.com> --- src/pages/build/signals.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index dfc581389..9c9c19e65 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -54,7 +54,7 @@ pub fn init() -> ExternResult { let mut fns = BTreeSet::new(); fns.insert((zome_info()?.name, "receive_heartbeat".into())); create_cap_grant(CapGrantEntry { - tag: "".into(), + tag: "heartbeat".into(), access: CapAccess::Unrestricted, functions: GrantedFunctions::Listed(fns), })?; @@ -143,7 +143,7 @@ pub fn init(_: ()) -> ExternResult { // the network -- see the note after this example. fns.insert((zome_info()?.name, "recv_remote_signal".into())); create_cap_grant(CapGrantEntry { - tag: "".into(), + tag: "remote signals".into(), access: CapAccess::Unrestricted, functions: GrantedFunctions::Listed(fns), })?; From dccbca5b0c8832ce2daed157193fb343b9de2b80 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 21 Feb 2025 10:24:02 -0800 Subject: [PATCH 08/20] edit: remove heartbeat-via-zome-call example --- src/pages/build/signals.md | 58 ++------------------------------------ 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 9c9c19e65..14da467c4 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -37,56 +37,6 @@ pub fn post_commit(committed_actions: Vec) { } ``` -This example shows a 'heartbeat' feature, where peers can periodically ping each other to let them know they're still online. - -```rust -use hdk::prelude::*; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type", content = "value", rename_all = "snake_case")] -pub enum LocalSignal { - Heartbeat(AgentPubKey), -} - -#[hdk_extern] -pub fn init() -> ExternResult { - // Let all agents send heartbeat messages to each other. - let mut fns = BTreeSet::new(); - fns.insert((zome_info()?.name, "receive_heartbeat".into())); - create_cap_grant(CapGrantEntry { - tag: "heartbeat".into(), - access: CapAccess::Unrestricted, - functions: GrantedFunctions::Listed(fns), - })?; - Ok(InitCallbackResult::Pass) -} - -// An agent's UI calls this function periodically to let others know they're -// online. -#[hdk_extern] -pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { - for agent in receivers { - call_remote( - agent, - zome_info()?.name, - "receive_heartbeat".into(), - None, - (), - )?; - } - Ok(()) -} - -#[hdk_extern] -pub fn receive_heartbeat() -> ExternResult<()> { - // Find out who called our heartbeat function. - let caller = call_info()?.provenance; - // Tell the UI that the caller is still online. - emit_signal(LocalSignal::Heartbeat(caller))?; - Ok(()) -} -``` - ### Listen for a signal The UI subscribes 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 can discriminate between them by cell ID and zome name. @@ -97,7 +47,6 @@ import { SignalType, encodeHashToBase64 } from "@holochain/client"; // Duplicate your zome's signal types in the UI. type MyZomeSignal = - | { type: "heartbeat"; value: AgentPubKey } | { type: "action_written"; value: ActionHash }; // Use the connection establishment function from @@ -117,9 +66,6 @@ getHolochainClient().then(client => { const payload: MyZomeSignal = appSignal.payload; switch (appSignal.payload.type) { - case "heartbeat": - console.log(`agent ${encodeHashToBase64(payload.value)} is still online`); - break; case "action_written": console.log(`action hash ${encodeHashToBase64(payload.value)} written`); } @@ -131,7 +77,7 @@ getHolochainClient().then(client => { 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 rewrites and expands the `heartbeat` function above to use remote signals. +This example implements a 'heartbeat' feature, where peers can periodically ping each other to let them know they're still online. ```rust use hdk::prelude::*; @@ -171,6 +117,8 @@ pub fn send_heartbeat(receivers: Vec) -> ExternResult<()> { 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. emit_signal(LocalSignal::Heartbeat(caller))?; } Ok(()) From 3f0af4825428c4372dedee44508cb9f94f206b18 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 21 Feb 2025 10:24:33 -0800 Subject: [PATCH 09/20] edit: send_remote_signal isn't exactly equivalent to call_zome --- src/pages/build/signals.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 14da467c4..319983086 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -126,10 +126,10 @@ pub fn recv_remote_signal(payload: RemoteSignal) -> ExternResult<()> { ``` !!! 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` with no capability secret. It works differently from a usual remote call, though, in that it's 'send-and-forget' --- it won't return an error if anything fails. In practice, these two are equivalent: +`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` with no capability secret. It works differently from a usual remote call, though, in that it's 'send-and-forget' --- it 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(agent: AgentPubKey) -> ExternResult<()> { +fn send_heartbeat_via_remote_signal(agent: AgentPubKey) -> ExternResult<()> { send_remote_signal(RemoteSignal::Heartbeat, vec![agent]) } From ec26e49d0eb98930319410de5e3b22c3fc9a750d Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Fri, 21 Feb 2025 10:25:43 -0800 Subject: [PATCH 10/20] chore: note about SignalType changing in 0.5 --- src/pages/build/signals.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 319983086..26b5a74fa 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -41,6 +41,8 @@ pub fn post_commit(committed_actions: Vec) { The UI subscribes 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 can discriminate between them by cell ID and zome name. + + ```typescript import type { Signal, AppSignal, AgentPubKey } from "@holochain/client"; import { SignalType, encodeHashToBase64 } from "@holochain/client"; From f77a60540fcf5301c304ed5ead2c4d975f760b7c Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 13:53:57 -0800 Subject: [PATCH 11/20] Update src/pages/build/signals.md Co-authored-by: ThetaSinner --- src/pages/build/signals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 26b5a74fa..8c1bd3eb3 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -12,7 +12,7 @@ title: "Signals" ### 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. 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. +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//zomes/coordinator//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.) From 3d37b39087c4351e21844796dffcda678a775d23 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 13:54:45 -0800 Subject: [PATCH 12/20] Update src/pages/build/signals.md Co-authored-by: ThetaSinner --- src/pages/build/signals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 8c1bd3eb3..1cd256ce8 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -47,7 +47,7 @@ The UI subscribes to signals with the [`AppWebsocket.prototype.on`](https://gith import type { Signal, AppSignal, AgentPubKey } from "@holochain/client"; import { SignalType, encodeHashToBase64 } from "@holochain/client"; -// Duplicate your zome's signal types in the UI. +// Represent your zome's signal types in the UI. type MyZomeSignal = | { type: "action_written"; value: ActionHash }; From d381f3035594cf2b2396fec8b05b42b26c0f206c Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 13:55:30 -0800 Subject: [PATCH 13/20] Update src/pages/build/signals.md Co-authored-by: ThetaSinner --- src/pages/build/signals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 1cd256ce8..cff06a1d6 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -128,7 +128,7 @@ pub fn recv_remote_signal(payload: RemoteSignal) -> ExternResult<()> { ``` !!! 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` with no capability secret. It works differently from a usual remote call, though, in that it's 'send-and-forget' --- it 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. +`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. . It works differently from a usual remote call, though, in that it's 'send-and-forget' --- it 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<()> { From 5e746c562d693141d284ca33ee215f1be7f1fc0a Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 14:20:50 -0800 Subject: [PATCH 14/20] edit: send-and-forget paragraph --- src/pages/build/signals.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index cff06a1d6..3e669081d 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -6,6 +6,10 @@ title: "Signals" **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. From fa1b8f7d0753d2583d592984e1d00a3339e8698b Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 14:21:33 -0800 Subject: [PATCH 15/20] edit: listening for signals --- src/pages/build/signals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 3e669081d..7fc3ea7ae 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -43,7 +43,7 @@ pub fn post_commit(committed_actions: Vec) { ### Listen for a signal -The UI subscribes 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 can discriminate between them by cell ID and zome name. +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. From d2421fb100256998833555f9608b14f2022321e7 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 14:21:49 -0800 Subject: [PATCH 16/20] edit: system signals aren't useless --- src/pages/build/signals.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 7fc3ea7ae..30daf617d 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -60,8 +60,7 @@ type MyZomeSignal = getHolochainClient().then(client => { // Subscribe to signals. client.on("signal", (signal: Signal) => { - // There's currently only one useful signal type to listen for -- an - // app signal. + // Signals coming from a coordinator zome are of the `App` type. if (!(SignalType.App in signal)) return; const appSignal = signal[SignalType.App]; From fc2d085109c539fbd4492e653272d6abd4b701f3 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 14:22:11 -0800 Subject: [PATCH 17/20] edit: heartbeat pattern could be costly --- src/pages/build/signals.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 30daf617d..6e91734b3 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -82,7 +82,7 @@ getHolochainClient().then(client => { 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 peers can periodically ping each other to let them know they're still online. +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::*; @@ -155,6 +155,8 @@ fn send_heartbeat_via_remote_call(agent: AgentPubKey) -> ExternResult<()> { This means an agent needs to set up an [`Unrestricted` capability grant](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/capability/enum.CapAccess.html#variant.Unrestricted) for it, so other agents can call it. Take care that this function does as little as possible, to avoid people abusing it. Permissions and privileges are another topic which we'll talk about soon. 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 From 09e0473a5ae1ba7ab2da792df997ac6de54b9ce0 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 14:22:30 -0800 Subject: [PATCH 18/20] edit: language about remote signals being sugar --- src/pages/build/signals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 6e91734b3..565274478 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -131,7 +131,7 @@ pub fn recv_remote_signal(payload: RemoteSignal) -> ExternResult<()> { ``` !!! 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. . It works differently from a usual remote call, though, in that it's 'send-and-forget' --- it 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. +`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. . 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<()> { From b623e47ef3e559141e977fe8c4bbe8d7563f4baa Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Wed, 26 Feb 2025 14:22:52 -0800 Subject: [PATCH 19/20] edit: remove doubled-up mention of unrestricted grants for remote signal handler --- src/pages/build/signals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index 565274478..af6454491 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -152,7 +152,7 @@ fn send_heartbeat_via_remote_call(agent: AgentPubKey) -> ExternResult<()> { } ``` -This means an agent needs to set up an [`Unrestricted` capability grant](https://docs.rs/holochain_integrity_types/latest/holochain_integrity_types/capability/enum.CapAccess.html#variant.Unrestricted) for it, so other agents can call it. Take care that this function does as little as possible, to avoid people abusing it. Permissions and privileges are another topic which we'll talk about soon. +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. 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. From 7c93df5ad8f61488a460ff474560b89908e4ef19 Mon Sep 17 00:00:00 2001 From: Paul d'Aoust Date: Thu, 27 Feb 2025 12:36:12 -0800 Subject: [PATCH 20/20] Update src/pages/build/signals.md Co-authored-by: matthme <36768177+matthme@users.noreply.github.com> --- src/pages/build/signals.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/build/signals.md b/src/pages/build/signals.md index af6454491..20e50cb16 100644 --- a/src/pages/build/signals.md +++ b/src/pages/build/signals.md @@ -124,6 +124,7 @@ pub fn recv_remote_signal(payload: RemoteSignal) -> ExternResult<()> { 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(())