Skip to content

feat(events): observe-only ServerAck event for server <ack> stanzas - #989

Merged
jlucaso1 merged 2 commits into
oxidezap:mainfrom
JeanCapixaba:feat/server-ack-event
Jul 7, 2026
Merged

feat(events): observe-only ServerAck event for server <ack> stanzas#989
jlucaso1 merged 2 commits into
oxidezap:mainfrom
JeanCapixaba:feat/server-ack-event

Conversation

@JeanCapixaba

@JeanCapixaba JeanCapixaba commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Motivation

The ack path resolves the internal response_waiters map (kept allocation-free by #827, reworked by #978) with no public hook, so consumers have no way to observe server acks. Two things are invisible from the outside:

  • send → server-accept latency — the timing that tells server-side acceptance apart from fan-out/delivery. SendResult resolves at socket write, and receipts only cover delivery, so there is currently no way to measure when the server accepted a sent message.
  • nack codes — 463/479/400/… are parsed in handle_ack_response but only surface as warn! logs; a consumer cannot react to them programmatically.

What this does

Adds an observe-only Event::ServerAck(ServerAck), dispatched in handle_ack_response for every server <ack> that carries an id — before and independently of the internal waiter resolution, so it never interacts with the send/phash flow.

The payload is a named struct (same pattern as Receipt(Receipt), so it can grow without breaking consumer matches):

pub struct ServerAck {
    pub id: String,
    /// "message", "receipt", "notification", "call", … — server acks cover
    /// every outgoing stanza class, so consumers filter here instead of
    /// correlating ids blind.
    pub class: String,
    pub from: Option<Jid>,
    /// The ack's `t` attribute — whatsmeow reads the same attribute into
    /// `SendResponse.Timestamp`, so for a message ack this is the
    /// authoritative send timestamp.
    pub timestamp: Option<DateTime<Utc>>,
    /// Nack code (e.g. "479"); None for a plain ack.
    pub error: Option<String>,
}

Design notes

  • Allocation-free when unused: the dispatch is gated on EventBus::has_handler_for(EventKind::ServerAck), so the hot ack path allocates nothing when no handler subscribes — ack_miss_path_does_not_heap_allocate stays green.
  • EventInterest stability: EventKind::ServerAck is appended at the end so existing bit indexes don't shift; the build-time ceiling tripwire now points at it.
  • id/error are read once at the top of handle_ack_response and reused by the nack diagnostics, the dispatch and the waiter branch.
  • Waiter behavior, phash validation and nack diagnostics are untouched.

Tests

  • test_ack_dispatches_server_ack_event: message ack carries class/from/t, nack carries the code with absent attrs staying empty/None, and an id-less <ack> dispatches nothing.
  • Existing ack tests (test_ack_waiter_resolves, test_ack_without_matching_waiter, ack_miss_path_does_not_heap_allocate) unchanged and green.
  • cargo fmt / cargo clippy -p whatsapp-rust -p wacore --tests clean; cargo test -p whatsapp-rust --lib 944 passed.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 00db2a45-d94e-4915-9e3c-6080b0b0515a

📥 Commits

Reviewing files that changed from the base of the PR and between 2b57a09 and 585613c.

📒 Files selected for processing (1)
  • src/client/tests.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a new server acknowledgment event that can be observed whenever an incoming <ack/> includes an ID.
    • The event now carries useful acknowledgement details such as class, sender, timestamp, and any error information.
  • Bug Fixes

    • Server ack/nack handling now emits the acknowledgement event before any matching response is consumed, so listeners can always observe it.
    • Improved handling for acknowledgements without an ID to avoid unnecessary event dispatches.

Walkthrough

A new observe-only ServerAck event kind and payload were added. handle_ack_response now dispatches that event for server <ack> stanzas with an id, before waiter resolution, and a test covers plain, errored, and id-less acks.

Changes

ServerAck event dispatch

Layer / File(s) Summary
ServerAck event kind and variant definitions
wacore/src/types/events.rs
Adds ServerAck to EventKind, updates the capacity tripwire, adds Event::ServerAck with payload fields, and maps it in Event::kind().
Dispatch ServerAck from ack handling and test coverage
src/client/node_io.rs, src/client/tests.rs
handle_ack_response now dispatches Event::ServerAck for id-bearing acks before waiter removal; a new async test validates plain ack, error ack, and id-less ack cases.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: api-design

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the new observe-only ServerAck event for server stanzas.
Description check ✅ Passed The description directly explains the ServerAck event, payload, gating, and tests for the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

Adds an observe-only Event::ServerAck(ServerAck) dispatched in handle_ack_response for every server <ack> stanza that carries an id, giving consumers a way to measure send→server-accept latency and react to nack codes programmatically instead of scraping logs.

  • EventKind::ServerAck is appended at the end of the enum to preserve existing bit-index assignments; the build-time overflow tripwire is updated to point at the new last variant.
  • Dispatch is gated on has_handler_for(EventKind::ServerAck) so the hot ack path performs no heap allocation when no handler has subscribed, and it fires before internal waiter resolution so it is fully independent of the send/phash flow.
  • New test test_ack_dispatches_server_ack_event covers plain ack, nack-with-error-code, id-less ack (must not fire), and the case where both a waiter and a handler coexist.

Confidence Score: 5/5

Safe to merge — the change is additive, the waiter path is untouched, and the allocation guard keeps the hot path overhead-free when no handler subscribes.

The dispatch block is inserted before — and completely independent of — the waiter resolution branch, so existing send/phash behavior is unchanged. EventKind::ServerAck is correctly appended at the end, leaving all prior bit-index assignments in place, and the build-time overflow assert is updated to track the new last variant. The tests exercise all four dispatch scenarios and are consistent with the project conventions.

No files require special attention.

Important Files Changed

Filename Overview
wacore/src/types/events.rs Adds EventKind::ServerAck at the end of the enum (preserving existing bit indexes), updates the build-time bitmask overflow assertion to point at the new last variant, and defines the ServerAck payload struct deriving Debug + Clone + Serialize — consistent with all other event payload types.
src/client/node_io.rs Hoists ack_id/ack_error reads to the top of handle_ack_response, adds an interest-gated dispatch block before the waiter branch, and reuses the cached attrs in both the nack-logging block and the new dispatch — clean refactor with no behavioral change to the waiter path.
src/client/tests.rs New test covers all four dispatch scenarios (plain ack, nack, id-less ack, and simultaneous waiter+handler); uses fictitious JIDs per the coding convention, let-chain assertions, and tokio timeout for the waiter leg.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Server
    participant handle_ack_response
    participant EventBus
    participant ResponseWaiters

    Server->>handle_ack_response: server ack stanza
    handle_ack_response->>handle_ack_response: read ack_id and ack_error
    alt error attribute present
        handle_ack_response->>handle_ack_response: warn nack code
    end
    handle_ack_response->>EventBus: has_handler_for(ServerAck)?
    alt handler subscribed AND id present
        handle_ack_response->>EventBus: dispatch Event::ServerAck
        EventBus->>EventBus: deliver to subscribed handlers
    end
    handle_ack_response->>ResponseWaiters: remove(id)?
    alt waiter found
        handle_ack_response->>ResponseWaiters: send OwnedNodeRef
        handle_ack_response-->>Server: returns true
    else no waiter
        handle_ack_response-->>Server: returns false
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Server
    participant handle_ack_response
    participant EventBus
    participant ResponseWaiters

    Server->>handle_ack_response: server ack stanza
    handle_ack_response->>handle_ack_response: read ack_id and ack_error
    alt error attribute present
        handle_ack_response->>handle_ack_response: warn nack code
    end
    handle_ack_response->>EventBus: has_handler_for(ServerAck)?
    alt handler subscribed AND id present
        handle_ack_response->>EventBus: dispatch Event::ServerAck
        EventBus->>EventBus: deliver to subscribed handlers
    end
    handle_ack_response->>ResponseWaiters: remove(id)?
    alt waiter found
        handle_ack_response->>ResponseWaiters: send OwnedNodeRef
        handle_ack_response-->>Server: returns true
    else no waiter
        handle_ack_response-->>Server: returns false
    end
Loading

Reviews (4): Last reviewed commit: "test(client): cover ServerAck dispatch a..." | Re-trigger Greptile

Comment thread src/client/node_io.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: Adds an observe-only ServerAck event for server acks; no impact on existing logic, gated on handler interest.

Re-trigger cubic

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 6, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Auto-approved: Adds an observe-only ServerAck event for server stanzas. Low-impact, allocation-free when unused, with tests.

Re-trigger cubic

@jlucaso1

jlucaso1 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Hey @JeanCapixaba, thanks a lot for this one and welcome. Really solid first PR. It is well scoped, the description actually explains the why, and the tests cover the cases that matter. Exactly the kind of contribution we like to get.

The approach is right, so nothing here is about the idea itself. A couple of things I would like to sort before it lands.

First, credit where it is due. Making this observe-only instead of blocking the send on the ack is the correct call, it keeps us in line with how WA Web behaves and avoids a latency regression. Gating the dispatch on has_handler_for so the hot ack path stays allocation free is also spot on, it keeps the ack_miss_path_does_not_heap_allocate guarantee from #827 intact. And appending the EventKind at the end to keep the bit indexes stable is the right instinct.

The main thing I want to nail down before this becomes public API is the payload. Right now it is { id, error }, but the server ack covers every outgoing stanza class, not just messages (message, receipt, notification, call). Without class on the event, a consumer cannot tell a message ack apart from an ack for a receipt we sent, so the send to server-accept latency story only really works if they already recorded the id on their side. Can you carry class, and probably from, on the event too? They are already on the node so it is basically free, and it turns this from a firehose the consumer has to correlate blind into something actually usable for the case you are describing. If the server ack also has a t timestamp, worth checking a capture, that would make the latency measurement cleaner too.

Tied to that: since consumers will match on Event::ServerAck { id, error }, adding a field later is a breaking change even though the enum is non_exhaustive (that only guards the variant list, not the fields). So I would either mark the variant itself non_exhaustive, or wrap it in a small named struct like we already do for Receipt(Receipt), so it can grow without breaking anyone. Getting the shape right now saves a churn PR later.

Smaller stuff, not blocking:

The description and commit message say #604 moved ack waiting behind response_waiters, but #604 is the SKDM flow PR. I think you meant #827 (the allocation free ack path this builds on) or #978 (the waiter refactor). Worth fixing so the history stays honest.
id and error get read a couple of extra times in handle_ack_response now. You could hoist the get_attr once. Purely cosmetic, not a perf thing.
Again, great first contribution and thanks for actually digging into the ack path properly. Ping me once the payload is sorted and I will take another pass.

The ack path resolves the internal response_waiters map (kept
allocation-free by oxidezap#827, reworked by oxidezap#978) with no public hook, so
consumers have no way to observe server acks. This adds an observe-only
Event::ServerAck dispatched in handle_ack_response for every <ack> that
carries an id, before and independently of the waiter resolution, so it
never interacts with the send/phash flow.

Use cases: measuring send -> server-accept latency (which tells
server-side acceptance apart from fan-out), and surfacing nack codes
(463/479/...) programmatically instead of scraping warn! logs.

The payload is a named ServerAck struct (same pattern as
Receipt(Receipt), so it can grow without breaking matches) carrying id,
class, from, the server t timestamp when present (whatsmeow reads the
same attribute into SendResponse.Timestamp), and the nack error code.
Server acks cover every outgoing stanza class, so class lets consumers
filter message acks without correlating ids blind.

The dispatch is gated on EventBus::has_handler_for(EventKind::ServerAck)
so the hot ack path stays allocation-free when nobody subscribes
(ack_miss_path_does_not_heap_allocate stays green). EventKind::ServerAck
is appended at the end to keep existing EventInterest bit indexes
stable; the build-time ceiling tripwire now points at it.
@JeanCapixaba
JeanCapixaba force-pushed the feat/server-ack-event branch from 64fa711 to 2b57a09 Compare July 7, 2026 12:32
@greptile-apps
greptile-apps Bot dismissed their stale review July 7, 2026 12:32

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@JeanCapixaba

JeanCapixaba commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Payload sorted in 2b57a09 (squashed to one commit with a corrected message):

Test updated to cover class/from/t on a message ack and empty/None on a nack that omits them. ack_miss_path_does_not_heap_allocate still green.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 7, 2026
@JeanCapixaba

JeanCapixaba commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Also, thank you for the warm welcome and for taking the time to write such a thorough review, especially on a first PR from an outsider. The class/from/t suggestion genuinely made the API better: what I had was enough for my own use case, but as you said, without class it's a firehose for anyone else. Happy to iterate again if anything else comes up on the next pass. Great project, by the way, the ack path was a pleasure to dig into.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/tests.rs`:
- Around line 211-277: The new ack coverage in
test_ack_dispatches_server_ack_event still misses the core guarantee that
ServerAck dispatch is independent of waiter resolution. Extend this test (or add
a sibling one) to register an ack waiter for a real id, call handle_ack_response
on the same ack node, and assert both the Event::ServerAck is emitted via
TestEventCollector and the waiter resolves successfully. Use the existing
create_test_client, register_handler, and handle_ack_response flow so the
behavior is verified end-to-end.

In `@wacore/src/types/events.rs`:
- Around line 1189-1210: Mark the ServerAck payload struct as non-exhaustive to
prevent external exhaustive construction and destructuring from breaking when
new fields are added later. Update the ServerAck definition in the events types
module by adding the non_exhaustive attribute directly on the ServerAck struct
so callers using Event::ServerAck and ServerAck remain forward-compatible.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b56b374d-0dc4-4dee-a593-bf9aff948906

📥 Commits

Reviewing files that changed from the base of the PR and between 64fa711 and 2b57a09.

📒 Files selected for processing (3)
  • src/client/node_io.rs
  • src/client/tests.rs
  • wacore/src/types/events.rs

Comment thread src/client/tests.rs
Comment thread wacore/src/types/events.rs
@greptile-apps
greptile-apps Bot dismissed their stale review July 7, 2026 12:40

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Adds new Event::ServerAck to core ack path. Though additive and gated, it modifies critical handler logic and introduces a new public event variant, requiring human review for safety.

Re-trigger cubic

@jlucaso1
jlucaso1 merged commit 376ac1b into oxidezap:main Jul 7, 2026
22 of 23 checks passed
@jlucaso1

jlucaso1 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Thank you <3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants