feat: scheduled wakeup primitive for delegates (ScheduleWakeup / WakeupFired) - #82
feat: scheduled wakeup primitive for delegates (ScheduleWakeup / WakeupFired)#82sanity wants to merge 1 commit into
Conversation
…upFired)
Add two variants to the host<->delegate protocol enums so a delegate can
schedule future execution without a connected UI (key rotation, TTL pruning,
scheduled publication):
- OutboundDelegateMsg::ScheduleWakeup { at: SystemTime, tag: Vec<u8> }
- InboundDelegateMsg::WakeupFired { tag: Vec<u8> }
Both are appended at the end of their enums (bincode variant tag 8), so the
change is wire-compatible for every existing variant: delegate WASM compiled
against an older stdlib keeps deserializing everything it already understood.
New wire-format pin tests freeze the tags. OutboundDelegateMsg stays exhaustive
(deliberately not #[non_exhaustive]) so the host must handle every outbound
variant; the inaccurate doc claiming it was already non_exhaustive is corrected.
Bumps 0.8.2 -> 0.8.3. Host-side implementation lands in freenet-core#3972.
Refs: freenet/freenet-core#3972
[AI-assisted - Claude]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8mqiskQracG7CDVLSxDJC
|
Host-side consumer: freenet/freenet-core#4666 (issue #3972). This stdlib PR must merge + publish 0.8.3 first (stdlib-first), then the core PR can build. [AI-assisted - Claude] |
|
Coordination note for whoever picks this up: tag 8 is now taken, and this PR needs to move to 9. Delegate unsubscribe ( Why it needed settling rather than being left to merge order: these enums are bincode-encoded with the variant index in declaration order, so two independent "append at the end" changes silently renumber each other. Nothing caught that before — #98 adds a pin asserting the tag of every variant in both enums, backed by an exhaustive match inside the crate so a new variant is a compile error until it is pinned. Once #98 merges, a colliding append fails CI loudly instead of shipping a wire break. Two other things worth knowing before rebasing, neither of them new to this PR but both easy to miss:
Nothing here is a judgment on the wakeup design itself, which is unchanged and still wanted — freenet-core#5467 Phase 2 tracks it, and notes that the host half (freenet-core#4666, branch [AI-assisted - Claude] |
|
Recording a tag decision here so it does not live only in a working session, since whoever rebases this will not have seen it. Ian ruled that unsubscribe takes tag 8; this PR moves to tag 9. freenet-stdlib#98 appends Both enums are bincode-encoded with the variant index in declaration order, so two independent "append at the end" branches silently renumber each other. That is now a loud failure rather than a silent one: #98 adds a per-variant tag pin whose map is an exhaustive Two other things worth knowing before picking this up, neither a criticism of the change itself:
[AI-assisted - Claude] |
Re-review of the unsubscribe commit found one real design mistake, the oracle problem in my own new test, and a false claim I had just reintroduced. The exactly-full re-check was wrong twice over. It re-called the length function whenever the read came back exactly filling the buffer, meaning to catch a set that had grown. But an exactly-full buffer is the NORMAL result, not an edge case: len is derived from the same set the read serialises. So it doubled a scan the docs describe as O(all contracts with any delegate subscription) on every non-empty call, and it could fail a correct read by reporting ERR_BUFFER_TOO_SMALL when a subscription happened to arrive in between. It was not even sound: grow-then-shrink passes it. Removed. The import contract already requires the host to return ERR_BUFFER_TOO_SMALL rather than truncate, so a short write means the set shrank, and completeness rests on that contract, which is why the contract is stated on the import rather than implied. The decisions now live in validate_list_len and resolve_written, which are pure and compiled on every target. That matters because CI runs cargo test on the host only; the wasm32 matrix entries build and lint but execute nothing, so everything previously inside cfg(target_family = "wasm") was type-checked and never run. Ten table-driven tests now cover the branches, including the wasm32 truncation case (1 << 32 as usize is 0 there, which would have surfaced as an empty list). My round-trip test for the new unsubscribe pair proved only that the code agrees with itself. Both structs' docs say the field ORDER is the wire format, and a round-trip through this crate's own encoder cannot establish that — swapping contract_id and result would round-trip just as happily. Both layouts are now frozen as hand-written bytes, the inbound half asserts the VALUES rather than just the variant, and the Err(String) path is exercised since it has a different bincode shape from Ok. Reintroduced false claim, the same defect class as this PR's headline fix: the doc said "several of them are #[non_exhaustive]" of the payload structs. Exactly one is, ApplicationMessage. Corrected and named. Also: the #82 note asserted that PR takes tag 9, which it does not yet — it still declares 8, so the text now says it must move and that the pin will catch whichever lands second. The unknown-tag probe gained an outbound control to match the inbound one. The terminality fixture's non-zero guard asserted some byte was non-zero when what ends_with relies on is a non-zero TAIL. Fixes a pre-existing bug found in review: get_context and get_mut_context returned None for UserResponse, which carries a context, because a `_ => None` wildcard swallowed the missing arm and nothing in the crate called either accessor. Arm added, both accessors are now exhaustive with no wildcard, and a table-driven test drives them off every_inbound/every_outbound so the next omission is a compile error rather than a silent None. Filed #101 for four sibling sites with the same unvalidated-length shape. One of these fixes was itself wrong first: replacing expect_err with unwrap_or_else to make a panic message interpolate inverted the test, since unwrap_or_else unwraps Ok and runs the closure on Err. The compiler caught it. Claude-Session: https://claude.ai/code/session_014tq59dRUCNsHR1GuUkguHw
Problem
Delegates have no way to schedule future execution. The
OutboundDelegateMsgenum has no
ScheduleWakeup-equivalent variant andInboundDelegateMsghas noWakeupFired, so delegate execution is purely message-driven. dApps that needperiodic background work (key rotation, TTL pruning, scheduled publication) are
forced to push it into a UI/client sync loop, which stops working when the UI is
closed. Driving use case: River private-rooms weekly secret rotation
(freenet/river#228).
Approach
Add the two wire variants the host↔delegate protocol needs:
OutboundDelegateMsg::ScheduleWakeup { at: SystemTime, tag: Vec<u8> }InboundDelegateMsg::WakeupFired { tag: Vec<u8> }Both are appended at the end of their enums (bincode variant tag 8), so the
change is wire-compatible for every existing variant — delegate WASM compiled
against an older stdlib keeps deserializing everything it already understood.
OutboundDelegateMsgis deliberately left not#[non_exhaustive]: thehost must consciously handle every outbound variant, so the compiler should
force an arm for each new one. The inaccurate doc comment on
InboundDelegateMsgthat claimedOutboundDelegateMsgwas already#[non_exhaustive]is corrected.InboundDelegateMsgstays#[non_exhaustive]; unknown inbound variants areforwarded to the delegate WASM unchanged, so
WakeupFiredflows through.FlatBuffers WS path, so no
.fbsschema / generated-code / TypeScript changesare needed.
ScheduleWakeupgets a "reached client serialization - this is abug" arm in the WS encoder mirroring
SendDelegateMessage.Bumps
0.8.2→0.8.3.Testing
New wire-format pin tests freeze the variant tags so a future reorder is caught
loudly:
inbound_wakeup_fired_wire_format_is_stable— full byte layout at tag 8.outbound_schedule_wakeup_wire_format_is_stable— tag 8 + round-trip.Downstream
Host-side implementation is in freenet-core#3972 (opens after this publishes,
per the stdlib-first release policy).
Refs: freenet/freenet-core#3972
[AI-assisted - Claude]