Skip to content

[ECO-5728] Fix presence auto reenter causes NACKs with "not currently attached" after reconnect from transient disconnect#2241

Merged
VeskeR merged 2 commits into
mainfrom
ECO-5728/fix-presence-auto-reenter
Jun 19, 2026
Merged

[ECO-5728] Fix presence auto reenter causes NACKs with "not currently attached" after reconnect from transient disconnect#2241
VeskeR merged 2 commits into
mainfrom
ECO-5728/fix-presence-auto-reenter

Conversation

@VeskeR

@VeskeR VeskeR commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

A presence operation invoked while the connection was transiently DISCONNECTED
(with the channel still ATTACHED) was placed on the connection-wide queue, which
RTL3d flushes on reconnect before the channel re-attaches. The server then
rejected it with "not currently attached" (code 90001), because presence
requires a real, non-transient channel attachment, unlike regular messages.

Now, when the channel is ATTACHED but the connection is not CONNECTED, presence
is queued at the channel level (RTP16d2) and sent once the channel next reaches
ATTACHED (RTP5b), honoring queueMessages.

Two commits:

  1. Honor queueMessages for channel-level presence queueing (RTP16b), a
    pre-existing gap fixed first.
  2. Queue presence at the channel level when the connection is not CONNECTED.

Companion spec PR: ably/specification#490.

Resolves ECO-5728

Summary by CodeRabbit

  • Bug Fixes
    • Presence enter/update/leave now fully respect per-channel queueMessages: when disabled, presence actions made before attachment/re-attachment are rejected instead of queued.
    • Pending presence operations are re-queued and delivered after a successful re-attach, improving ordering and membership consistency during reconnects.
  • Documentation
    • Updated presenceEnterWithoutAttach docs to clarify queueing behavior when queueMessages is enabled by default.
  • Tests
    • Added coverage for queueMessages: false rejection and for ensuring queued presence transmits only after post-reconnect attachment, with membership verification.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Presence ENTER/UPDATE and LEAVE now queue only when the channel is attaching and channel.client.options.queueMessages is enabled; incompatible states reject with a PartialErrorInfo. Connection-level queued presence can be moved into channel presence queues before queued messages are flushed. Tests and a specpartial were added. The minimal useful Realtime bundle size threshold increased by 1 KiB.

Changes

Presence Message Queuing Behavior

Layer / File(s) Summary
Enter/update/leave conditional queuing & requeue API
src/common/lib/client/realtimepresence.ts
ENTER/UPDATE and LEAVE now queue in the attaching case only when queueMessages is enabled; incompatible states reject with a unified PartialErrorInfo. Added requeuePresenceMessages() to re-add presence messages into pendingPresence.
Channel: requeue presence from connection queue
src/common/lib/client/realtimechannel.ts
Adds requeuePresenceFromConnectionQueue(pendingMessage) to forward connection-queued presence into the channel presence queue when a presence plugin exists and the channel is in attached/attaching/suspended.
ConnectionManager: move queued presence to channels
src/common/lib/transport/connectionmanager.ts
Before flushing queued messages when enabling sends, queuedPresenceToChannelQueues() migrates eligible presence messages from the connection-wide queue into per-channel presence queues (removing handled messages from the connection queue).
Presence queuing behavior test coverage
test/realtime/presence.test.js
Adds a @specpartial note and two tests: presence_enter_no_queueing (rejection when queuing disabled) and presence_enter_while_disconnected_sent_after_reattach (queued presence sent after channel re-attach and verified via presence.get).

Bundle Size Threshold Update

Layer / File(s) Summary
Minimal useful Realtime bundle size threshold
scripts/moduleReport.ts
Raw size threshold for the minimal useful Realtime bundle increased from 107 to 108 KiB to accommodate presence queuing implementation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Soft paws on message queues I tread,
If channels cling, the messages are led,
When queueing's off, a careful stop—no send,
Then requeue hops back when channels mend.
I nibble bugs and see the tests extend.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly identifies the main bug being fixed: presence operations causing NACKs after reconnect from transient disconnect, which aligns with the changeset's core purpose of improving presence message queueing logic.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ECO-5728/fix-presence-auto-reenter

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 and usage tips.

@VeskeR VeskeR changed the title Eco 5728/fix presence auto reenter [ECO-5728] Fix presence auto reenter causes NACKs with "not currently attached" after reconnect from transient disconnect Jun 9, 2026
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/bundle-report June 9, 2026 12:12 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/typedoc June 9, 2026 12:13 Inactive
@VeskeR VeskeR force-pushed the ECO-5728/fix-presence-auto-reenter branch from 87c4827 to 6675f4f Compare June 9, 2026 12:14
@VeskeR VeskeR requested a review from ttypic June 9, 2026 12:14
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/bundle-report June 9, 2026 12:15 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/typedoc June 9, 2026 12:15 Inactive

@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: 1

🧹 Nitpick comments (1)
test/realtime/presence.test.js (1)

1829-1834: ⚡ Quick win

Assert the channel is still attached before queuing the enter.

Right now this only waits for the connection-level disconnected event. If the channel has already left attached by then, the enter will take the existing ATTACHING path and the test still passes, so it doesn't prove the new RTP16d2 branch. A channel.state === 'attached' assertion in this callback would pin the regression to the intended case.

🤖 Prompt for 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.

In `@test/realtime/presence.test.js` around lines 1829 - 1834, Add an assertion
that the channel is still attached before queuing the presence enter: inside the
realtime.connection.once('disconnected', ...) callback (where
channel.presence.enterClient(testClientId, 'data') is called), check
channel.state === 'attached' and throw or fail the test if not; this ensures the
test exercises the path where the channel remains ATTACHED while the connection
becomes DISCONNECTED and prevents the enter from taking the ATTACHING path.
🤖 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/common/lib/client/realtimepresence.ts`:
- Around line 135-140: The queued presence branch pushes into pendingPresence
but leaves _myMembers unchanged, so onAttached()’s _ensureMyMembersPresent() can
auto-reenter with stale state before the queued update/leave is flushed; update
the logic so either (A) when queueing a self update/leave in the branches that
push to pendingPresence (the code around pendingPresence, including the similar
branch at the 199-204 area), also mutate _myMembers to reflect the queued op
(apply the intended enter/update/leave locally) or (B) change
_ensureMyMembersPresent()/onAttached() to first inspect pendingPresence for any
queued self ops for the client and suppress or supersede the auto-enter when a
later queued leave/update would contradict it; pick one approach and make sure
both the queueing sites (where pendingPresence is appended) and
_ensureMyMembersPresent()/onAttached() are updated to remain consistent.

---

Nitpick comments:
In `@test/realtime/presence.test.js`:
- Around line 1829-1834: Add an assertion that the channel is still attached
before queuing the presence enter: inside the
realtime.connection.once('disconnected', ...) callback (where
channel.presence.enterClient(testClientId, 'data') is called), check
channel.state === 'attached' and throw or fail the test if not; this ensures the
test exercises the path where the channel remains ATTACHED while the connection
becomes DISCONNECTED and prevents the enter from taking the ATTACHING path.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81420361-6caa-4b74-9805-0ac0e237494b

📥 Commits

Reviewing files that changed from the base of the PR and between 2a0eae5 and 6675f4f.

📒 Files selected for processing (2)
  • src/common/lib/client/realtimepresence.ts
  • test/realtime/presence.test.js

Comment on lines +135 to +140
if (channel.client.options.queueMessages) {
return new Promise((resolve, reject) => {
this.pendingPresence.push({
presence: wirePresMsg,
callback: (err) => (err ? reject(err) : resolve()),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Queued own update/leave can be undone by auto-reenter.

These branches only append to pendingPresence. _myMembers still reflects the pre-disconnect state, and onAttached() auto-reenters _myMembers before flushing pendingPresence. If an already-present client calls leave() or update() while DISCONNECTED, reconnect will first send a stale enter, then the queued op. That gives other subscribers an incorrect enter→leave sequence, or a stale enter→update. Please reconcile _myMembers with queued own presence ops, or make _ensureMyMembersPresent() consult/supersede pending presence first.

Also applies to: 199-204

🤖 Prompt for 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.

In `@src/common/lib/client/realtimepresence.ts` around lines 135 - 140, The queued
presence branch pushes into pendingPresence but leaves _myMembers unchanged, so
onAttached()’s _ensureMyMembersPresent() can auto-reenter with stale state
before the queued update/leave is flushed; update the logic so either (A) when
queueing a self update/leave in the branches that push to pendingPresence (the
code around pendingPresence, including the similar branch at the 199-204 area),
also mutate _myMembers to reflect the queued op (apply the intended
enter/update/leave locally) or (B) change _ensureMyMembersPresent()/onAttached()
to first inspect pendingPresence for any queued self ops for the client and
suppress or supersede the auto-enter when a later queued leave/update would
contradict it; pick one approach and make sure both the queueing sites (where
pendingPresence is appended) and _ensureMyMembersPresent()/onAttached() are
updated to remain consistent.

@ttypic ttypic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good, but I think we should add some ConnectionManager handling as well. Right now, if the connection drops during a sendPresence call and we don’t receive an ack, those messages are queued in the ConnectionManager-level queue. I believe we should filter them out and move them to the channel-level queue on reconnect. Otherwise, we may still send those nacks.

This is a much less likely scenario, so we could potentially skip it, but I think it’s worth addressing.

Comment thread src/common/lib/client/realtimepresence.ts
VeskeR added a commit that referenced this pull request Jun 10, 2026
Addresses review feedback on PR #2241. The previous approach queued
presence at the channel level whenever the channel was ATTACHED but the
connection was not CONNECTED. That left a rare corner case: if the
connection dropped while a presence message was in flight, it was
unconditionally resent on reconnect via the connection-wide queue,
bypassing the re-attach gate.

Instead, when the connection becomes CONNECTED, transfer any queued
presence off the connection-wide queue onto the relevant channel's
pending-presence queue (RTL3d/RTL3d2), so it is only sent once the
channel has (re-)attached (RTP5b). This mirrors ably-java's
transferQueuedPresenceMessages and also covers the in-flight resend case.

Also narrows the queueMessages change: the implicit attach of an
initialized or detached channel (RTP8d) happens regardless of
queueMessages; only the channel-level queuing of the presence message
itself (RTP16b) is gated on it.
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/bundle-report June 10, 2026 13:13 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/typedoc June 10, 2026 13:14 Inactive

@ttypic ttypic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@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: 1

♻️ Duplicate comments (2)
src/common/lib/client/realtimepresence.ts (2)

127-132: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Queued own update/leave can be undone by auto-reenter.

This issue was previously flagged and remains unresolved. When queueing presence operations here, _myMembers is not updated to reflect the queued operation. On reconnect, onAttached() (line 321) calls _ensureMyMembersPresent() (line 337) before flushing pendingPresence (lines 340-362). If a client calls update() or leave() while DISCONNECTED, the stale _myMembers state will cause an incorrect auto-reenter, followed by the queued operation, giving subscribers an incorrect sequence.

Please reconcile _myMembers with queued own-presence operations, or make _ensureMyMembersPresent() consult/supersede pending presence first.

🤖 Prompt for 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.

In `@src/common/lib/client/realtimepresence.ts` around lines 127 - 132, When
queueing an own presence operation in pendingPresence (in the branch where
channel.client.options.queueMessages is true) the code does not update
_myMembers, so on reconnect onAttached() -> _ensureMyMembersPresent() runs
against stale state and may auto-reenter; fix by reconciling _myMembers when
pushing a pending presence (i.e., when update() or leave() queues wirePresMsg)
so _myMembers reflects the intended queued state, or alternatively change
_ensureMyMembersPresent() to inspect pendingPresence first and treat queued
own-presence messages as authoritative before deciding to auto-reenter; update
the logic around pendingPresence, _myMembers, and the pendingPresence flush in
onAttached()/flush path to ensure the queued op and _myMembers are consistent
and that subscribers see the correct sequence.

184-190: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Queued leave can be undone by auto-reenter.

This issue was previously flagged and remains unresolved. When queueing leave operations here, _myMembers is not updated. On reconnect, onAttached() will auto-reenter the stale _myMembers state before flushing the queued leave, causing subscribers to see an incorrect enter→leave sequence.

Please reconcile _myMembers with queued own-presence operations, or make _ensureMyMembersPresent() consult/supersede pending presence first.

🤖 Prompt for 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.

In `@src/common/lib/client/realtimepresence.ts` around lines 184 - 190, The queued
leave path pushes a pending presence (wirePresMsg) but doesn't update the local
_myMembers, so on reconnect onAttached() can auto-reenter stale members before
the queued leave is flushed; fix by reconciling pending own-presence when
queueing: when channel.client.options.queueMessages is true and you push to
pendingPresence in the block that handles wirePresMsg, detect own-client
leave/enter (compare wirePresMsg.clientId to this.clientId and
wirePresMsg.action === 'leave' or 'enter') and immediately update _myMembers
accordingly (remove member on leave, add/update on enter) OR update
_ensureMyMembersPresent() to consult pendingPresence for any queued own-presence
ops and compute the effective _myMembers before auto-reentering; reference
pendingPresence, wirePresMsg, _myMembers, onAttached(), and
_ensureMyMembersPresent() when making the change.
🧹 Nitpick comments (2)
src/common/lib/client/realtimepresence.ts (2)

196-203: ⚡ Quick win

Remove redundant code assignment.

Line 201 sets err.code = 90001, but the PartialErrorInfo constructor (lines 197-200) already set the code to 90001 as the second parameter. The assignment is redundant.

♻️ Proposed fix
       const err = new PartialErrorInfo(
         'Unable to leave presence channel while in ' + channel.state + ' state',
         90001,
       );
-      err.code = 90001;
       throw err;
🤖 Prompt for 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.

In `@src/common/lib/client/realtimepresence.ts` around lines 196 - 203, In the
default branch where a PartialErrorInfo is constructed, remove the redundant
manual assignment of err.code (the line "err.code = 90001"); the
PartialErrorInfo constructor call in that block already sets the code to 90001,
so keep the construction and throw err without reassigning err.code (locate this
in the default case that creates PartialErrorInfo in realtimepresence.ts).

137-144: ⚡ Quick win

Remove redundant code assignment.

Line 142 sets err.code = 90001, but the PartialErrorInfo constructor (line 138-141) already set the code to 90001 as the second parameter. The assignment is redundant.

♻️ Proposed fix
       const err = new PartialErrorInfo(
         'Unable to ' + action + ' presence channel while in ' + channel.state + ' state',
         90001,
       );
-      err.code = 90001;
       throw err;
🤖 Prompt for 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.

In `@src/common/lib/client/realtimepresence.ts` around lines 137 - 144, The code
creates a PartialErrorInfo with code 90001 and then redundantly reassigns
err.code = 90001; remove the redundant assignment so the error code comes solely
from the PartialErrorInfo constructor. Edit the default case in the presence
handling (where PartialErrorInfo is constructed using 'Unable to ' + action + '
presence channel while in ' + channel.state) and delete the line that sets
err.code = 90001.
🤖 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/common/lib/client/realtimepresence.ts`:
- Around line 403-406: requeuePresenceMessages currently pairs the same callback
with each presence and can invoke it multiple times; wrap the incoming callback
in a multicaster before pushing so the batch triggers a single invocation (see
requeuePresenceMessages and this.pendingPresence), mirroring the multicaster
pattern used in onAttached (lines ~346-361), or alternatively change the method
signature to accept per-message callbacks (Array<{ presence:
WirePresenceMessage; callback: ErrCallback }>) and update
RealtimeChannel.requeuePresenceFromConnectionQueue accordingly; implement the
multicaster wrapper at the point where you push into this.pendingPresence so
only one callback invocation occurs for the entire presenceMessages array.

---

Duplicate comments:
In `@src/common/lib/client/realtimepresence.ts`:
- Around line 127-132: When queueing an own presence operation in
pendingPresence (in the branch where channel.client.options.queueMessages is
true) the code does not update _myMembers, so on reconnect onAttached() ->
_ensureMyMembersPresent() runs against stale state and may auto-reenter; fix by
reconciling _myMembers when pushing a pending presence (i.e., when update() or
leave() queues wirePresMsg) so _myMembers reflects the intended queued state, or
alternatively change _ensureMyMembersPresent() to inspect pendingPresence first
and treat queued own-presence messages as authoritative before deciding to
auto-reenter; update the logic around pendingPresence, _myMembers, and the
pendingPresence flush in onAttached()/flush path to ensure the queued op and
_myMembers are consistent and that subscribers see the correct sequence.
- Around line 184-190: The queued leave path pushes a pending presence
(wirePresMsg) but doesn't update the local _myMembers, so on reconnect
onAttached() can auto-reenter stale members before the queued leave is flushed;
fix by reconciling pending own-presence when queueing: when
channel.client.options.queueMessages is true and you push to pendingPresence in
the block that handles wirePresMsg, detect own-client leave/enter (compare
wirePresMsg.clientId to this.clientId and wirePresMsg.action === 'leave' or
'enter') and immediately update _myMembers accordingly (remove member on leave,
add/update on enter) OR update _ensureMyMembersPresent() to consult
pendingPresence for any queued own-presence ops and compute the effective
_myMembers before auto-reentering; reference pendingPresence, wirePresMsg,
_myMembers, onAttached(), and _ensureMyMembersPresent() when making the change.

---

Nitpick comments:
In `@src/common/lib/client/realtimepresence.ts`:
- Around line 196-203: In the default branch where a PartialErrorInfo is
constructed, remove the redundant manual assignment of err.code (the line
"err.code = 90001"); the PartialErrorInfo constructor call in that block already
sets the code to 90001, so keep the construction and throw err without
reassigning err.code (locate this in the default case that creates
PartialErrorInfo in realtimepresence.ts).
- Around line 137-144: The code creates a PartialErrorInfo with code 90001 and
then redundantly reassigns err.code = 90001; remove the redundant assignment so
the error code comes solely from the PartialErrorInfo constructor. Edit the
default case in the presence handling (where PartialErrorInfo is constructed
using 'Unable to ' + action + ' presence channel while in ' + channel.state) and
delete the line that sets err.code = 90001.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8813e8a6-d4d4-47f5-9a97-664700433976

📥 Commits

Reviewing files that changed from the base of the PR and between 6675f4f and 877a1e7.

📒 Files selected for processing (4)
  • src/common/lib/client/realtimechannel.ts
  • src/common/lib/client/realtimepresence.ts
  • src/common/lib/transport/connectionmanager.ts
  • test/realtime/presence.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/realtime/presence.test.js

Comment on lines +403 to +406
requeuePresenceMessages(presenceMessages: WirePresenceMessage[], callback: ErrCallback): void {
for (const presence of presenceMessages) {
this.pendingPresence.push({ presence, callback });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Callback invoked multiple times for batched messages.

The method signature accepts a single callback for an array of presenceMessages, implying one callback invocation for the entire batch. However, the implementation pairs the same callback with each individual message (lines 404-406). If the batch contains multiple messages and individual messages fail during send, the callback will be invoked once per failing message instead of once total.

The caller (RealtimeChannel.requeuePresenceFromConnectionQueue, line 560 in realtimechannel.ts) wraps pendingMessage.callback and expects a single invocation. Multicall will break this contract.

🔧 Suggested fix using Multicaster

Wrap the callback in a multicaster (similar to onAttached, lines 346-361) to ensure one invocation:

 requeuePresenceMessages(presenceMessages: WirePresenceMessage[], callback: ErrCallback): void {
+  const multicaster = Multicaster.create(this.logger);
+  multicaster.push(callback);
   for (const presence of presenceMessages) {
-    this.pendingPresence.push({ presence, callback });
+    this.pendingPresence.push({ presence, callback: (err) => multicaster(err) });
   }
 }

Alternatively, if each message should have independent error handling, change the signature to accept Array<{ presence: WirePresenceMessage; callback: ErrCallback }>.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
requeuePresenceMessages(presenceMessages: WirePresenceMessage[], callback: ErrCallback): void {
for (const presence of presenceMessages) {
this.pendingPresence.push({ presence, callback });
}
requeuePresenceMessages(presenceMessages: WirePresenceMessage[], callback: ErrCallback): void {
const multicaster = Multicaster.create(this.logger);
multicaster.push(callback);
for (const presence of presenceMessages) {
this.pendingPresence.push({ presence, callback: (err) => multicaster(err) });
}
}
🤖 Prompt for 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.

In `@src/common/lib/client/realtimepresence.ts` around lines 403 - 406,
requeuePresenceMessages currently pairs the same callback with each presence and
can invoke it multiple times; wrap the incoming callback in a multicaster before
pushing so the batch triggers a single invocation (see requeuePresenceMessages
and this.pendingPresence), mirroring the multicaster pattern used in onAttached
(lines ~346-361), or alternatively change the method signature to accept
per-message callbacks (Array<{ presence: WirePresenceMessage; callback:
ErrCallback }>) and update RealtimeChannel.requeuePresenceFromConnectionQueue
accordingly; implement the multicaster wrapper at the point where you push into
this.pendingPresence so only one callback invocation occurs for the entire
presenceMessages array.

VeskeR added a commit that referenced this pull request Jun 19, 2026
Addresses review feedback on PR #2241. The previous approach queued
presence at the channel level whenever the channel was ATTACHED but the
connection was not CONNECTED. That left a rare corner case: if the
connection dropped while a presence message was in flight, it was
unconditionally resent on reconnect via the connection-wide queue,
bypassing the re-attach gate.

Instead, when the connection becomes CONNECTED, transfer any queued
presence off the connection-wide queue onto the relevant channel's
pending-presence queue (RTL3d/RTL3d2), so it is only sent once the
channel has (re-)attached (RTP5b). This mirrors ably-java's
transferQueuedPresenceMessages and also covers the in-flight resend case.

Also narrows the queueMessages change: the implicit attach of an
initialized or detached channel (RTP8d) happens regardless of
queueMessages; only the channel-level queuing of the presence message
itself (RTP16b) is gated on it.
@VeskeR VeskeR force-pushed the ECO-5728/fix-presence-auto-reenter branch from 877a1e7 to e4820f8 Compare June 19, 2026 10:16
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/bundle-report June 19, 2026 10:17 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/typedoc June 19, 2026 10:17 Inactive
@VeskeR VeskeR force-pushed the ECO-5728/fix-presence-auto-reenter branch from e4820f8 to d794d79 Compare June 19, 2026 11:04
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/bundle-report June 19, 2026 11:05 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/typedoc June 19, 2026 11:05 Inactive
@VeskeR VeskeR force-pushed the ECO-5728/fix-presence-auto-reenter branch from d794d79 to 310393b Compare June 19, 2026 11:23
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/bundle-report June 19, 2026 11:25 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2241/typedoc June 19, 2026 11:25 Inactive
VeskeR added 2 commits June 19, 2026 12:58
A presence operation on a channel that has not yet reached the ATTACHED
state is queued at the channel level only when the queueMessages client
option is enabled (RTP16b); when it is disabled the operation is rejected
instead of queued. The implicit attach of an initialized or detached
channel (RTP8d) is independent of queueMessages and always happens.
When the connection becomes CONNECTED the connection-wide message queue is
flushed (RTL3d). A presence message must not be flushed while its channel
is still re-attaching, or the server rejects it with "not currently
attached". Transfer any such queued presence off the connection-wide queue
onto the channel's pending-presence queue, so it is sent only once the
channel has re-attached (RTL3d2, RTP5b). Presence for channels that are not
being re-attached is left on the connection-wide queue.

Resolves ECO-5728
@VeskeR VeskeR force-pushed the ECO-5728/fix-presence-auto-reenter branch from 310393b to 2931422 Compare June 19, 2026 11:58
@VeskeR VeskeR merged commit fa63502 into main Jun 19, 2026
14 of 16 checks passed
@VeskeR VeskeR deleted the ECO-5728/fix-presence-auto-reenter branch June 19, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Presence messages NACK'd with "not currently attached" after reconnect from transient disconnect

2 participants