Skip to content

fix: drive connection lifecycle from WhatsApp's Stream model#201853

Open
Adi1231234 wants to merge 1 commit into
wwebjs:mainfrom
Adi1231234:upstream-conn-fix
Open

fix: drive connection lifecycle from WhatsApp's Stream model#201853
Adi1231234 wants to merge 1 commit into
wwebjs:mainfrom
Adi1231234:upstream-conn-fix

Conversation

@Adi1231234

@Adi1231234 Adi1231234 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Description

After a client is ready, an account can get stuck on a permanent loading state even though WhatsApp is connected and messages keep arriving. It comes from how the connection state is derived: from the one-shot ready event plus the raw offline-delivery percentage (getOfflineDeliveryProgress()). A background (non-blocking) offline sync fires offline_progress_update events that are surfaced as loading_screen, which flips a connected client back to loading. That percentage caps at 99 for a non-blocking resume, and ready only fires once, so it never comes back.

WhatsApp Web itself doesn't track the connection like that. Its own loading screen renders from WhatsApp's Stream model, reading Stream.mode and Stream.displayInfo. This does the same thing: it subscribes to Stream's change:mode / change:displayInfo and maps that state onto the existing ready / loading_screen events. It emits ready when mode is MAIN and displayInfo === NORMAL, loading_screen while it's syncing or offline, and nothing for the QR and error screens (those keep their own paths). The model only fires a change event on an actual change, so there's nothing to de-duplicate by hand.

The listener is registered after ClientInfo is built and fires once for the current state. Because it registers after init and only reacts to real Stream changes, there's no first-connect race, no separate "first ready" path, and nothing to de-dup manually. As a side effect this also generalizes #201653: ready now comes only from real Stream transitions (deduped by last screen), so a re-injection can't double-emit it.

Changes:

  • Subscribe to Stream change:mode change:displayInfo and map it onto the existing events: emit ready when connected, loading_screen while syncing or offline, and nothing for QR or error screens (they keep their own paths)
  • Register it after ClientInfo exists and fire once, so a consumer gets the current state without waiting for the next change
  • Drop the one-shot ready emit from the app-state-synced handler (the init flow stays, it just no longer decides "connected")
  • Remove the old offline_progress_update to loading_screen path

No new events and no signature changes, so existing consumers keep working unchanged.

Related

This is a long-standing, frequently reported symptom: WhatsApp Web is connected and its UI is fully loaded, but the client never surfaces ready, or ready only arrives later when a message happens to come in, typically after a restart or with a restored session. Deriving the state from WhatsApp's own Stream model and firing once for the current state is what closes that gap: it no longer depends on catching a single one-time transition. Reports of this exact symptom:

Prior work this builds on:

For accuracy, these share the same "ready never fires" symptom but have a different, already-identified root cause and are not addressed here: #3971 / #3798 (an A/B test leaves WAWebSetPushnameConnAction unloaded, so the Store.js injection throws), #3958 (local cache corruption), #5717 (missing IndexedDB session blobs).

Testing Summary

Test Details

Ran it against a persisted session over several hours, watching the emitted events and the resulting connection state. Boot connects and emits a single ready; the background offline sync that used to pin it to loading no longer touches the state (zero loading_screen reverts after ready across the whole run), while incoming messages keep processing. Verified across repeated full re-launches that it reaches connected via exactly one ready, with no duplicate and no listeners left behind.

Environment

  • Machine OS: Windows 11
  • Phone OS: Android 14
  • Library Version: 1.34.6
  • WhatsApp Web Version: 2.3000.1043363706
  • Browser Type and Version: Chromium 138
  • Node Version: 22.20.0

Type of Change

  • Dependency change
  • Bug fix
  • New feature
  • Breaking change
  • Non-code change

Checklist

  • My code follows the style guidelines of this project.
  • All new and existing tests pass (npm test).
  • Typings (e.g. index.d.ts) have been updated if necessary.
  • Usage examples (e.g. example.js) / documentation have been updated if applicable.

Connection state was derived from a one-shot `ready` plus the raw
offline-delivery percentage. A background (non-blocking) offline sync
emits `offline_progress_update`, which was surfaced as `loading_screen`
and flipped an already-connected client back to loading. That percentage
caps at 99 for a non-blocking resume and `ready` only fires once, so the
client never recovered.

Derive the state from WhatsApp Web's own Stream model instead (the same
`Stream.mode` / `Stream.displayInfo` its loading screen renders from):
subscribe to `change:mode change:displayInfo` after ClientInfo exists,
map the resolved screen onto the existing `ready` / `loading_screen`
events, and fire once for the current state. Backbone only fires change
events on real changes, so there is no init race, no backstop and no
manual de-dup. QR and error screens keep their own paths.

Because `ready` now comes only from real Stream transitions (deduped by
last screen), a re-injection can no longer double-emit it. No new events
and no signature changes, so existing consumers keep working unchanged.
@github-actions github-actions Bot added the api changes API modifications label Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api changes API modifications

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant