Skip to content

Hail

A hail is a channel for one value. Rust holds the value, JS gets a signal that tracks it.

Two constructors, and the choice decides what JS gets.

fn run_hail(key: Hail) -> JsValue {
let state = use_context::<Stock<State>>().unwrap();
match key {
// read-write: JS gets a value and a setter
Hail::Count => state.count().set_hail::<Converter>(),
// read-only: JS gets a value
Hail::Doubled => state.count().memo(|c| *c * 2).set_read_hail::<Converter>(),
}
}

set_hail needs something writable. set_read_hail works on anything you can read.

You have Can use
Stock<T> both
ReadStock<T> set_read_hail
Memo<T> set_read_hail
Resource<T> set_read_hail

Like run_pier, run_hail runs once per key: when a component first asks for it. After that the value is pushed.

const pier = usePier();
const [count, setCount] = pier.hail("Count"); // () => number
const doubled = pier.readHail("Doubled"); // () => number
setCount(count() + 1);

Each adapter returns whatever is idiomatic for that framework. A Vue hail is a writable ref, so v-model works on it directly. A Svelte hail is a store, so $count works.

A key is any value your enum can be. Give a variant a field and the key becomes an object.

#[derive(Rets, Serialize, Deserialize)]
pub enum Hail {
#[ret(Vec<i32>)]
Items,
#[ret(Option<i32>)]
Item(usize),
}
pier.readHail("Items"); // number[]
pier.readHail({ Item: 3 }); // number | undefined

That is plain serde. No constructors, no wrapper objects. The key is the value you write.

These two keys read an items: Vec<i32> field on the app’s state, reached with state.items(). Those accessors come from #[derive(Stock)]. See deriving stocks when you get to the Rust side.

Some values are not always there. An index past the end of a Vec, a missing map key, a field on the wrong enum variant.

Those become OptStock<T> on the Rust side and undefined on the JS side, so declare the ret as Option<T>:

#[ret(Option<i32>)]
Item(usize),
Hail::Item(index) => state.items().get(index).set_hail::<Converter>(),

Writing to an absent path is simply ignored. It does not panic.

Items is a plain list. Item(0) is derived from it: writable, and undefined once the list is empty.

Push and pop go through tells; the +1 button writes straight into the derived path.

Items.tsx
import { usePier } from "../../setup/solid/bridge";
export default function Items() {
const pier = usePier();
const items = pier.readHail("Items"); // () => number[]
const [first, setFirst] = pier.hail({ Item: 0 }); // path-derived, writable
return (
<div class="demo">
<p>
items: <b id="items">{items().join(", ") || "(empty)"}</b>
</p>
<p>
item 0: <b id="item0">{first() ?? "undefined"}</b>
</p>
<button id="push" onClick={() => pier.tell({ PushItem: items().length * 10 })}>
push
</button>
<button id="pop" onClick={() => pier.tell("PopItem")}>
pop
</button>
<button id="bump" onClick={() => setFirst((first() ?? 0) + 1)}>
+1 on item 0
</button>
</div>
);
}

Running now, in your browser

Watch what happens when you pop the list empty: item 0 becomes undefined rather than throwing, and +1 on it does nothing.

Ahoi tracks which value each hail actually read. When you write, only the hails that depend on that value are recomputed.

Derivation is path-selective. Writing items[0] notifies subscribers of that path and of its ancestors, but not of items[1].

Recomputed hails are collected and sent to JS in a single batch per propagation cycle.

One write that touches twenty hails costs one crossing, not twenty. You do not need to batch anything by hand.

Hails are how values come out. Tells are how commands go in.