pub enum OutboundDelegateMsg {
ApplicationMessage(ApplicationMessage),
RequestUserInput(UserInputRequest<'static>),
ContextUpdated(DelegateContext),
GetContractRequest(GetContractRequest),
PutContractRequest(PutContractRequest),
UpdateContractRequest(UpdateContractRequest),
SubscribeContractRequest(SubscribeContractRequest),
SendDelegateMessage(DelegateMessage),
UnsubscribeContractRequest(UnsubscribeContractRequest),
}Expand description
Messages emitted out of a delegate’s process() function.
This is the outbound counterpart of InboundDelegateMsg and sits on the
same host↔delegate wire boundary.
§Deliberately not #[non_exhaustive]
Adding a variant here is a source-level break for any downstream crate that matches on it exhaustively. That is the intended behaviour and it should not be “fixed” by marking the enum.
Every variant of this enum is a request the host must act on. There is
one host — freenet-core — and it dispatches these in exhaustive matches with
no wildcard (crates/core/src/contract.rs, in the request loop and again in
the app-message filter). Marking this enum #[non_exhaustive] would force
those matches to grow _ => arms, and a newly added variant would then
compile against the host with no arm of its own: the delegate’s request
would fall into the wildcard, the call would appear to succeed, and nothing
would report that it did nothing.
The compile error is what stops that, and it is the only mechanism that does. Keep it.
Two honest limits on this argument, because it is easy to claim more:
- It forces an arm to exist, not a handler to be correct. This crate’s
own FlatBuffers encoder (
client_api::client_events) has explicit arms for six outbound variants that log an error and drop the message. The compile error made someone write those arms deliberately; it could not make them do anything useful. - It is not the bug behind this workstream. A delegate
SubscribeContractRequestis handled by the host today. Its defect is different and subtler: it registers no demand in the network, so the subscription does not pin the contract (freenet-core#4669). Do not read the compile-error argument as a fix for that; it is a guard against a different failure that has not happened yet, which is the point of a guard.
InboundDelegateMsg carries the opposite trade-off, and is marked: its
consumers are third-party delegate WASM, which can reasonably ignore a
variant it does not know about.
§Wire format and compatibility
bincode, variant index 0..=N in declaration order. Never insert or
reorder a variant: that silently reassigns every later tag, and deployed
delegate WASM built against an older stdlib would encode into what the host
now reads as a different variant. delegate_msg_variant_tags_are_pinned
pins every tag so a reorder fails CI rather than production.
Appending is compatible in one direction only, and this enum is the direction that bites:
- Old delegate → new host: fine, for appended VARIANTS. The host
understands every tag an older delegate can emit, so deployed delegate
WASM keeps working against an upgraded node with no rebuild. This does
not extend to appending a FIELD to an existing variant’s payload
struct, because a field breaks in the opposite direction. See
struct_field_wire_compatinclient_api::client_events. (ApplicationMessageis#[non_exhaustive], which invites precisely that edit. It is the only payload struct here that is.) - New delegate → old host: fails, and fails loudly. bincode rejects the
unknown variant tag — as
ErrorKind::Custom("invalid value: integerN, expected variant index 0 <= i < M"), since it hands the index to serde’s derived visitor rather than validating it itself — so the host surfaces a decode error on that message rather than misreading it.
There is deliberately no feature-detection handshake. A delegate cannot ask the host which variants it understands, and adding a probe would itself be a wire change with the same bootstrapping problem. The rule is therefore the blunt one: a delegate that emits a variant introduced in stdlib version X requires a host built against stdlib >= X.
Where a host function exists for the same capability, it is the better choice against older hosts. Host functions are resolved by name at module instantiation, so an import an old host does not provide fails at load time with a named missing-import error, instead of mid-protocol on a decode.
That said, the freenet_delegate_contracts namespace holds only
get_contract_state(_len) — a local read. There is no host function for
writing or subscribing, so PutContractRequest, UpdateContractRequest and
SubscribeContractRequest below are the only route for those, and the
variant rule above governs them.