Skip to content

fix: keep Push connections consistent across concurrent subscribes and credential changes - #1948

Merged
ArnabChatterjee20k merged 31 commits into
feat/rn-push-cookie-sessionfrom
fix/push-connection-state
Sep 29, 2026
Merged

ArnabChatterjee20k merged 31 commits into
feat/rn-push-cookie-sessionfrom
fix/push-connection-state

Conversation

@ArnabChatterjee20k

@ArnabChatterjee20k ArnabChatterjee20k commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1947. Fixes the MQTT Push issues reported on appwrite/sdk-for-console#114 in the Web SDK, and the same issues in the other SDKs that have them. React Native got these fixes in #1947.

What was wrong

Web Android Flutter (io / web) Unity Apple
A subscribe during a pending connect used an unaccepted client yes yes (and two threads could build two clients) no yes (a second client replaced the first) no
A failed connect cleared other callers' subscriptions yes yes (and stopped background delivery) yes yes no
A credential or project change kept the old connection yes yes (in-process) yes (in-process) yes (reconnects also reused the old options) yes
Two tabs of one user shared a client id yes no (per install) n/a n/a n/a

Fixes

Connects

  • Web waits on the pending open.
  • Android serializes connect() under a lock.
  • Unity shares one in-flight connect task.
  • Flutter and Apple already shared one connect.

Failed connects

  • A failed connect now only drops its own client. The failing subscribe rolls back only its own registrations.

Credential changes

  • Every SDK keys the connection on endpoint, project, client id, auth method and credential.
  • When a subscribe finds the key changed, it supersedes the open (or opening) connection, bumps a connection epoch and connects with the current credential. The subscriptions it had are re-subscribed on the new connection.
  • A connect superseded while in flight gives up, instead of tearing down its replacement or hanging.
  • A subscribe that was waiting on it, or on its SUBACK, retries on the new connection. A plain close() never retries.

Web client id

  • The default is now <userId>-<tabId>, with the tab id kept in sessionStorage. It stays stable across reloads, so replay resumes, and differs per tab. An explicit setPushClientId() still wins.

Tests

PUSH_CREDENTIAL_SWITCH_RESPONSES is added to Web, Android (16 and 5), Flutter (stable, beta, web), Unity and Apple:

  • Switch: the same Push subscribes, the client's JWT changes to one the mock refuses, and the next subscribe must fail with the broker's reason, which proves it reconnected.
  • Pending switch: the switch lands while the first subscribe is still connecting. The second subscribe must fail. The first must fail the same way, or succeed only if it had already connected; it must never hang.
  • Apple: checks only that an error arrives, since MQTTNIO does not expose the reason string.

Not changed

Session expiry interval. Appwrite's broker does not use MQTT sessions for offline replay. It keeps a ledger and a cursor per project, user and client id, replays from the cursor on SUBSCRIBE, and purges the cursor only on clean start. So clean: false with a stable client id keeps the replay window; no session expiry is needed.

Cleartext credentials over ws:// or mqtt://. The push endpoint follows the API endpoint's scheme, the same way Realtime sends the session over ws:// for an http:// endpoint. A plain-text endpoint is a local-development setup the app chooses explicitly.

Empty default client ids. Apple, Flutter iOS/desktop/web and Unity send an empty client id and let the broker derive one. Whether two devices of one user then collide depends on how the broker derives it.

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Push service connection handling for concurrent subscribes and credential changes.

The PR appears safe to merge based on the reviewed changes.

Summary

The PR coordinates Push connections across concurrent subscribes and credential changes in Web, Android, Flutter, Unity, and Apple SDKs. It also gives Web connections distinct default client IDs and adds broker-backed tests. Since the previous review, it removed a shared MQTT test diagnostic and added an Android test delay.

Reviews (12) · Last reviewed commit: "test(android): let the mock's delayed pu..."

Comment thread templates/web/src/services/push.ts.twig Outdated
Comment thread tests/e2e/languages/web/index.html Outdated
Comment thread templates/web/src/services/push.ts.twig Outdated
@greptile-apps

This comment has been minimized.

Comment thread templates/unity/Assets/Runtime/Push.cs.twig
Comment thread templates/unity/Assets/Runtime/Push.cs.twig
Comment thread tests/e2e/languages/apple/Tests.swift Outdated
Comment thread tests/e2e/languages/apple/Tests.swift Outdated
Comment thread tests/e2e/languages/android/Tests.kt Outdated
Comment thread templates/web/src/services/push.ts.twig Outdated
Comment thread tests/e2e/languages/web/index.html Outdated
Comment thread templates/web/src/services/push.ts.twig Outdated
The tab id was claimed once per page and shared, so two Push instances
alive at the same time connected with the same MQTT client id and took
over each other's broker session. Now the first live connection takes the
tab's stored id (stable across reloads, for replay) and any connection
alive at the same time takes a distinct locked id, released on close so a
later connection can reuse it.
Open two Push instances at the same time in one page and require the
broker to report different client ids for them, alongside the existing
reopen-resumes-same-id and popup-differs checks. The prior test closed one
instance before opening the next, so it never exercised two live at once.
Also close the disconnect-error Push before this block frees its id.
Comment thread templates/web/src/services/push.ts.twig Outdated
…ng it

A credential switch could start a second open() while the first was still
awaiting its client-id claim; both passed the empty-tabId check and claimed,
and the loser overwrote this.tabId with its fallback, so the stored id it
had claimed was never released and later connections could not resume its
replay cursor. Memoize the claim per instance so concurrent opens share one,
and release a claim that resolves after close().
Comment thread templates/web/src/services/push.ts.twig Outdated
Comment thread templates/web/src/services/push.ts.twig Outdated
Capture the generation before awaiting the claim and re-check it after, so
a close() during a pending claim stops the open before it connects. Key the
memoized claim by generation and chain a reopen's claim after the previous
one settles and releases its id, so a close+reopen while a claim is pending
takes the tab's stored id back instead of a fallback, and a claim resolving
after its generation ended releases its id rather than stranding it.
The generated src/services/push.ts failed the web format:check because the
memoized-claim guard exceeded prettier's print width. Wrap it as prettier
formats it so the Build SDK step passes.
Comment thread tests/e2e/languages/android/Tests.kt Outdated
Comment thread mock-server/app/mqtt.php Outdated
@ArnabChatterjee20k
ArnabChatterjee20k merged commit 31e0aa8 into feat/rn-push-cookie-session Sep 29, 2026
61 checks passed
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