Skip to content

Add notifly::exchange, fix post_and_wait's double-answer bug - #12

Merged
draugvar merged 2 commits into
mainfrom
feature/exchange
Aug 26, 2026
Merged

Add notifly::exchange, fix post_and_wait's double-answer bug#12
draugvar merged 2 commits into
mainfrom
feature/exchange

Conversation

@draugvar

Copy link
Copy Markdown
Owner

Summary

Adds notifly::exchange, the subscribe/wait primitive post_and_wait() is now built on top of,
and fixes a real bug in post_and_wait() along the way: a second answer from a sender that replies
twice used to call promise->set_value() twice, throwing std::future_error. exchange holds its
mutex across the "first done wins" decision, so that can't happen.

post_and_wait() covers one shape: post one notification, wait for one reply, take the first that
arrives. exchange covers what that shape can't express — several possible answers where which one
fired matters, ignoring a delivery that isn't the one being waited for, a reply streamed in pieces of
unstated length, treating silence as success, or waiting on something no command asked for. See the
new "Waiting For A Reply" section in the README for the full shape table and capture(), the
convenience on() wraps for a plain value or tuple readback.

Tests

56 tests (30 existing notifly + 8 NotiflyMemoryLeak, both untouched, + 16 new exchange tests +
2 for capture() added while reviewing this before tagging). example/exchange_example.cpp drives a
simulated device through each shape in the table.

Version

NOTIFLY_VERSION_{MAJOR,MINOR,PATCH} in include/notifly.h is already at 3.6.0. Tagging v3.6 on
this merge commit.

🤖 Generated with Claude Code

draugvar and others added 2 commits August 25, 2026 13:44
post_and_wait() covers one request/reply shape: post one notification, wait
for one reply, take the first that arrives. Real protocols keep producing
shapes it cannot express, so callers hand-roll subscribe/post/wait/unsubscribe
around add_observer() and have to get the ordering right themselves.

notifly::exchange is that pattern as a scoped object. Handlers return a
notifly_verdict per delivery -- skip, keep or done -- which is what lets one
primitive cover the rest:

  - skip: ignore a delivery and stay subscribed, for a sender that reports the
    state it is leaving before the one it is entering. Taking the first reply
    there reports the wrong state.
  - several on() calls: wait on alternatives at once; wait() returns which
    notification answered.
  - keep + drain(quiet, deadline): a reply streamed in pieces whose length the
    protocol never states.
  - silent_for(window): protocols where the sender only speaks up to refuse, so
    silence is the successful outcome.
  - subscribe and wait() without posting: waiting on an external event rather
    than on a command.

post_and_wait() is now a wrapper over it, unchanged in signature and behaviour
except that it no longer throws when a sender answers twice: the response
observer called promise->set_value() unguarded, and the observer was only
removed after the wait returned, so a second delivery in that window raised
std::future_error out of the dispatch loop -- which runs with the centre
locked and no try/catch, so it surfaced on the poster's thread. An exchange
ignores deliveries after the first done, so the guard now applies everywhere.

The private is_tuple trait moves to notifly_detail so exchange::capture() can
use it; add_response_observer() goes away with its only caller.

Tests cover each shape plus lifetime, subscription errors and the
double-answer regression (16 new, 54 total green). example/exchange_example.cpp
walks a simulated device through all five shapes.
capture() is what post_and_wait() is built on, and it's public on its own --
useful shorthand for a branch of a multi-alternative exchange that just needs
the payload, no handler. It had no test that exercised it directly (only
indirectly, through post_and_wait()'s own tests) and no mention outside the
doxygen comment on the declaration. Add both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@draugvar
draugvar merged commit 566e99c into main Aug 26, 2026
3 of 4 checks passed
@draugvar
draugvar deleted the feature/exchange branch August 26, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant