Skip to content

Move resumability decisions from the client to the server (spec 6.1.0)#2273

Open
SimonWoolf wants to merge 6 commits into
mainfrom
spec-6.1.0-server-resumability
Open

Move resumability decisions from the client to the server (spec 6.1.0)#2273
SimonWoolf wants to merge 6 commits into
mainfrom
spec-6.1.0-server-resumability

Conversation

@SimonWoolf

@SimonWoolf SimonWoolf commented Jul 22, 2026

Copy link
Copy Markdown
Member

Implements specification version 6.1.0 (spec commit 2a0a0006, ably/specification#488).

The client no longer decides whether continuity can be preserved on reconnection. It always attempts to resume and lets the server make the authoritative decision (surfaced via the resumed flag on the subsequent attached/connection state change).

Behaviour changes

  • RTN8d/RTN9dConnection#id and Connection#key are now cleared only in the terminal states (CLOSED, CLOSING, FAILED); they are retained through SUSPENDED.
  • RTN15g removed / RTN14h — dropped checkConnectionStateFreshness(). The client no longer discards connection state based on how long it has been disconnected, so reconnection (including out of SUSPENDED) always attempts a resume.
  • RTL15b2 — a channel's channelSerial is retained through SUSPENDED (cleared only on DETACHED/FAILED), so the subsequent ATTACH carries it (RTL4c1) for the server's continuity decision.
  • RTL4j removed / TR3f — the library no longer sets the (now deprecated) ATTACH_RESUME flag; the _attachResume tracking is removed. The flag constant is retained in the protocol definitions.

Also updates the SUSPENDED/DISCONNECTED docstrings in ably.d.ts, and drops the now-unused ConnectionManager#lastActivity mirror (the transport keeps its own copy for the idle timer).

Tests

Reworked or removed the tests that asserted the pre-6.1.0 behaviour, and added positive coverage for the new behaviour (id/key and channelSerial retained through SUSPENDED, reconnects still carrying the resume param after the TTL, ATTACH_RESUME not set, createRecoveryKey returning a key in SUSPENDED).

Follow-up in the spec repo

A companion PR is needed in ably/specification to bring the shared UTS .md test-spec sources in line with 6.1.0 — spec PR #488 updated features.md/api-docstrings.md but not the UTS sources. That PR should also fix RTN16g2, which still says createRecoveryKey() returns Null in SUSPENDED; since connectionKey is now retained there, this implementation returns a valid recovery key in SUSPENDED instead.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Improved realtime reconnection after extended disconnections by preserving connection and channel continuity through SUSPENDED.
    • While suspended, reconnect attempts continue using resume mode (including after connectionStateTtl expiry).
    • Updated channel reattachment to retain continuity without setting the attach-resume flag.
    • Presence auto-reenter and member continuity were adjusted to match the updated suspended/resume semantics.
  • Documentation
    • Clarified suspension/reconnection and queue handling semantics, including behavior after longer-term disconnects.
  • Tests
    • Updated and expanded resume/suspension coverage to reflect the new behavior.

SimonWoolf and others added 3 commits July 22, 2026 18:57
Implements specification version 6.1.0 (spec commit 2a0a0006).

The client no longer decides whether continuity can be preserved on
reconnection. It always attempts to resume and lets the server make the
authoritative decision (surfaced via the `resumed` flag).

- RTN8d/RTN9d: retain Connection#id and Connection#key through the
  SUSPENDED state; only clear them in the terminal states (CLOSED,
  CLOSING, FAILED).
- RTN15g removed / RTN14h: drop checkConnectionStateFreshness(). The
  client no longer discards connection state based on how long it has
  been disconnected, so reconnection always attempts a resume.
- RTL15b2: retain a channel's channelSerial through SUSPENDED (clear it
  only on DETACHED/FAILED), so the subsequent ATTACH carries it for the
  server's continuity decision.
- RTL4j removed / TR3f: stop setting the (now deprecated) ATTACH_RESUME
  flag; remove the _attachResume tracking. The flag constant is retained
  in the protocol definitions.

Also drop the now-unused ConnectionManager#lastActivity mirror (the
transport keeps its own copy for the idle timer).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework or remove the tests that asserted the pre-6.1.0 client behaviour,
matching the source changes for spec commit 2a0a0006:

- resume.test.js: replace no_resume_once_suspended and no_resume_last_activity
  (RTN15g) with resume_after_suspended (RTN14h), which checks the client still
  attempts a resume after being suspended.
- connection.test.js: remove stale_connection_resets_msgSerial_and_sendAttempted,
  which exercised the now-deleted checkConnectionStateFreshness path.
- UTS connection_id_key: RTN8c/RTN9c "id/key null in SUSPENDED" becomes
  RTN8d/RTN9d "id/key retained in SUSPENDED" (now connects first, then suspends).
- UTS channel_properties: RTL15b1 "channelSerial cleared on suspended" becomes
  RTL15b2 "channelSerial retained in suspended"; the detach/failed cases are
  relabelled RTL15b2.
- UTS channel_attach: RTL4j "ATTACH_RESUME flag on reattach" becomes
  "ATTACH_RESUME flag not set on reattach".
- private_api_recorder: drop the now-unused connectionManager.lastActivity entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two more UTS unit tests asserted the pre-6.1.0 behaviour:

- connection_failures: RTN15g "no resume after connectionStateTtl expires"
  becomes RTN14h "reconnection still attempts resume after connectionStateTtl
  expires" — verifying every post-suspension reconnect carries the resume
  param with the original connectionKey.
- connection_recovery: RTN16g2 "createRecoveryKey returns null in SUSPENDED"
  becomes "returns a key in SUSPENDED" — since connectionKey is now retained
  through SUSPENDED (RTN8d/RTN9d) the connection remains recoverable there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 49c74435-32f6-4942-bec9-d14b22316529

📥 Commits

Reviewing files that changed from the base of the PR and between ed86873 and c098162.

📒 Files selected for processing (1)
  • test/uts/realtime/integration/proxy/connection_resume.test.ts

Walkthrough

Connection suspension now retains resume identifiers and retries with resume parameters. Channel suspension retains channelSerial without setting ATTACH_RESUME. Tests, helpers, and TypeScript JSDoc are updated to describe and verify these semantics.

Changes

Resume continuity behavior

Layer / File(s) Summary
Connection suspension and resume lifecycle
src/common/lib/transport/..., src/common/lib/transport/transport.ts, test/realtime/..., test/uts/realtime/unit/connection/..., test/common/modules/private_api_recorder.js
Connection freshness tracking and clearing are removed, connection identifiers remain in SUSPENDED, and reconnection tests verify continued resume attempts and recovery-key availability.
Channel serial and attach handling
src/common/lib/client/realtimechannel.ts, test/uts/realtime/unit/channels/...
channelSerial is retained through SUSPENDED, internal _attachResume handling is removed, and channel tests verify reattach and state-transition behavior.
Suspension test support and presence scenarios
test/common/modules/shared_helper.js, test/realtime/presence.test.js
Suspension helpers can clear connection state, and presence tests use this mode for fresh reconnection cases while updating continuity expectations.
State semantics documentation
ably.d.ts
Connection and channel state JSDoc describes suspended-state continuity, queued messages, and reconnection behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: paddybyers, ttypic

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ConnectionManager
  participant Transport
  Client->>ConnectionManager: transition to SUSPENDED
  ConnectionManager->>Transport: reconnect with resume key
  Transport-->>ConnectionManager: connection result
  ConnectionManager-->>Client: connection state update
Loading

Poem

A rabbit hops through states anew,
Keeps keys and serials safely too.
Resume flags fade from channel flight,
Reconnects knock through suspended night.
“Hop on,” says Bun, “the tests now bloom!”

🚥 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 title accurately summarizes the main change: resumability logic shifts from client-side checks to server-side decisions for spec 6.1.0.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch spec-6.1.0-server-resumability

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.

@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/features July 22, 2026 18:42 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/bundle-report July 22, 2026 18:42 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/typedoc July 22, 2026 18:43 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

🤖 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 `@test/uts/realtime/unit/connection/connection_id_key.test.ts`:
- Around line 329-335: Update the RTN8d/RTN9d lifecycle comment to remove
CLOSING from the terminal-state list, stating that connection identifiers are
cleared only in CLOSED and FAILED while retained through CLOSING and SUSPENDED.
🪄 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: 241d9ab9-7e80-42af-bd02-41766b8fe093

📥 Commits

Reviewing files that changed from the base of the PR and between 9619f3d and 28b0a58.

📒 Files selected for processing (12)
  • ably.d.ts
  • src/common/lib/client/realtimechannel.ts
  • src/common/lib/transport/connectionmanager.ts
  • src/common/lib/transport/transport.ts
  • test/common/modules/private_api_recorder.js
  • test/realtime/connection.test.js
  • test/realtime/resume.test.js
  • test/uts/realtime/unit/channels/channel_attach.test.ts
  • test/uts/realtime/unit/channels/channel_properties.test.ts
  • test/uts/realtime/unit/connection/connection_failures.test.ts
  • test/uts/realtime/unit/connection/connection_id_key.test.ts
  • test/uts/realtime/unit/connection/connection_recovery.test.ts
💤 Files with no reviewable changes (2)
  • test/common/modules/private_api_recorder.js
  • test/realtime/connection.test.js

Comment on lines +329 to +335
* RTN8d, RTN9d - ID and key are retained in the SUSPENDED state
*
* As of specification version 6.1.0 (RTN8d/RTN9d, replacing RTN8c/RTN9c) the
* connection id and key are cleared only in the terminal states (CLOSED,
* CLOSING, FAILED). They are retained through SUSPENDED, since the client
* always attempts to resume on reconnecting and lets the server decide
* whether continuity can be preserved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove CLOSING from the terminal-state list.

closing is non-terminal; identifiers are cleared only on closed and failed. The current comment misstates the lifecycle contract.

🤖 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/uts/realtime/unit/connection/connection_id_key.test.ts` around lines 329
- 335, Update the RTN8d/RTN9d lifecycle comment to remove CLOSING from the
terminal-state list, stating that connection identifiers are cleared only in
CLOSED and FAILED while retained through CLOSING and SUSPENDED.

The spec-side change introduces RTN16g3 (replacing RTN16g2) to say
createRecoveryKey() no longer returns null in SUSPENDED. Match the label
in the ably-js UTS test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/features July 22, 2026 18:51 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/bundle-report July 22, 2026 18:51 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/typedoc July 22, 2026 18:51 Inactive
Three presence tests encoded the pre-6.1.0 assumption that becoming
suspended discards the connection state, so that reconnecting makes a
fresh connection with a new connectionId and re-syncs presence from
scratch. Since 6.1.0 the client retains its connection state through
suspended and resumes on reconnect (RTN8d/RTN14h), so a local
becomeSuspended followed by connect now resumes with the same
connectionId and preserves presence continuity.

Add an optional clearConnectionState flag to the becomeSuspended test
helper. When set, it calls connectionManager.clearConnection() once
suspended, modelling the case where the connection genuinely can no
longer be resumed (e.g. the device slept past the server's retention),
so the reconnect makes a fresh connection with a new connectionId.

- presence_auto_reenter (RTP17i) and presence_auto_reenter_different_connid
  (RTP17g/g1): use the flag, since both rely on a fresh connection — the
  former to force own-member re-entry, the latter to get a changed
  connectionId. Without it the successful resume preserved presence and
  the tests' premises no longer held (timeout / connId unchanged).
- suspended_preserves_presence (RTP5f/RTP11d): left on the genuine resume
  path (this test is about the members map surviving suspension). The
  final member count is now 2 (main + continuous) rather than 3: the
  resume preserves both cleanly and the only change is the leave of
  'leaves', consistent with the single leave event the test already
  asserts. The old fresh-reconnect re-entered main, leaving a residual
  that showed 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/features July 23, 2026 18:18 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/bundle-report July 23, 2026 18:18 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2273/typedoc July 23, 2026 18:18 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

🤖 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 `@test/common/modules/shared_helper.js`:
- Around line 283-288: Ensure the clearConnectionState path clears the
connection before the suspended state notification is emitted, so reconnect
callbacks cannot use stale recovery credentials. Update the relevant
ConnectionManager state-transition logic, such as enactStateChange or
_becomeSuspended, and preserve the existing conditional behavior and private API
recording.
🪄 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 Plus

Run ID: 31325e26-c3e1-4243-95b5-7be26641e6f7

📥 Commits

Reviewing files that changed from the base of the PR and between 26ddf91 and ed86873.

📒 Files selected for processing (2)
  • test/common/modules/shared_helper.js
  • test/realtime/presence.test.js

Comment on lines 283 to +288
self.recordPrivateApi('call.connectionManager.notifyState');
realtime.connection.connectionManager.notifyState({ state: 'suspended' });
if (clearConnectionState) {
self.recordPrivateApi('call.connectionManager.clearConnection');
realtime.connection.connectionManager.clearConnection();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)test/common/modules/shared_helper\.js$|connectionManager|connect|clearConnection|notifyState'

echo "== file outline =="
ast-grep outline test/common/modules/shared_helper.js --view compact || true

echo "== relevant lines =="
nl -ba test/common/modules/shared_helper.js | sed -n '240,330p'

echo "== search connectionManager definitions/usages =="
rg -n "connectionManager|clearConnection|notifyState|connectionState|suspended|clearConnectionState" test/common/modules/shared_helper.js -C 3

Repository: ably/ably-js

Length of output: 1980


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== shared_helper relevant section =="
awk 'NR>=250 && NR<=315 { printf "%5d\t%s\n", NR, $0 }' test/common/modules/shared_helper.js

echo "== connectionmanager outline/sections =="
wc -l src/common/lib/transport/connectionmanager.ts
grep -n "class ConnectionManager\|clearConnection\|notifyState\|emit\|eventEmitter\|stateChange\|tryTransition" src/common/lib/transport/connectionmanager.ts | head -200

echo "== implementation sections =="
awk 'NR>=1 && NR<=220 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

Repository: ably/ably-js

Length of output: 15400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== clearConnection implementation =="
awk 'NR>=820 && NR<=905 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

echo "== enactStateChange implementation =="
awk 'NR>=920 && NR<=970 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

echo "== notifyState implementation =="
awk 'NR>=1136 && NR<=1260 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

echo "== relevant callback listeners =="
awk 'NR>=1280 && NR<=1365 { printf "%5d\t%s\n", NR, $0 }' src/common/lib/transport/connectionmanager.ts

echo "== call sites around becomeSuspended tests usage =="
rg -n "becomeSuspended|clearConnectionState|notifyState\\(\\{ state: 'suspended'|clearConnection\\(" test src/common -C 2

Repository: ably/ably-js

Length of output: 22170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== resume tests around becomeSuspended with clearConnectionState =="
awk 'NR>=470 && NR<=580 { printf "%5d\t%s\n", NR, $0 }' test/realtime/resume.test.js

echo "== static behavioral probe for _becomeSuspended ordering =="
node - <<'JS'
const events = [];
const realtime = {
  connection: {
    once(event, listener) {
      events.push({ event, type: 'listener registered', eventListeners: realtime.connection._listeners?.[event] || [] });
      realtime.connection._listeners = realtime.connection._listeners || {};
      (realtime.connection._listeners[event] = realtime.connection._listeners[event] || []).push(listener);
    },
    emit(event, ...args) {
      events.push({ event, type: 'emit', arg: args[0], listeners: realtime.connection._listeners?.[event]?.length || 0 });
      for (const listener of realtime.connection._listeners?.[event] || []) listener(...args);
    },
    _listeners: {},
  },
  connectionManager: {
    clearConnectionState: false,
    disconnectAllTransports() {},
    notifyState(ind) {
      if (ind.state === 'suspended') {
        this.emit('disconnected');
        this.emit='suspended;';
        this.inSuspendedCallback = true;
      }
    },
    emit(...args){}
  },
};
let cbCalledWithIdKey;
function clearConnectionStateWithSameValues() {
  realtime.becameSuspended = true;
}
try {
  const helperPrototype = { addingHelperFunction: () => ({ disconnectAllTransports() {}, _becomeSuspended(realtime, cb, clearConnectionState) {
    realtime.connection.connectionManager.disconnectAllTransports();
    const self = this;
    self.recordPrivateApi = () => {};
    realtime.connection.once('disconnected', function () {
      self.recordPrivateApi('call.connectionManager.notifyState');
      realtime.connection.connectionManager.notifyState({ state: 'suspended' });
      if (clearConnectionState) {
        self.recordPrivateApi('call.connectionManager.clearConnection');
        realtime.connection.connectionManager.clearConnection();
      }
    });
    if (cb) {
      realtime.connection.once('suspended', function () {
        cb();
      });
    }
  }}); }
}
JS

echo "== behavioral probe from source text =="
python3 - <<'PY'
from pathlib import Path
source = Path("src/common/lib/transport/connectionmanager.ts").read_text()
notify = source[source.index("  notifyState(indicated: ConnectionState): void {"):source.index("  requestState(request: any): void {")]
print("notifyState calls ConnectionManager.emitter emit:", "this.emit('connectionstate'" in notify)
print("clearConnection only invoked for terminal states in enactStateChange:", "this.clearConnection()" in source[source.index("  enactStateChange"):source.index("  startTransitionTimer")])
print("suspended state non-terminal:", "terminal: false,\n      state: 'suspended'," in source)
print("suspended failState is suspended:", source[source.index("suspended"):].split("\n")[9])
PY

Repository: ably/ably-js

Length of output: 5128


Clear state before notifying suspended.

For clearConnectionState: true, notifyState({ state: 'suspended' }) enacts suspended, emits connectionstate synchronously, then clearConnection() runs in _becomeSuspended, so pending reconnect callbacks can still use the old recovery credentials. Move the clearing into ConnectionManager.enactStateChange() with non-terminal handling before emitting connectionstate, or clear in becomeSuspended before notifying.

🤖 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/common/modules/shared_helper.js` around lines 283 - 288, Ensure the
clearConnectionState path clears the connection before the suspended state
notification is emitted, so reconnect callbacks cannot use stale recovery
credentials. Update the relevant ConnectionManager state-transition logic, such
as enactStateChange or _becomeSuspended, and preserve the existing conditional
behavior and private API recording.

The ably-js translation of the proxy connection_resume UTS still asserted
the pre-6.1.0 RTN15g behaviour: that the reconnection made after the
connectionStateTtl expired carried no resume param. Under RTN14h the
client always attempts to resume, so that reconnection now carries the
resume param (the server, having discarded the state, then responds with
a new connectionId).

Rework the test (RTN15g/g2 -> RTN14h) to assert the final ws_connect
carries resume=<connectionKey>. Verified against the real uts-proxy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant