Skip to content

refactor(events): complete the event-payload API freeze - #1004

Merged
jlucaso1 merged 3 commits into
mainfrom
claude/whatsapp-rust-pr-review-wjppqi
Jul 8, 2026
Merged

refactor(events): complete the event-payload API freeze#1004
jlucaso1 merged 3 commits into
mainfrom
claude/whatsapp-rust-pr-review-wjppqi

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

What

Final tranche of the pre-1.0 event-payload API freeze, after ServerAck (#1002) and the notification/sync payloads (#1003). It seals every remaining event payload so fields can be added later without breaking consumers, and clears the related tech debt in the same pass. After this, the whole Event surface is frozen.

Sealing (#[non_exhaustive] + #[derive(bon::Builder)])

  • 25 field-bearing payloads: Receipt, InboundMessage, MessageBatch, UndecryptableMessage, LoggedOut, Disconnected, PairSuccess, PairError, PairPasskeyRequest/Confirmation/Error, DeviceListUpdate + nested DeviceNotificationInfo, IdentityChange, BusinessStatusUpdate, DisappearingModeChanged, MexNotification, NewsletterLiveUpdate (+ Message, + Reaction), TemporaryBan, ConnectFailure, StreamError, OfflineSyncPreview, OfflineSyncCompleted.
  • 3 inline Event variants converted to sealed newtypes: PairingQrCode, PairingCode, PairingCodeRefresh. Event::kind() and every construct/match site updated.
  • 4 unit-marker events converted to empty sealed structs: Connected, ClientOutdated, QrScannedWithoutMultidevice, StreamReplaced — built via X::builder().build().

Tech debt cleared along the way

  • EventInterest bitmask migrated u64 to u128, doubling the EventKind ceiling from 64 to 128 (it was at 58/64, so this was the next real wall). CAPACITY and the build-time tripwire updated.

Migration

All ~62 cross-crate construction/match sites in whatsapp-rust, plus the in-crate wacore test sites and one e2e-tests reader, moved to the builder / newtype form. Behavior-preserving: same values, same control flow; Option fields routed through maybe_* setters. Compiler-enforced end to end — E0639 on any missed struct literal, and bon's typestate build() rejects any missed required field at compile time.

Deliberately not added: a trybuild guard

The repo builds on nightly, where trybuild .stderr snapshots drift on every toolchain bump. The seal is already enforced structurally by E0639 on every literal, so a snapshot test would be net-negative maintenance debt rather than a real safety gain.

Verification

  • cargo build -p whatsapp-rust / -p wacore clean.
  • cargo clippy -p whatsapp-rust -p wacore --tests clean.
  • cargo check -p e2e-tests -p bench-integration --all-targets clean.
  • cargo fmt --all --check clean.
  • cargo test -p wacore --lib → 1074 passed; cargo test -p whatsapp-rust --lib → 954 passed, 0 failed.
  • voip-cli was not checked here — it fails to build in this environment on the alsa-sys system dependency (missing ALSA headers), unrelated to this change.

Follow-ups

None for the freeze itself — the Event surface is fully sealed. AGENTS.md and the Event doc now state the policy as complete rather than rolling out.

Final tranche of the pre-1.0 freeze (after ServerAck in #1002 and the
notification/sync payloads in #1003). Seals every remaining event payload
so fields can be added later without breaking consumers, and clears the
related tech debt in the same pass.

Sealing (#[non_exhaustive] + #[derive(bon::Builder)]):
- 25 field-bearing payloads: Receipt, InboundMessage, MessageBatch,
  UndecryptableMessage, LoggedOut, Disconnected, PairSuccess, PairError,
  PairPasskey{Request,Confirmation,Error}, DeviceListUpdate +
  DeviceNotificationInfo, IdentityChange, BusinessStatusUpdate,
  DisappearingModeChanged, MexNotification, NewsletterLiveUpdate(+Message,
  +Reaction), TemporaryBan, ConnectFailure, StreamError,
  OfflineSyncPreview, OfflineSyncCompleted.
- 3 inline Event variants converted to sealed newtypes: PairingQrCode,
  PairingCode, PairingCodeRefresh. Event::kind() and every construct/match
  site updated accordingly.
- 4 unit-marker events converted to empty sealed structs: Connected,
  ClientOutdated, QrScannedWithoutMultidevice, StreamReplaced — built via
  X::builder().build().

Tech debt cleared along the way:
- EventInterest bitmask migrated u64 -> u128, doubling the EventKind
  ceiling from 64 to 128 (was at 58/64); CAPACITY and the build-time
  tripwire updated.

All ~62 cross-crate construction/match sites in whatsapp-rust plus the
in-crate wacore test sites and one e2e-tests reader migrated to the
builder / newtype form. Behavior-preserving: same values, same control
flow; Option fields routed through maybe_* setters. Compiler-enforced end
to end (E0639 on any missed literal, typestate build() on any missed
required field).

Deliberately not added: a trybuild compile-fail guard. The repo builds on
nightly, where trybuild .stderr snapshots drift on every toolchain bump;
the seal is already enforced structurally by E0639, so a snapshot test
would be net-negative maintenance debt.

AGENTS.md and the Event doc updated: the freeze is complete, not rolling.
@coderabbitai

coderabbitai Bot commented Jul 7, 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: b1e9b080-24e9-486c-9f2e-81d5a4778d9e

📥 Commits

Reviewing files that changed from the base of the PR and between d88863e and 635f030.

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

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Expanded event tracking capacity to support a larger set of app events.
    • Updated pairing and passkey event payload construction for more robust QR/code and onboarding flows.
  • Bug Fixes

    • Improved consistency of connection lifecycle event dispatch (connected/disconnected/logged out).
    • Made message, receipt, offline-sync, and notification event delivery behavior more uniform across scenarios.
  • Documentation

    • Refreshed critical event-payload guidance to clarify stable, non-exhaustive payload handling.

Walkthrough

Event payloads are sealed and built through bon::Builder, several pairing events now use tuple variants, EventInterest widens to u128, and the affected client, notification, message, pairing, and test call sites now construct and match those payloads with the updated shapes.

Changes

Event payload sealing and dispatch migration

Layer / File(s) Summary
Event contracts, bitset widening, and docs
wacore/src/types/events.rs, AGENTS.md
EventKind::CAPACITY moves 64→128, EventInterest wraps u128, three Event variants become tuple variants, many payload structs gain #[non_exhaustive]/bon::Builder, Event::kind() updates, and the payload convention doc is revised.
Client lifecycle and connection-event dispatch
src/client/lifecycle.rs, src/client/node_io.rs, src/client/sessions.rs
Connected, Disconnected, LoggedOut, StreamReplaced, StreamError, TemporaryBan, ClientOutdated, ConnectFailure, and OfflineSyncCompleted now dispatch via builders instead of struct literals.
Pairing, pair-code, and passkey event dispatch
src/pair.rs, src/pair_code.rs, src/passkey/flow.rs, src/bot.rs, tests/e2e/src/lib.rs, examples/benchmark.rs
Pairing/passkey event payload construction and Event::PairingCode/PairingQrCode/PairingCodeRefresh matching switch to builders/tuple-pattern accessors, with matching test and example updates.
Notification handlers
src/handlers/notification/device.rs, src/handlers/notification/groups.rs, src/handlers/notification/privacy_business.rs, src/handlers/ib.rs
IdentityChange, DeviceListUpdate, NewsletterLiveUpdate*, MexNotification, DisappearingModeChanged, BusinessStatusUpdate, and OfflineSyncPreview construction migrate to builders.
Message batching and receipts
src/message/commit_batch.rs, src/message/dispatch.rs, src/message/durability.rs, src/message/retry.rs, src/message/tests.rs, src/pdo.rs, src/receipt.rs, src/retry.rs
MessageBatch, InboundMessage, UndecryptableMessage, and Receipt construction migrate to builders across production and test code.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: api-design, breaking-change

Suggested reviewers: greptile-apps, cubic-dev-ai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: completing the event-payload API freeze.
Description check ✅ Passed The description matches the PR and explains the freeze, builders, newtype variants, and EventInterest migration.
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
  • Commit unit tests in branch claude/whatsapp-rust-pr-review-wjppqi

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 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR completes the pre-1.0 API freeze for all event payload structs in wacore/src/types/events.rs, sealing the entire Event surface with #[non_exhaustive] + #[derive(bon::Builder)]. It also migrates EventInterest from u64 to u128 to double the EventKind ceiling, and resolves the prior sentinel-string issue on ConnectFailure::message by switching to Option<String>.

  • 25 field-bearing payload structs sealed; 3 inline Event variants (PairingQrCode, PairingCode, PairingCodeRefresh) extracted to newtype structs; 4 unit-marker events (Connected, ClientOutdated, QrScannedWithoutMultidevice, StreamReplaced) converted to empty sealed structs built via X::builder().build().
  • All ~62 cross-crate construction and match sites migrated to builder form, with Option<T> fields correctly routed through maybe_* setters; ConnectFailure::message changed from String (empty-string sentinel) to Option<String> per AGENTS.md convention.

Confidence Score: 5/5

Safe to merge — all ~62 construction and match sites are updated consistently, behavior is preserved across the board, and the compiler enforces completeness via E0639 and bon's typestate builder.

The changes are entirely mechanical: struct literal → builder, inline variant fields → newtype wrapper, unit struct → empty struct. The only semantic delta is ConnectFailure::message changing from String (empty-string sentinel) to Option, which is the correct fix per AGENTS.md convention and was explicitly flagged in a prior review. The EventInterest u64→u128 migration is self-contained to in-memory filtering with no serialization surface. Tests pass (1074 + 954) and the seal is compiler-enforced at every call site.

No files require special attention. wacore/src/types/events.rs is the largest change but is consistent throughout.

Important Files Changed

Filename Overview
wacore/src/types/events.rs Core change: seals all remaining payload structs with #[non_exhaustive] + bon::Builder, extracts three inline Event variants to newtype structs, converts four unit-marker events to empty sealed structs, migrates EventInterest from u64 to u128, and corrects ConnectFailure::message from String to Option. All consistent and correct.
src/client/node_io.rs Updates all ConnectFailure, LoggedOut, StreamReplaced, StreamError, TemporaryBan, and ClientOutdated construction sites to use builders; correctly routes the optional message attribute through maybe_message(Option) instead of unwrap_or(""). No behavior changes other than the sentinel→Option fix.
src/pair.rs Migrates PairSuccess, PairError, and PairingQrCode construction to builder form; match sites updated to newtype pattern. Mechanical and correct.
src/pair_code.rs Migrates PairingCode and PairingCodeRefresh dispatch and match sites to builder/newtype form, including two test assertions. Clean.
src/receipt.rs Both Receipt construction sites migrated to builder, using r#type(…) raw-identifier setter correctly. Behavior preserved.
src/handlers/notification/device.rs DeviceListUpdate and DeviceNotificationInfo construction migrated to builders; Option fields (lid_user, key_index, contact_hash) correctly routed through maybe_* setters.
src/bot.rs All PairingCode and PairingCodeRefresh match and construction sites updated to newtype pattern; test helper pairing_code_event migrated to builder. Clean.
AGENTS.md Event payload policy updated from 'rolling out per struct' to 'every payload is sealed', accurately reflecting the completed freeze.
tests/e2e/src/lib.rs Single PairingQrCode match site updated to newtype pattern. Correct.
src/passkey/flow.rs PairPasskeyRequest, PairPasskeyConfirmation, and PairPasskeyError construction migrated to builder at all five dispatch/return sites. Clean.

Class Diagram

%%{init: {'theme': 'neutral'}}%%
classDiagram
    class Event {
        <<non_exhaustive enum>>
        +PairingQrCode(PairingQrCode)
        +PairingCode(PairingCode)
        +PairingCodeRefresh(PairingCodeRefresh)
        +Connected(Connected)
        +ClientOutdated(ClientOutdated)
        +QrScannedWithoutMultidevice(QrScannedWithoutMultidevice)
        +StreamReplaced(StreamReplaced)
        +LoggedOut(LoggedOut)
        +Disconnected(Disconnected)
        +ConnectFailure(ConnectFailure)
        +StreamError(StreamError)
        +Messages(MessageBatch)
        +Receipt(Receipt)
        +PairSuccess(PairSuccess)
        +PairError(PairError)
        +kind() EventKind
    }
    class EventInterest {
        <<u128 bitmask>>
        +ALL: EventInterest
        +CAPACITY: u8 = 128
        +of(kinds) EventInterest
        +with(kind) EventInterest
        +wants(kind) bool
    }
    class ConnectFailure {
        <<non_exhaustive>>
        +reason: ConnectFailureReason
        +message: Option~String~
        +raw: Option~Node~
    }
    class MessageBatch {
        <<non_exhaustive>>
        +messages: Arc~[InboundMessage]~
        +origin: BatchOrigin
    }
    class InboundMessage {
        <<non_exhaustive>>
        +message: Arc~wa::Message~
        +info: Arc~MessageInfo~
    }
    Event --> EventInterest : filtered by
    Event --> ConnectFailure
    Event --> MessageBatch
    MessageBatch --> InboundMessage
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"}}}%%
classDiagram
    class Event {
        <<non_exhaustive enum>>
        +PairingQrCode(PairingQrCode)
        +PairingCode(PairingCode)
        +PairingCodeRefresh(PairingCodeRefresh)
        +Connected(Connected)
        +ClientOutdated(ClientOutdated)
        +QrScannedWithoutMultidevice(QrScannedWithoutMultidevice)
        +StreamReplaced(StreamReplaced)
        +LoggedOut(LoggedOut)
        +Disconnected(Disconnected)
        +ConnectFailure(ConnectFailure)
        +StreamError(StreamError)
        +Messages(MessageBatch)
        +Receipt(Receipt)
        +PairSuccess(PairSuccess)
        +PairError(PairError)
        +kind() EventKind
    }
    class EventInterest {
        <<u128 bitmask>>
        +ALL: EventInterest
        +CAPACITY: u8 = 128
        +of(kinds) EventInterest
        +with(kind) EventInterest
        +wants(kind) bool
    }
    class ConnectFailure {
        <<non_exhaustive>>
        +reason: ConnectFailureReason
        +message: Option~String~
        +raw: Option~Node~
    }
    class MessageBatch {
        <<non_exhaustive>>
        +messages: Arc~[InboundMessage]~
        +origin: BatchOrigin
    }
    class InboundMessage {
        <<non_exhaustive>>
        +message: Arc~wa::Message~
        +info: Arc~MessageInfo~
    }
    Event --> EventInterest : filtered by
    Event --> ConnectFailure
    Event --> MessageBatch
    MessageBatch --> InboundMessage
Loading

Reviews (3): Last reviewed commit: "Merge branch 'main' into claude/whatsapp..." | Re-trigger Greptile

Comment thread wacore/src/types/events.rs
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.81 MiB 10.82 MiB +6.78 KiB (+0.06%) 🔺
bin .text 8.81 MiB 8.82 MiB +7.19 KiB (+0.08%) 🔺
bin allocated (text+data+bss) 10.81 MiB 10.82 MiB +3.83 KiB (+0.03%) 🔺
llvm-lines wacore 504,323 504,433 +110 (+0.02%) 🔺
llvm-lines wacore copies 17,278 17,332 +54 (+0.31%) 🔺
llvm-lines whatsapp-rust lib 761,162 764,868 +3,706 (+0.49%) 🔺
llvm-lines whatsapp-rust lib copies 24,680 24,862 +182 (+0.74%) 🔺
deps crates (Cargo.lock) 468 468 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.63 MiB 1.64 MiB +11.53 KiB (+0.69%) 🔺
.text wacore 531.47 KiB 522.99 KiB -8.48 KiB (-1.60%) 🎉
.text wacore_binary 157.70 KiB 157.70 KiB 0
.text wacore_libsignal 178.73 KiB 179.42 KiB +701 B (+0.38%) 🔺
.text wacore_appstate 156.45 KiB 158.46 KiB +2.01 KiB (+1.29%) ⚠️
.text wacore_noise 26.05 KiB 26.05 KiB 0
.text waproto 1.60 MiB 1.60 MiB +1.01 KiB (+0.06%) 🔺
.text whatsapp_rust_sqlite_storage 512.98 KiB 512.98 KiB 0
.text whatsapp_rust_tokio_transport 43.61 KiB 43.61 KiB 0
.text whatsapp_rust_ureq_http_client 10.47 KiB 10.47 KiB 0
.text std 1.00 MiB 1.00 MiB +548 B (+0.05%) 🔺
.text other deps 2.95 MiB 2.95 MiB -98 B (-0.00%) 🔽
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.63 MiB 1.64 MiB +11.53 KiB (+0.69%)
wacore 531.47 KiB 522.99 KiB -8.48 KiB (-1.60%)
wacore_appstate 156.45 KiB 158.46 KiB +2.01 KiB (+1.29%)
rustix 1.88 KiB 191 B -1.69 KiB (-90.08%)
buffa_descriptor 2.98 KiB 4.67 KiB +1.69 KiB (+56.87%)
waproto 1.60 MiB 1.60 MiB +1.01 KiB (+0.06%)

Baseline: cd317bc27 (latest main run) · Head: 030967d21 · Graphs

@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.

2 issues found across 22 files

Confidence score: 3/5

  • In wacore/src/types/events.rs, unit-marker events now serialize as {} instead of null, which can break observers that rely on the previous JSON shape and cause downstream parsing/regression issues in public event consumers — restore the prior wire format (or add explicit compatibility handling/versioning) before merging.
  • In src/client/node_io.rs, mapping a missing <failure message> to "" collapses two different states (“absent” vs “empty”), so clients may lose error semantics and make incorrect retry/reporting decisions — preserve presence information in the model (e.g., optional message) or add a compatibility-safe encoding before merging.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread wacore/src/types/events.rs
Comment thread src/client/node_io.rs Outdated
Two follow-ups on the freeze PR surfaced by CI and review:

- examples/benchmark.rs matched the old inline `Event::PairingQrCode
  { code, .. }`; under `--all-targets` (both the default Build & Test and
  the all-features job) this failed to compile. Updated to the newtype
  pattern. The earlier local check missed it because a plain
  `cargo build -p whatsapp-rust` does not compile examples and the
  workspace `--all-targets` check aborted on the voip-cli `alsa-sys`
  system dep first.
- ConnectFailure.message changed String -> Option<String>. It was
  populated with `unwrap_or("")`, an empty-string sentinel for an absent
  server `message` attribute — exactly the sentinel the freeze convention
  forbids, and this is the point it would have been locked in. Now built
  via maybe_message, so absent stays None. No consumer read the field, so
  the change is contained to the one construction site.

Verified: cargo build -p whatsapp-rust --all-targets (default) and
clippy -p whatsapp-rust --all-features --all-targets -- -D warnings both
clean; wacore 1074 + whatsapp-rust 954 tests pass; fmt clean.

@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 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@codspeed-hq

codspeed-hq Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 182 untouched benchmarks


Comparing claude/whatsapp-rust-pr-review-wjppqi (c374cbb) with main (ba1ac3b)

Open in CodSpeed

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