Skip to content

Sync UTS specs with 6.1.0 server-side resumability - #511

Merged
SimonWoolf merged 1 commit into
mainfrom
uts-6.1.0-server-resumability
Sep 3, 2026
Merged

Sync UTS specs with 6.1.0 server-side resumability#511
SimonWoolf merged 1 commit into
mainfrom
uts-6.1.0-server-resumability

Conversation

@SimonWoolf

Copy link
Copy Markdown
Member

Spec PR #488 ("Move resumability decisions from the client to the server") updated features.md and api-docstrings.md but did not update the UTS test-spec sources or address createRecoveryKey's behaviour in SUSPENDED. This brings both in line.

features.md:

  • RTN16g2 is replaced by RTN16g3: createRecoveryKey() no longer returns Null in SUSPENDED. Since the client now retains its connectionKey and always attempts to resume (RTN14h), the connection remains recoverable while suspended. (RTN16g2 as written still listed SUSPENDED, which contradicts RTN8d/RTN9d retaining the key.)

UTS sources:

  • connection_id_key: RTN8c/RTN9c "id/key null in SUSPENDED" -> RTN8d/RTN9d "id/key retained in SUSPENDED" (connect first, then suspend).
  • connection_failures: RTN15g "no resume after connectionStateTtl" -> RTN14h "still attempts resume after the TTL", asserting every post-suspension reconnect carries resume=.
  • connection_recovery: RTN16g2 -> RTN16g3; the SUSPENDED case now expects a non-null recovery key.
  • channel_properties: RTL15b1 "channelSerial cleared on suspended" -> RTL15b2 "retained in suspended"; detach/failed cases relabelled.
  • channel_attach: RTL4j "ATTACH_RESUME set on reattach" -> "not set".
  • proxy/connection_resume: Test 22 RTN15g/g2 -> RTN14h; the post-TTL reconnect now asserts the resume param is present.
  • completion-status: matrix updated for RTN8d/RTN9d, RTN14h, RTN16g3, RTL15b2.

@SimonWoolf
SimonWoolf requested a review from ttypic July 23, 2026 18:07

@ttypic ttypic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@paddybyers

Copy link
Copy Markdown
Member

@ttypic how are we tracking now the spec version that each library implements (especially those that have UTS-based test suites)?

AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 3, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16g3 is not yet in the published specification - ably/specification#511 adds it and
tombstones RTN16g2. Implemented ahead of that merging because it is the direct consequence
of RTN14h in the previous commit, and shipping the two apart would mean two behaviour
changes for callers instead of one. ably-js already behaves this way, from 2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndyTWF

AndyTWF commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

(RTN27d) SUSPENDED - a state where the library is not connected to the server and is not actively attempting to become connected, but where a reconnect attempt is scheduled in the future after the TO3l2 suspendedRetryTimeout elapses, and where next connect attempt is a clean connection (not a resume attempt).

Feels like RTN27d could be in the scope of this PR as I believe it's a relic of the pre-RTN14h days, as theoretically an SDK could become "suspended" but the server may well still have the information it needs to resume when it comes back? That is also the basis on which ably-js implemented it.

AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 3, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 is not yet in the published specification - ably/specification#511 adds it and
tombstones RTN16g2. Implemented ahead of that merging because it is the direct consequence
of RTN14h in the previous commit, and shipping the two apart would mean two behaviour
changes for callers instead of one. ably-js already behaves this way, from 2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 3, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 is not yet in the published specification - ably/specification#511 adds it and
tombstones RTN16g2. Implemented ahead of that merging because it is the direct consequence
of RTN14h in the previous commit, and shipping the two apart would mean two behaviour
changes for callers instead of one. ably-js already behaves this way, from 2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec PR #488 ("Move resumability decisions from the client to the
server", specification version 6.1.0) updated features.md and
api-docstrings.md but did not update the UTS test-spec sources or address
createRecoveryKey's behaviour in SUSPENDED. This brings both in line.

features.md:
- RTN16g2 is replaced by RTN16g3: createRecoveryKey() no longer returns
  Null in SUSPENDED. Since the client now retains its connectionKey and
  always attempts to resume (RTN14h), the connection remains recoverable
  while suspended. (RTN16g2 as written still listed SUSPENDED, which
  contradicts RTN8d/RTN9d retaining the key.)
- tweak wording of RTN27d (which is one of those "spec points" that shouldn't
  really be a spec point because it's kindof flavour-text for an enum member
  rather than specifying behaviour, so I don't think it needs a break)

UTS sources:
- connection_id_key: RTN8c/RTN9c "id/key null in SUSPENDED" -> RTN8d/RTN9d
  "id/key retained in SUSPENDED" (connect first, then suspend).
- connection_failures: RTN15g "no resume after connectionStateTtl"
  -> RTN14h "still attempts resume after the TTL", asserting every
  post-suspension reconnect carries resume=<connectionKey>.
- connection_recovery: RTN16g2 -> RTN16g3; the SUSPENDED case now expects
  a non-null recovery key.
- channel_properties: RTL15b1 "channelSerial cleared on suspended"
  -> RTL15b2 "retained in suspended"; detach/failed cases relabelled.
- channel_attach: RTL4j "ATTACH_RESUME set on reattach" -> "not set".
- proxy/connection_resume: Test 22 RTN15g/g2 -> RTN14h; the post-TTL
  reconnect now asserts the resume param is present.
- completion-status: matrix updated for RTN8d/RTN9d, RTN14h, RTN16g3,
  RTL15b2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@SimonWoolf
SimonWoolf force-pushed the uts-6.1.0-server-resumability branch from 3406716 to d0d1c02 Compare September 3, 2026 21:28
@SimonWoolf

Copy link
Copy Markdown
Member Author

oh, I forgot to merge this, sorry

Feels like RTN27d could be in the scope of this PR

Good point thx, I'll trim off the last clause. (it's one of those "spec points" that shouldn't really be a spec point because it's kindof flavour-text for an enum member rather than specifying behaviour, so I don't think it needs a break)

@SimonWoolf
SimonWoolf merged commit ba10845 into main Sep 3, 2026
2 checks passed
@SimonWoolf
SimonWoolf deleted the uts-6.1.0-server-resumability branch September 3, 2026 21:29
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey parameter goes from SetDisconnectedStateCommand and
SetSuspendedStateCommand. No caller sets it, and leaving it would let the violation back
in unnoticed. SetConnectingStateCommand keeps its own, still used by ConnectionClosingState
for RTN11b/RTN11d, where a clean connection is the intent.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey parameter goes from SetDisconnectedStateCommand and
SetSuspendedStateCommand. No caller sets it, and leaving it would let the violation back
in unnoticed. SetConnectingStateCommand keeps its own, still used by ConnectionClosingState
for RTN11b/RTN11d, where a clean connection is the intent.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey plumbing goes entirely - from SetDisconnectedStateCommand
and SetSuspendedStateCommand, whose violation it was, and from SetConnectingStateCommand and
HandleConnectingErrorCommand, where it no longer does anything. Leaving it would let the
violation back in unnoticed, and leave a retired mechanism looking load-bearing.
ConnectionClosingState was its last caller, for RTN11b/RTN11d's clean connection, and needs
it no longer: entering CLOSING now clears key and id for RTN8d/RTN9d before the emit, and the
single reader processes that before it can reach ClosingState.Connect(), so the following
CONNECTED finds no id to match and restarts the serial sequence under RTN15c7 regardless.
HandleConnectingErrorCommand's copy was never read by any handler.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey plumbing goes entirely - from SetDisconnectedStateCommand
and SetSuspendedStateCommand, whose violation it was, and from SetConnectingStateCommand and
HandleConnectingErrorCommand, where it no longer does anything. Leaving it would let the
violation back in unnoticed, and leave a retired mechanism looking load-bearing.
ConnectionClosingState was its last caller, for RTN11b/RTN11d's clean connection, and needs
it no longer: entering CLOSING now clears key and id for RTN8d/RTN9d before the emit, and the
single reader processes that before it can reach ClosingState.Connect(), so the following
CONNECTED finds no id to match and restarts the serial sequence under RTN15c7 regardless.
HandleConnectingErrorCommand's copy was never read by any handler.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey plumbing goes entirely - from SetDisconnectedStateCommand
and SetSuspendedStateCommand, whose violation it was, and from SetConnectingStateCommand and
HandleConnectingErrorCommand, where it no longer does anything. Leaving it would let the
violation back in unnoticed, and leave a retired mechanism looking load-bearing.
ConnectionClosingState was its last caller, for RTN11b/RTN11d's clean connection, and needs
it no longer: entering CLOSING now clears key and id for RTN8d/RTN9d before the emit, and the
single reader processes that before it can reach ClosingState.Connect(), so the following
CONNECTED finds no id to match and restarts the serial sequence under RTN15c7 regardless.
HandleConnectingErrorCommand's copy was never read by any handler.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey plumbing goes entirely - from SetDisconnectedStateCommand
and SetSuspendedStateCommand, whose violation it was, and from SetConnectingStateCommand and
HandleConnectingErrorCommand, where it no longer does anything. Leaving it would let the
violation back in unnoticed, and leave a retired mechanism looking load-bearing.
ConnectionClosingState was its last caller, for RTN11b/RTN11d's clean connection, and needs
it no longer: entering CLOSING now clears key and id for RTN8d/RTN9d before the emit, and the
single reader processes that before it can reach ClosingState.Connect(), so the following
CONNECTED finds no id to match and restarts the serial sequence under RTN15c7 regardless.
HandleConnectingErrorCommand's copy was never read by any handler.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey plumbing goes entirely - from SetDisconnectedStateCommand
and SetSuspendedStateCommand, whose violation it was, and from SetConnectingStateCommand and
HandleConnectingErrorCommand, where it no longer does anything. Leaving it would let the
violation back in unnoticed, and leave a retired mechanism looking load-bearing.
ConnectionClosingState was its last caller, for RTN11b/RTN11d's clean connection, and needs
it no longer: entering CLOSING now clears key and id for RTN8d/RTN9d before the emit, and the
single reader processes that before it can reach ClosingState.Connect(), so the following
CONNECTED finds no id to match and restarts the serial sequence under RTN15c7 regardless.
HandleConnectingErrorCommand's copy was never read by any handler.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey plumbing goes entirely - from SetDisconnectedStateCommand
and SetSuspendedStateCommand, whose violation it was, and from SetConnectingStateCommand and
HandleConnectingErrorCommand, where it no longer does anything. Leaving it would let the
violation back in unnoticed, and leave a retired mechanism looking load-bearing.
ConnectionClosingState was its last caller, for RTN11b/RTN11d's clean connection, and needs
it no longer: entering CLOSING now clears key and id for RTN8d/RTN9d before the emit, and the
single reader processes that before it can reach ClosingState.Connect(), so the following
CONNECTED finds no id to match and restarts the serial sequence under RTN15c7 regardless.
HandleConnectingErrorCommand's copy was never read by any handler.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN14h - implemented. Replaces RTN15g as of specification 6.1.0. The client discarded
its connection state once connectionStateTtl had passed and reconnected fresh, throwing
away a resume the server would still have honoured. DF1a settles the scope - the ttl is
no longer used to decide whether to resume at all - so the gate is general rather than
SUSPENDED-only, which is how ably-js reads it too.
RTN27c - fixed. DISCONNECTED is a state where "if the library was previously connected,
the next connect attempt will be an RTN15b resume attempt". Clearing the key on every
failed attempt made that false from the second attempt onwards.
RTN8d, RTN9d - fixed twice over. Both list only CLOSED, CLOSING and FAILED, so SUSPENDED
must keep the key and id; it cleared them. And all three of the states they do name cleared
after SetState, which is what emits the state change - inline, with no SynchronizationContext
installed - so the application was told it had reached a terminal state while Connection.Key
still read as a resumable key. The clear now happens before the transition, which also stops
it depending on a finally. Only a listener reading during the transition could observe this,
so it was inherited rather than introduced here.
RTN15g1, RTN15g2, RTN15g3 - deleted at 6.1.0. HasConnectionStateTtlPassed and its tests
go with them. The reattach RTN15g3 asked for is already unconditional under RTL3d.
RTL4j, RTL4j1, RTL4j2 - deleted at 6.1.0; SDKs need not set ATTACH_RESUME. Safe only
because RTL4c1 already sends channelSerial on ATTACH and RTL15b2 keeps it across a
suspend, so the reattach still carries a continuity signal. The Flag constant stays,
per TR3f.

The now-dead clearConnectionKey plumbing goes entirely - from SetDisconnectedStateCommand
and SetSuspendedStateCommand, whose violation it was, and from SetConnectingStateCommand and
HandleConnectingErrorCommand, where it no longer does anything. Leaving it would let the
violation back in unnoticed, and leave a retired mechanism looking load-bearing.
ConnectionClosingState was its last caller, for RTN11b/RTN11d's clean connection, and needs
it no longer: entering CLOSING now clears key and id for RTN8d/RTN9d before the emit, and the
single reader processes that before it can reach ClosingState.Connect(), so the following
CONNECTED finds no id to match and restarts the serial sequence under RTN15c7 regardless.
HandleConnectingErrorCommand's copy was never read by any handler.

RTN27d - the 6.1.0 edit left it describing the old model. It called SUSPENDED a state whose
"next connect attempt is a clean connection (not a resume attempt)", which RTN14h, RTN8d,
RTN9d and DF1a between them contradict; the commit that made those changes did not touch
RTN27 at all. ably/specification#511 has since amended the clause in place and dropped that
sentence, so this commit follows RTN27d rather than deviating from it. Amending in place is
the right shape for RTN27, which declares its states "mutually exclusive and exhaustive" -
tombstoning the clause would have left SUSPENDED undescribed. ably-js retains the key in
SUSPENDED too, citing RTN8d/RTN9d and RTN14h for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndyTWF added a commit to ably/ably-pubsub-dotnet that referenced this pull request Sep 10, 2026
RTN16g3 - implemented. Replaces RTN16g2, which listed SUSPENDED among the states where
createRecoveryKey returns null. RTN8d and RTN9d now keep the connectionKey through
SUSPENDED because RTN14h always attempts a resume, so the connection is still recoverable
there and the key has to be available to hand to another client. Withholding it left the
SDK holding a usable recovery key it would not surface, in the one prolonged-outage state
where handing recovery over is most useful.

RTN16i - fixed alongside it. GetChannelSerials filtered on ChannelState.Attached, and RTL3c
puts every channel into SUSPENDED when the connection suspends, so the key would have gone
out with no channelSerials at all - connection continuity without message continuity, and
nothing to tell the caller. Gated on the serial instead, as ably-js does; RTL15b2 already
keeps it through SUSPENDED.

RTN16g3 comes from ably/specification#511, which tombstones RTN16g2 and drops SUSPENDED from
the states where createRecoveryKey returns null. Written here before that merged, because it
is the direct consequence of RTN14h in the previous commit and shipping the two apart would
mean two behaviour changes for callers instead of one. ably-js has behaved this way since
2.27.0.

Co-Authored-By: Claude Opus 5 <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.

4 participants